aboutsummaryrefslogtreecommitdiff
path: root/libpod/lock/shm/shm_lock_test.go
blob: 41a150c59143a8da2e21f4444d9076e73f4bd274 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// +build linux

package shm

import (
	"fmt"
	"os"
	"runtime"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// All tests here are in the same process, which somewhat limits their utility
// The big intent of this package it multiprocess locking, which is really hard
// to test without actually having multiple processes...
// We can at least verify that the locks work within the local process.

var (
	// 4 * BITMAP_SIZE to ensure we have to traverse bitmaps
	numLocks = 4 * BitmapSize
)

const lockPath = "/libpod_test"

// We need a test main to ensure that the SHM is created before the tests run
func TestMain(m *testing.M) {
	shmLock, err := CreateSHMLock(lockPath, numLocks)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error creating SHM for tests: %v\n", err)
		os.Exit(-1)
	}

	// Close the SHM - every subsequent test will reopen
	if err := shmLock.Close(); err != nil {
		fmt.Fprintf(os.Stderr, "Error closing SHM locks: %v\n", err)
		os.Exit(-1)
	}

	exitCode := m.Run()

	// We need to remove the SHM segment to clean up after ourselves
	os.RemoveAll("/dev/shm/libpod_lock")

	os.Exit(exitCode)
}

func runLockTest(t *testing.T, testFunc func(*testing.T, *SHMLocks)) {
	locks, err := OpenSHMLock(lockPath, numLocks)
	if err != nil {
		t.Fatalf("Error opening locks: %v", err)
	}
	defer func() {
		// Deallocate all locks
		if err := locks.DeallocateAllSemaphores(); err != nil {
			t.Fatalf("Error deallocating semaphores: %v", err)
		}

		if err := locks.Close(); err != nil {
			t.Fatalf("Error closing locks: %v", err)
		}
	}()

	success := t.Run("locks", func(t *testing.T) {
		testFunc(t, locks)
	})
	if !success {
		t.Fail()
	}
}

// Test that creating an SHM with a bad size rounds up to a good size
func TestCreateNewSHMBadSizeRoundsUp(t *testing.T) {
	// Odd number, not a power of 2, should never be a word size on a system
	lock, err := CreateSHMLock("/test1", 7)
	assert.NoError(t, err)

	assert.Equal(t, lock.GetMaxLocks(), BitmapSize)

	if err := lock.Close(); err != nil {
		t.Fatalf("Error closing locks: %v", err)
	}
}

// Test that creating an SHM with 0 size fails
func TestCreateNewSHMZeroSize(t *testing.T) {
	_, err := CreateSHMLock("/test2", 0)
	assert.Error(t, err)
}

// Test that deallocating an unallocated lock errors
func TestDeallocateUnallocatedLockErrors(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		err := locks.DeallocateSemaphore(0)
		assert.Error(t, err)
	})
}

// Test that unlocking an unlocked lock fails
func TestUnlockingUnlockedLockFails(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		err := locks.UnlockSemaphore(0)
		assert.Error(t, err)
	})
}

// Test that locking and double-unlocking fails
func TestDoubleUnlockFails(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		err := locks.LockSemaphore(0)
		assert.NoError(t, err)

		err = locks.UnlockSemaphore(0)
		assert.NoError(t, err)

		err = locks.UnlockSemaphore(0)
		assert.Error(t, err)
	})
}

// Test allocating - lock - unlock - deallocate cycle, single lock
func TestLockLifecycleSingleLock(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		sem, err := locks.AllocateSemaphore()
		require.NoError(t, err)

		err = locks.LockSemaphore(sem)
		assert.NoError(t, err)

		err = locks.UnlockSemaphore(sem)
		assert.NoError(t, err)

		err = locks.DeallocateSemaphore(sem)
		assert.NoError(t, err)
	})
}

// Test allocate two locks returns different locks
func TestAllocateTwoLocksGetsDifferentLocks(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		sem1, err := locks.AllocateSemaphore()
		assert.NoError(t, err)

		sem2, err := locks.AllocateSemaphore()
		assert.NoError(t, err)

		assert.NotEqual(t, sem1, sem2)
	})
}

// Test allocate all locks successful and all are unique
func TestAllocateAllLocksSucceeds(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		sems := make(map[uint32]bool)
		var i uint32
		for i = 0; i < numLocks; i++ {
			sem, err := locks.AllocateSemaphore()
			assert.NoError(t, err)

			// Ensure the allocate semaphore is unique
			_, ok := sems[sem]
			assert.False(t, ok)

			sems[sem] = true
		}
	})
}

// Test allocating more than the given max fails
func TestAllocateTooManyLocksFails(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		// Allocate all locks
		var i uint32
		for i = 0; i < numLocks; i++ {
			_, err := locks.AllocateSemaphore()
			assert.NoError(t, err)
		}

		// Try and allocate one more
		_, err := locks.AllocateSemaphore()
		assert.Error(t, err)
	})
}

// Test allocating max locks, deallocating one, and then allocating again succeeds
func TestAllocateDeallocateCycle(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		// Allocate all locks
		var i uint32
		for i = 0; i < numLocks; i++ {
			_, err := locks.AllocateSemaphore()
			assert.NoError(t, err)
		}

		// Now loop through again, deallocating and reallocating.
		// Each time we free 1 semaphore, allocate again, and make sure
		// we get the same semaphore back.
		var j uint32
		for j = 0; j < numLocks; j++ {
			err := locks.DeallocateSemaphore(j)
			assert.NoError(t, err)

			newSem, err := locks.AllocateSemaphore()
			assert.NoError(t, err)
			assert.Equal(t, j, newSem)
		}
	})
}

// Test that DeallocateAllSemaphores deallocates all semaphores
func TestDeallocateAllSemaphoresDeallocatesAll(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		// Allocate a lock
		locks1, err := locks.AllocateSemaphore()
		assert.NoError(t, err)

		// Free all locks
		err = locks.DeallocateAllSemaphores()
		assert.NoError(t, err)

		// Allocate another lock
		locks2, err := locks.AllocateSemaphore()
		assert.NoError(t, err)

		assert.Equal(t, locks1, locks2)
	})
}

// Test that locks actually lock
func TestLockSemaphoreActuallyLocks(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		// This entire test is very ugly - lots of sleeps to try and get
		// things to occur in the right order.
		// It also doesn't even exercise the multiprocess nature of the
		// locks.

		// Get the current time
		startTime := time.Now()

		// Start a goroutine to take the lock and then release it after
		// a second.
		go func() {
			err := locks.LockSemaphore(0)
			assert.NoError(t, err)

			time.Sleep(1 * time.Second)

			err = locks.UnlockSemaphore(0)
			assert.NoError(t, err)
		}()

		// Sleep for a quarter of a second to give the goroutine time
		// to kick off and grab the lock
		time.Sleep(250 * time.Millisecond)

		// Take the lock
		err := locks.LockSemaphore(0)
		assert.NoError(t, err)

		// Get the current time
		endTime := time.Now()

		// Verify that at least 1 second has passed since start
		duration := endTime.Sub(startTime)
		assert.True(t, duration.Seconds() > 1.0)
	})
}

// Test that locking and unlocking two semaphores succeeds
// Ensures that runtime.LockOSThread() is doing its job
func TestLockAndUnlockTwoSemaphore(t *testing.T) {
	runLockTest(t, func(t *testing.T, locks *SHMLocks) {
		err := locks.LockSemaphore(5)
		assert.NoError(t, err)

		err = locks.LockSemaphore(6)
		assert.NoError(t, err)

		err = locks.UnlockSemaphore(6)
		assert.NoError(t, err)

		// Now yield scheduling
		// To try and get us on another OS thread
		runtime.Gosched()

		// And unlock the last semaphore
		// If we are in a different OS thread, this should fail.
		// However, runtime.UnlockOSThread() should guarantee we are not
		err = locks.UnlockSemaphore(5)
		assert.NoError(t, err)
	})
}