mirror of
				https://github.com/taigrr/bitcask
				synced 2025-01-18 04:03:17 -08:00 
			
		
		
		
	Use package github.com/gofrs/flock as flock implementation. (#224)
Supercesd #219 after rebasing on master after migrating off Github. Co-authored-by: Nicolò Santamaria <nicolo.santamaria@protonmail.com> Co-authored-by: James Mills <prologic@shortcircuit.net.au> Co-authored-by: Tai Groot <taigrr@noreply@mills.io> Reviewed-on: https://git.mills.io/prologic/bitcask/pulls/224 Co-authored-by: James Mills <prologic@noreply@mills.io> Co-committed-by: James Mills <prologic@noreply@mills.io>
This commit is contained in:
		
							parent
							
								
									a49bbf666a
								
							
						
					
					
						commit
						5e4d863ab7
					
				
							
								
								
									
										23
									
								
								bitcask.go
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								bitcask.go
									
									
									
									
									
								
							| @ -13,8 +13,10 @@ import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/gofrs/flock" | ||||
| 	art "github.com/plar/go-adaptive-radix-tree" | ||||
| 	"git.mills.io/prologic/bitcask/flock" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 
 | ||||
| 	"git.mills.io/prologic/bitcask/internal" | ||||
| 	"git.mills.io/prologic/bitcask/internal/config" | ||||
| 	"git.mills.io/prologic/bitcask/internal/data" | ||||
| @ -22,7 +24,6 @@ import ( | ||||
| 	"git.mills.io/prologic/bitcask/internal/index" | ||||
| 	"git.mills.io/prologic/bitcask/internal/metadata" | ||||
| 	"git.mills.io/prologic/bitcask/scripts/migrations" | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -68,10 +69,8 @@ var ( | ||||
| // and in-memory hash of key/value pairs as per the Bitcask paper and seen | ||||
| // in the Riak database. | ||||
| type Bitcask struct { | ||||
| 	mu sync.RWMutex | ||||
| 
 | ||||
| 	*flock.Flock | ||||
| 
 | ||||
| 	mu         sync.RWMutex | ||||
| 	flock      *flock.Flock | ||||
| 	config     *config.Config | ||||
| 	options    []Option | ||||
| 	path       string | ||||
| @ -114,7 +113,7 @@ func (b *Bitcask) Close() error { | ||||
| 	b.mu.RLock() | ||||
| 	defer func() { | ||||
| 		b.mu.RUnlock() | ||||
| 		b.Flock.Unlock() | ||||
| 		b.flock.Unlock() | ||||
| 	}() | ||||
| 
 | ||||
| 	return b.close() | ||||
| @ -669,6 +668,10 @@ func (b *Bitcask) Merge() error { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, file := range files { | ||||
| 		// see #225 | ||||
| 		if file.Name() == lockfile { | ||||
| 			continue | ||||
| 		} | ||||
| 		err := os.Rename( | ||||
| 			path.Join([]string{mdb.path, file.Name()}...), | ||||
| 			path.Join([]string{b.path, file.Name()}...), | ||||
| @ -723,7 +726,7 @@ func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 	} | ||||
| 
 | ||||
| 	bitcask := &Bitcask{ | ||||
| 		Flock:      flock.New(filepath.Join(path, lockfile)), | ||||
| 		flock:      flock.New(filepath.Join(path, lockfile)), | ||||
| 		config:     cfg, | ||||
| 		options:    options, | ||||
| 		path:       path, | ||||
| @ -732,12 +735,12 @@ func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 		metadata:   meta, | ||||
| 	} | ||||
| 
 | ||||
| 	locked, err := bitcask.Flock.TryLock() | ||||
| 	ok, err := bitcask.flock.TryLock() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if !locked { | ||||
| 	if !ok { | ||||
| 		return nil, ErrDatabaseLocked | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -54,13 +54,6 @@ func (s *server) handleSet(cmd redcon.Command, conn redcon.Conn) { | ||||
| 		ttl = &d | ||||
| 	} | ||||
| 
 | ||||
| 	err := s.db.Lock() | ||||
| 	if err != nil { | ||||
| 		conn.WriteError("ERR " + fmt.Errorf("failed to lock db: %v", err).Error() + "") | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.db.Unlock() | ||||
| 
 | ||||
| 	if ttl != nil { | ||||
| 		if err := s.db.PutWithTTL(key, value, *ttl); err != nil { | ||||
| 			conn.WriteString(fmt.Sprintf("ERR: %s", err)) | ||||
| @ -82,13 +75,6 @@ func (s *server) handleGet(cmd redcon.Command, conn redcon.Conn) { | ||||
| 
 | ||||
| 	key := cmd.Args[1] | ||||
| 
 | ||||
| 	err := s.db.Lock() | ||||
| 	if err != nil { | ||||
| 		conn.WriteError("ERR " + fmt.Errorf("failed to lock db: %v", err).Error() + "") | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.db.Unlock() | ||||
| 
 | ||||
| 	value, err := s.db.Get(key) | ||||
| 	if err != nil { | ||||
| 		conn.WriteNull() | ||||
| @ -98,13 +84,6 @@ func (s *server) handleGet(cmd redcon.Command, conn redcon.Conn) { | ||||
| } | ||||
| 
 | ||||
| func (s *server) handleKeys(cmd redcon.Command, conn redcon.Conn) { | ||||
| 	err := s.db.Lock() | ||||
| 	if err != nil { | ||||
| 		conn.WriteError("ERR " + fmt.Errorf("failed to lock db: %v", err).Error() + "") | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.db.Unlock() | ||||
| 
 | ||||
| 	conn.WriteArray(s.db.Len()) | ||||
| 	for key := range s.db.Keys() { | ||||
| 		conn.WriteBulk(key) | ||||
| @ -119,13 +98,6 @@ func (s *server) handleExists(cmd redcon.Command, conn redcon.Conn) { | ||||
| 
 | ||||
| 	key := cmd.Args[1] | ||||
| 
 | ||||
| 	err := s.db.Lock() | ||||
| 	if err != nil { | ||||
| 		conn.WriteError("ERR " + fmt.Errorf("failed to lock db: %v", err).Error() + "") | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.db.Unlock() | ||||
| 
 | ||||
| 	if s.db.Has(key) { | ||||
| 		conn.WriteInt(1) | ||||
| 	} else { | ||||
| @ -141,13 +113,6 @@ func (s *server) handleDel(cmd redcon.Command, conn redcon.Conn) { | ||||
| 
 | ||||
| 	key := cmd.Args[1] | ||||
| 
 | ||||
| 	err := s.db.Lock() | ||||
| 	if err != nil { | ||||
| 		conn.WriteError("ERR " + fmt.Errorf("failed to lock db: %v", err).Error() + "") | ||||
| 		return | ||||
| 	} | ||||
| 	defer s.db.Unlock() | ||||
| 
 | ||||
| 	if err := s.db.Delete(key); err != nil { | ||||
| 		conn.WriteInt(0) | ||||
| 	} else { | ||||
|  | ||||
| @ -1,97 +0,0 @@ | ||||
| package flock | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| type Flock struct { | ||||
| 	path string | ||||
| 	m    sync.Mutex | ||||
| 	fh   *os.File | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	ErrAlreadyLocked = errors.New("Double lock: already own the lock") | ||||
| 	ErrLockFailed    = errors.New("Could not acquire lock") | ||||
| 	ErrLockNotHeld   = errors.New("Could not unlock, lock is not held") | ||||
| 
 | ||||
| 	ErrInodeChangedAtPath = errors.New("Inode changed at path") | ||||
| ) | ||||
| 
 | ||||
| // New returns a new instance of *Flock. The only parameter | ||||
| // it takes is the path to the desired lockfile. | ||||
| func New(path string) *Flock { | ||||
| 	return &Flock{path: path} | ||||
| } | ||||
| 
 | ||||
| // Path returns the file path linked to this lock. | ||||
| func (f *Flock) Path() string { | ||||
| 	return f.path | ||||
| } | ||||
| 
 | ||||
| // Lock will acquire the lock. This function may block indefinitely if some other process holds the lock. For a non-blocking version, see Flock.TryLock(). | ||||
| func (f *Flock) Lock() error { | ||||
| 	f.m.Lock() | ||||
| 	defer f.m.Unlock() | ||||
| 
 | ||||
| 	if f.fh != nil { | ||||
| 		return ErrAlreadyLocked | ||||
| 	} | ||||
| 
 | ||||
| 	var fh *os.File | ||||
| 
 | ||||
| 	fh, err := lock_sys(f.path, false) | ||||
| 	// treat "ErrInodeChangedAtPath" as "some other process holds the lock, retry locking" | ||||
| 	for err == ErrInodeChangedAtPath { | ||||
| 		fh, err = lock_sys(f.path, false) | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if fh == nil { | ||||
| 		return ErrLockFailed | ||||
| 	} | ||||
| 
 | ||||
| 	f.fh = fh | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // TryLock will try to acquire the lock, and returns immediately if the lock is already owned by another process. | ||||
| func (f *Flock) TryLock() (bool, error) { | ||||
| 	f.m.Lock() | ||||
| 	defer f.m.Unlock() | ||||
| 
 | ||||
| 	if f.fh != nil { | ||||
| 		return false, ErrAlreadyLocked | ||||
| 	} | ||||
| 
 | ||||
| 	fh, err := lock_sys(f.path, true) | ||||
| 	if err != nil { | ||||
| 		return false, ErrLockFailed | ||||
| 	} | ||||
| 
 | ||||
| 	f.fh = fh | ||||
| 	return true, nil | ||||
| } | ||||
| 
 | ||||
| // Unlock removes the lock file from disk and releases the lock. | ||||
| // Whatever the result of `.Unlock()`, the caller must assume that it does not hold the lock anymore. | ||||
| func (f *Flock) Unlock() error { | ||||
| 	f.m.Lock() | ||||
| 	defer f.m.Unlock() | ||||
| 
 | ||||
| 	if f.fh == nil { | ||||
| 		return ErrLockNotHeld | ||||
| 	} | ||||
| 
 | ||||
| 	err1 := rm_if_match(f.fh, f.path) | ||||
| 	err2 := f.fh.Close() | ||||
| 
 | ||||
| 	if err1 != nil { | ||||
| 		return err1 | ||||
| 	} | ||||
| 	return err2 | ||||
| } | ||||
| @ -1,121 +0,0 @@ | ||||
| package flock | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| // WARNING : this test will delete the file located at "testLockPath". Choose an adequate temporary file name. | ||||
| const testLockPath = "/tmp/bitcask_unit_test_lock" // file path to use for the lock | ||||
| 
 | ||||
| func TestTryLock(t *testing.T) { | ||||
| 	// test that basic locking functionalities are consistent | ||||
| 
 | ||||
| 	// make sure there is no present lock when starting this test | ||||
| 	os.Remove(testLockPath) | ||||
| 
 | ||||
| 	assert := assert.New(t) | ||||
| 
 | ||||
| 	lock1 := New(testLockPath) | ||||
| 	lock2 := New(testLockPath) | ||||
| 
 | ||||
| 	// 1- take the first lock | ||||
| 	locked1, err := lock1.TryLock() | ||||
| 	assert.True(locked1) | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	// 2- check that the second lock cannot acquire the lock | ||||
| 	locked2, err := lock2.TryLock() | ||||
| 	assert.False(locked2) | ||||
| 	assert.Error(err) | ||||
| 
 | ||||
| 	// 3- release the first lock | ||||
| 	err = lock1.Unlock() | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	// 4- check that the second lock can acquire and then release the lock without error | ||||
| 	locked2, err = lock2.TryLock() | ||||
| 	assert.True(locked2) | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	err = lock2.Unlock() | ||||
| 	assert.NoError(err) | ||||
| } | ||||
| 
 | ||||
| func TestLock(t *testing.T) { | ||||
| 	assert := assert.New(t) | ||||
| 
 | ||||
| 	// make sure there is no present lock when starting this test | ||||
| 	os.Remove(testLockPath) | ||||
| 
 | ||||
| 	syncChan := make(chan bool) | ||||
| 
 | ||||
| 	// main goroutine: take lock on testPath | ||||
| 	lock := New(testLockPath) | ||||
| 
 | ||||
| 	err := lock.Lock() | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	go func() { | ||||
| 		// sub routine: | ||||
| 		lock := New(testLockPath) | ||||
| 
 | ||||
| 		// before entering the block '.Lock()' call, signal we are about to do it | ||||
| 		// see below : the main goroutine will wait for a small delay before releasing the lock | ||||
| 		syncChan <- true | ||||
| 		// '.Lock()' should ultimately return without error : | ||||
| 		err := lock.Lock() | ||||
| 		assert.NoError(err) | ||||
| 
 | ||||
| 		err = lock.Unlock() | ||||
| 		assert.NoError(err) | ||||
| 
 | ||||
| 		close(syncChan) | ||||
| 	}() | ||||
| 
 | ||||
| 	// wait for the "ready" signal from the sub routine, | ||||
| 	<-syncChan | ||||
| 
 | ||||
| 	// after that signal wait for a small delay before releasing the lock | ||||
| 	<-time.After(100 * time.Microsecond) | ||||
| 	err = lock.Unlock() | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	// wait for the sub routine to finish | ||||
| 	<-syncChan | ||||
| } | ||||
| 
 | ||||
| func TestErrorConditions(t *testing.T) { | ||||
| 	// error conditions implemented in this version : | ||||
| 	// - you can't release a lock you do not hold | ||||
| 	// - you can't lock twice the same lock | ||||
| 
 | ||||
| 	// -- setup | ||||
| 	assert := assert.New(t) | ||||
| 
 | ||||
| 	// make sure there is no present lock when starting this test | ||||
| 	os.Remove(testLockPath) | ||||
| 
 | ||||
| 	lock := New(testLockPath) | ||||
| 
 | ||||
| 	// -- run tests : | ||||
| 
 | ||||
| 	err := lock.Unlock() | ||||
| 	assert.Error(err, "you can't release a lock you do not hold") | ||||
| 
 | ||||
| 	// take the lock once: | ||||
| 	lock.TryLock() | ||||
| 
 | ||||
| 	locked, err := lock.TryLock() | ||||
| 	assert.False(locked) | ||||
| 	assert.Error(err, "you can't lock twice the same lock (using .TryLock())") | ||||
| 
 | ||||
| 	err = lock.Lock() | ||||
| 	assert.Error(err, "you can't lock twice the same lock (using .Lock())") | ||||
| 
 | ||||
| 	// -- teardown | ||||
| 	lock.Unlock() | ||||
| } | ||||
| @ -1,79 +0,0 @@ | ||||
| // +build !aix,!windows | ||||
| 
 | ||||
| package flock | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 
 | ||||
| 	"golang.org/x/sys/unix" | ||||
| ) | ||||
| 
 | ||||
| func lock_sys(path string, nonBlocking bool) (_ *os.File, err error) { | ||||
| 	var fh *os.File | ||||
| 
 | ||||
| 	fh, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	defer func() { | ||||
| 		if err != nil { | ||||
| 			fh.Close() | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	flag := unix.LOCK_EX | ||||
| 	if nonBlocking { | ||||
| 		flag |= unix.LOCK_NB | ||||
| 	} | ||||
| 
 | ||||
| 	err = unix.Flock(int(fh.Fd()), flag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if !sameInodes(fh, path) { | ||||
| 		return nil, ErrInodeChangedAtPath | ||||
| 	} | ||||
| 
 | ||||
| 	return fh, nil | ||||
| } | ||||
| 
 | ||||
| func rm_if_match(fh *os.File, path string) error { | ||||
| 	// Sanity check : | ||||
| 	// before running "rm", check that the file pointed at by the | ||||
| 	// filehandle has the same inode as the path in the filesystem | ||||
| 	// | ||||
| 	// If this sanity check doesn't pass, store a "ErrInodeChangedAtPath" error, | ||||
| 	// if the check passes, run os.Remove, and store the error if any. | ||||
| 	// | ||||
| 	// note : this sanity check is in no way atomic, but : | ||||
| 	//   - as long as only cooperative processes are involved, it will work as intended | ||||
| 	//   - it allows to avoid 99.9% the major pitfall case: "root user forcefully removed the lockfile" | ||||
| 
 | ||||
| 	if !sameInodes(fh, path) { | ||||
| 		return ErrInodeChangedAtPath | ||||
| 	} | ||||
| 
 | ||||
| 	return os.Remove(path) | ||||
| } | ||||
| 
 | ||||
| func sameInodes(f *os.File, path string) bool { | ||||
| 	// get inode from opened file f: | ||||
| 	var fstat unix.Stat_t | ||||
| 	err := unix.Fstat(int(f.Fd()), &fstat) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	fileIno := fstat.Ino | ||||
| 
 | ||||
| 	// get inode for path on disk: | ||||
| 	var dstat unix.Stat_t | ||||
| 	err = unix.Stat(path, &dstat) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	pathIno := dstat.Ino | ||||
| 
 | ||||
| 	return pathIno == fileIno | ||||
| } | ||||
| @ -1,236 +0,0 @@ | ||||
| package flock | ||||
| 
 | ||||
| // the "nd" in "nd_test.go" stands for non-deterministic | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"syscall" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| // The two tests in this file are test some concurrency scenarios : | ||||
| // 1- TestRaceLock() runs several threads racing for the same lock | ||||
| // 2- TestShatteredLock() runs racing racing threads, along with another threads which forcibly remove the file from disk | ||||
| // | ||||
| // Note that these tests are non-deterministic : the coverage produced by each test depends | ||||
| // on how the runtime chooses to schedule the concurrent goroutines. | ||||
| 
 | ||||
| var lockerCount int64 | ||||
| 
 | ||||
| // lockAndCount tries to take a lock on "lockpath" | ||||
| // if it fails : it returns 0 and stop there | ||||
| // if it succeeds : | ||||
| //   1- it sets a defer function to release the lock in the same fashion as "func (b *Bitcask) Close()" | ||||
| //   2- it increments the shared "lockerCount" above | ||||
| //   3- it waits for a short amount of time | ||||
| //   4- it decrements "lockerCount" | ||||
| //   5- it returns the value it has seen at step 2. | ||||
| // | ||||
| // If the locking and unlocking behave as we expect them to, | ||||
| // instructions 1-5 should be in a critical section, | ||||
| // and the only possible value at step 2 should be "1". | ||||
| // | ||||
| // Returning a value > 0 indicates this function successfully acquired the lock, | ||||
| // returning a value == 0 indicates that TryLock failed. | ||||
| 
 | ||||
| func lockAndCount(lockpath string) int64 { | ||||
| 	lock := New(lockpath) | ||||
| 	ok, _ := lock.TryLock() | ||||
| 	if !ok { | ||||
| 		return 0 | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		lock.Unlock() | ||||
| 	}() | ||||
| 
 | ||||
| 	x := atomic.AddInt64(&lockerCount, 1) | ||||
| 	// emulate a workload : | ||||
| 	<-time.After(1 * time.Microsecond) | ||||
| 	atomic.AddInt64(&lockerCount, -1) | ||||
| 
 | ||||
| 	return x | ||||
| } | ||||
| 
 | ||||
| // locker will call the lock function above in a loop, until one of the following holds : | ||||
| //  - reading from the "timeout" channel doesn't block | ||||
| //  - the number of calls to "lock()" that indicate the lock was successfully taken reaches "successfullLockCount" | ||||
| func locker(t *testing.T, id int, lockPath string, successfulLockCount int, timeout <-chan struct{}) { | ||||
| 	timedOut := false | ||||
| 
 | ||||
| 	failCount := 0 | ||||
| 	max := int64(0) | ||||
| 
 | ||||
| lockloop: | ||||
| 	for successfulLockCount > 0 { | ||||
| 		select { | ||||
| 		case <-timeout: | ||||
| 			timedOut = true | ||||
| 			break lockloop | ||||
| 		default: | ||||
| 		} | ||||
| 
 | ||||
| 		x := lockAndCount(lockPath) | ||||
| 
 | ||||
| 		if x > 0 { | ||||
| 			// if x indicates the lock was taken : decrement the counter | ||||
| 			successfulLockCount-- | ||||
| 		} | ||||
| 
 | ||||
| 		if x > 1 { | ||||
| 			// if x indicates an invalid value : increase the failCount and update max accordingly | ||||
| 			failCount++ | ||||
| 			if x > max { | ||||
| 				max = x | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// check failure cases : | ||||
| 	if timedOut { | ||||
| 		t.Fail() | ||||
| 		t.Logf("[runner %02d] timed out", id) | ||||
| 	} | ||||
| 	if failCount > 0 { | ||||
| 		t.Fail() | ||||
| 		t.Logf("[runner %02d] lockCounter was > 1 on %2.d occasions, max seen value was %2.d", id, failCount, max) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TestRaceLock checks that no error occurs when several concurrent actors (goroutines in this case) race for the same lock. | ||||
| func TestRaceLock(t *testing.T) { | ||||
| 	// test parameters, written in code : | ||||
| 	//   you may want to tweak these values for testing | ||||
| 
 | ||||
| 	goroutines := 20          // number of concurrent "locker" goroutines to launch | ||||
| 	successfulLockCount := 50 // how many times a "locker" will successfully take the lock before halting | ||||
| 
 | ||||
| 	// make sure there is no present lock when startng this test | ||||
| 	os.Remove(testLockPath) | ||||
| 
 | ||||
| 	// timeout implemented in code | ||||
| 	// (the lock acquisition depends on the behavior of the filesystem, | ||||
| 	//	avoid sending CI in endless loop if something fishy happens on the test server ...) | ||||
| 	// tweak this value if needed ; comment out the "close(ch)" instruction below | ||||
| 	timeout := 10 * time.Second | ||||
| 	ch := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		<-time.After(timeout) | ||||
| 		close(ch) | ||||
| 	}() | ||||
| 
 | ||||
| 	wg := &sync.WaitGroup{} | ||||
| 	wg.Add(goroutines) | ||||
| 
 | ||||
| 	for i := 0; i < goroutines; i++ { | ||||
| 		go func(id int) { | ||||
| 			locker(t, id, testLockPath, successfulLockCount, ch) | ||||
| 			wg.Done() | ||||
| 		}(i) | ||||
| 	} | ||||
| 
 | ||||
| 	wg.Wait() | ||||
| } | ||||
| 
 | ||||
| func isExpectedError(err error) bool { | ||||
| 	switch { | ||||
| 	case err == nil: | ||||
| 		return true | ||||
| 	case err == ErrInodeChangedAtPath: | ||||
| 		return true | ||||
| 	case errors.Is(err, syscall.ENOENT): | ||||
| 		return true | ||||
| 
 | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TestShatteredLock runs concurrent goroutines on one lock, with an extra goroutine | ||||
| // which removes the lock file from disk without checking the locks | ||||
| // (e.g: a user who would run 'rm lockfile' in a loop while the program is running). | ||||
| // | ||||
| // In this scenario, errors may occur on .Unlock() ; this test checks that only errors | ||||
| // relating to the file being deleted occur. | ||||
| // | ||||
| // This test additionally logs the number of errors that occurred, grouped by error message. | ||||
| func TestShatteredLock(t *testing.T) { | ||||
| 	// test parameters, written in code : | ||||
| 	//   you may want to tweak these values for testing | ||||
| 
 | ||||
| 	goroutines := 4           // number of concurrent "locker" and "remover" goroutines to launch | ||||
| 	successfulLockCount := 10 // how many times a "locker" will successfully take the lock before halting | ||||
| 
 | ||||
| 	// make sure there is no present lock when startng this test | ||||
| 	os.Remove(testLockPath) | ||||
| 	assert := assert.New(t) | ||||
| 
 | ||||
| 	wg := &sync.WaitGroup{} | ||||
| 	wg.Add(goroutines) | ||||
| 
 | ||||
| 	stopChan := make(chan struct{}) | ||||
| 
 | ||||
| 	errChan := make(chan error, 10) | ||||
| 
 | ||||
| 	for i := 0; i < goroutines; i++ { | ||||
| 		go func(id int, count int) { | ||||
| 			for count > 0 { | ||||
| 				lock := New(testLockPath) | ||||
| 				ok, _ := lock.TryLock() | ||||
| 				if !ok { | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				count-- | ||||
| 				err := lock.Unlock() | ||||
| 				if !isExpectedError(err) { | ||||
| 					assert.Fail("goroutine %d - unexpected error: %v", id, err) | ||||
| 				} | ||||
| 
 | ||||
| 				if err != nil { | ||||
| 					errChan <- err | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			wg.Done() | ||||
| 		}(i, successfulLockCount) | ||||
| 	} | ||||
| 
 | ||||
| 	var wgCompanion = &sync.WaitGroup{} | ||||
| 	wgCompanion.Add(2) | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer wgCompanion.Done() | ||||
| 		for { | ||||
| 			os.Remove(testLockPath) | ||||
| 
 | ||||
| 			select { | ||||
| 			case <-stopChan: | ||||
| 				return | ||||
| 			default: | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	var errs = make(map[string]int) | ||||
| 	go func() { | ||||
| 		for err := range errChan { | ||||
| 			errs[err.Error()]++ | ||||
| 		} | ||||
| 		wgCompanion.Done() | ||||
| 	}() | ||||
| 
 | ||||
| 	wg.Wait() | ||||
| 	close(stopChan) | ||||
| 	close(errChan) | ||||
| 	wgCompanion.Wait() | ||||
| 
 | ||||
| 	for err, count := range errs { | ||||
| 		t.Logf("  seen %d times: %s", count, err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -3,6 +3,7 @@ module git.mills.io/prologic/bitcask | ||||
| go 1.13 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/gofrs/flock v0.8.0 | ||||
| 	github.com/pkg/errors v0.9.1 | ||||
| 	github.com/plar/go-adaptive-radix-tree v1.0.4 | ||||
| 	github.com/sirupsen/logrus v1.8.1 | ||||
| @ -13,5 +14,4 @@ require ( | ||||
| 	github.com/stretchr/testify v1.7.0 | ||||
| 	github.com/tidwall/redcon v1.4.1 | ||||
| 	golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85 | ||||
| 	golang.org/x/sys v0.0.0-20210510120138-977fb7262007 | ||||
| ) | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -93,6 +93,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 | ||||
| github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||
| github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= | ||||
| github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user