diff --git a/vm/concurrent_rw_lock.go b/vm/concurrent_rw_lock.go new file mode 100644 index 000000000..86e612432 --- /dev/null +++ b/vm/concurrent_rw_lock.go @@ -0,0 +1,279 @@ +package vm + +import ( + "sync" + + "github.com/goby-lang/goby/vm/errors" +) + +// ConcurrentRWLockObject is a Readers-Writer Lock (readers can concurrently put a lock, while a +// writer requires exclusive access). +// +// The implementation internally uses Go's `sync.RWLock` type. +// +// ```ruby +// require 'concurrent/rw_lock' +// lock = Concurrent::RWLock.new +// lock.with_read_lock do +// # critical section +// end +// lock.with_write_lock do +// # critical section +// end +// ``` +// +type ConcurrentRWLockObject struct { + *baseObj + mutex sync.RWMutex +} + +// Class methods -------------------------------------------------------- +func builtinConcurrentRWLockClassMethods() []*BuiltinMethodObject { + return []*BuiltinMethodObject{ + { + Name: "new", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if len(args) != 0 { + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + return t.vm.initConcurrentRWLockObject() + } + }, + }, + } +} + +// Instance methods ----------------------------------------------------- +func builtinConcurrentRWLockInstanceMethods() []*BuiltinMethodObject { + return []*BuiltinMethodObject{ + { + // Acquires a read lock. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.acquire_read_lock + // # critical section + // lock.release_read_lock + // + // @return [nil] + // ``` + Name: "acquire_read_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.RLock(); + + return NULL + } + }, + }, + { + // Acquires a write lock. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.acquire_write_lock + // # critical section + // lock.release_write_lock + // + // @return [nil] + // ``` + Name: "acquire_write_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.Lock(); + + return NULL + } + }, + }, + { + // Releases a read lock. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.acquire_read_lock + // # critical section + // lock.release_read_lock + // + // @return [nil] + // ``` + Name: "release_read_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.RUnlock(); + + return NULL + } + }, + }, + { + // Releases a write lock. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.acquire_write_lock + // # critical section + // lock.release_write_lock + // + // @return [nil] + // ``` + Name: "release_write_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.Unlock(); + + return NULL + } + }, + }, + { + // Executes the block with a read lock. + // The lock is freed upon exiting the block. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.with_read_lock do + // # critical section + // end + // + // @return [Object] the yielded value of the block. + // ``` + Name: "with_read_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if blockFrame == nil { + return t.vm.initErrorObject(errors.InternalError, sourceLine, errors.CantYieldWithoutBlockFormat) + } + + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.RLock(); + + blockReturnValue := t.builtinMethodYield(blockFrame).Target + + lockObject.mutex.RUnlock(); + + return blockReturnValue + } + }, + }, + { + // Executes the block with a write lock. + // The lock is freed upon exiting the block. + // + // ```Ruby + // lock = Concurrent::RWLock.new + // lock.with_write_lock do + // # critical section + // end + // + // @return [Object] the yielded value of the block. + // ``` + Name: "with_write_lock", + Fn: func(receiver Object, sourceLine int) builtinMethodBody { + return func(t *thread, args []Object, blockFrame *normalCallFrame) Object { + if blockFrame == nil { + return t.vm.initErrorObject(errors.InternalError, sourceLine, errors.CantYieldWithoutBlockFormat) + } + + if len(args) != 0 { + t.callFrameStack.pop() + + return t.vm.initErrorObject(errors.ArgumentError, sourceLine, "Expected 0 arguments, got %d", len(args)) + } + + lockObject := receiver.(*ConcurrentRWLockObject) + + lockObject.mutex.Lock(); + + blockReturnValue := t.builtinMethodYield(blockFrame).Target + + lockObject.mutex.Unlock(); + + return blockReturnValue + } + }, + }, + } +} + +// Internal functions =================================================== + +// Functions for initialization ----------------------------------------- + +func (vm *VM) initConcurrentRWLockObject() *ConcurrentRWLockObject { + concurrentModule := vm.loadConstant("Concurrent", true) + lockClass := concurrentModule.getClassConstant("RWLock") + + return &ConcurrentRWLockObject{ + baseObj: &baseObj{class: lockClass}, + mutex: sync.RWMutex{}, + } +} + +func initConcurrentRWLockClass(vm *VM) { + concurrentModule := vm.loadConstant("Concurrent", true) + lockClass := vm.initializeClass("RWLock", false) + + lockClass.setBuiltinMethods(builtinConcurrentRWLockInstanceMethods(), false) + lockClass.setBuiltinMethods(builtinConcurrentRWLockClassMethods(), true) + + concurrentModule.setClassConstant(lockClass) +} + +// Polymorphic helper functions ----------------------------------------- + +// Value returns the object +func (lock *ConcurrentRWLockObject) Value() interface{} { + return lock.mutex +} + +// toString returns the object's name as the string format +func (lock *ConcurrentRWLockObject) toString() string { + return "" +} + +// toJSON just delegates to toString +func (lock *ConcurrentRWLockObject) toJSON() string { + return lock.toString() +} diff --git a/vm/concurrent_rw_lock_test.go b/vm/concurrent_rw_lock_test.go new file mode 100644 index 000000000..175c8ade6 --- /dev/null +++ b/vm/concurrent_rw_lock_test.go @@ -0,0 +1,309 @@ +package vm + +import ( + "os" + "testing" +) + +// Failures + +func TestRWLockNewMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new(5) + `, "ArgumentError: Expected 0 arguments, got 1", 3, 1}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, tt.expectedCFP) + v.checkSP(t, i, 1) + } +} + +func TestRWLockAcquireReadLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.acquire_read_lock(5) + `, "ArgumentError: Expected 0 arguments, got 1", 3, 0}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, tt.expectedCFP) + v.checkSP(t, i, 1) + } +} + +func TestRWLockReleaseReadLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.release_read_lock(5) + `, "ArgumentError: Expected 0 arguments, got 1", 3, 1}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestRWLockAcquireWriteLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.acquire_write_lock(5) + `, "ArgumentError: Expected 0 arguments, got 1", 3, 1}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestRWLockReleaseWriteLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.release_write_lock(5) + `, "ArgumentError: Expected 0 arguments, got 1", 3, 1}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) + } +} + +func TestRWLockWithReadLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.with_read_lock + `, "InternalError: Can't yield without a block", 3, 1}, + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.with_read_lock(5) do end + `, "ArgumentError: Expected 0 arguments, got 1", 3, 0}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, tt.expectedCFP) + v.checkSP(t, i, 1) + } +} + +func TestRWLockWithWriteLockMethodFail(t *testing.T) { + testsFail := []errorTestCase{ + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.with_write_lock + `, "InternalError: Can't yield without a block", 3, 1}, + {` + require 'concurrent/rw_lock' + Concurrent::RWLock.new.with_write_lock(5) do end + `, "ArgumentError: Expected 0 arguments, got 1", 3, 0}, + } + + for i, tt := range testsFail { + v := initTestVM() + evaluated := v.testEval(t, tt.input, getFilename()) + checkErrorMsg(t, i, evaluated, tt.expected) + v.checkCFP(t, i, tt.expectedCFP) + v.checkSP(t, i, 1) + } +} + +// Isolated lock types + +func TestRWLockAcquireAndReleaseReadLock(t *testing.T) { + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + + lock.acquire_read_lock + lock.release_read_lock + + "completed" + ` + + expected := "completed" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +func TestRWLockAcquireAndReleaseWriteLock(t *testing.T) { + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + + lock.acquire_write_lock + lock.release_write_lock + + "completed" + ` + + expected := "completed" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +func TestRWLockWithReadLockMethod(t *testing.T) { + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + message = nil + + lock.with_read_lock do + message = "completed" + end + + message + ` + + expected := "completed" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +func TestRWLockWithWriteLockMethod(t *testing.T) { + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + message = nil + + lock.with_write_lock do + message = "completed" + end + + message + ` + + expected := "completed" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +// Mixed locks (functional tests) + +func TestRWLockAcquireAndReleaseLocksReadBlocksWriteNoRaceDetection(t *testing.T) { + skipRWLockTestIfRaceDetectionEnabled(t) + + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + message = nil + + thread do + lock.acquire_read_lock + sleep 2 + message ||= "thread 1" + lock.release_read_lock + end + + thread do + sleep 1 + lock.acquire_write_lock + message ||= "thread 2" + lock.release_write_lock + end + + sleep 3 + lock.with_read_lock do + message + end + ` + + expected := "thread 1" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +func TestRWLockWithReadLockReadBlocksWriteNoRaceDetection(t *testing.T) { + skipRWLockTestIfRaceDetectionEnabled(t) + + code := ` + require 'concurrent/rw_lock' + + lock = Concurrent::RWLock.new + message = nil + + thread do + lock.with_read_lock do + sleep 2 + message ||= "thread 1" + end + end + + thread do + sleep 1 + lock.with_write_lock do + message ||= "thread 2" + end + end + + sleep 3 + lock.with_read_lock do + message + end + ` + + expected := "thread 1" + + v := initTestVM() + evaluated := v.testEval(t, code, getFilename()) + testStringObject(t, i, evaluated, expected) + v.checkCFP(t, i, 0) + v.checkSP(t, i, 1) +} + +func skipRWLockTestIfRaceDetectionEnabled(t *testing.T) { + if os.Getenv("NO_RACE_DETECTION") == "" { + t.Skip("skipping RW lock related tests") + } +} diff --git a/vm/vm.go b/vm/vm.go index df039e6f1..69ca79622 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -39,14 +39,15 @@ type filename = string type errorMessage = string var standardLibraries = map[string]func(*VM){ - "net/http": initHTTPClass, - "net/simple_server": initSimpleServerClass, - "uri": initURIClass, - "db": initDBClass, - "plugin": initPluginClass, - "json": initJSONClass, - "concurrent/array": initConcurrentArrayClass, - "concurrent/hash": initConcurrentHashClass, + "net/http": initHTTPClass, + "net/simple_server": initSimpleServerClass, + "uri": initURIClass, + "db": initDBClass, + "plugin": initPluginClass, + "json": initJSONClass, + "concurrent/array": initConcurrentArrayClass, + "concurrent/hash": initConcurrentHashClass, + "concurrent/rw_lock": initConcurrentRWLockClass, } // VM represents a stack based virtual machine.