mirror of
				https://gitlab.com/veilid/veilidchat.git
				synced 2025-10-31 14:18:57 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			486 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			486 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'dart:async';
 | |
| import 'package:mutex/mutex.dart';
 | |
| import 'package:test/test.dart';
 | |
| 
 | |
| //################################################################
 | |
| 
 | |
| class RWTester {
 | |
|   int _operation = 0;
 | |
|   final _operationSequences = <int>[];
 | |
| 
 | |
|   /// Execution sequence of the operations done.
 | |
|   ///
 | |
|   /// Each element corresponds to the position of the initial execution
 | |
|   /// order of the read/write operation future.
 | |
|   List<int> get operationSequences => _operationSequences;
 | |
| 
 | |
|   ReadWriteMutex mutex = ReadWriteMutex();
 | |
| 
 | |
|   /// Set to true to print out read/write to the balance during deposits
 | |
|   static const bool debugOutput = false;
 | |
| 
 | |
|   final DateTime _startTime = DateTime.now();
 | |
| 
 | |
|   void _debugPrint(String message) {
 | |
|     if (debugOutput) {
 | |
|       final t = DateTime.now().difference(_startTime).inMilliseconds;
 | |
|       // ignore: avoid_print
 | |
|       print('$t: $message');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void reset() {
 | |
|     _operationSequences.clear();
 | |
|     _debugPrint('reset');
 | |
|   }
 | |
| 
 | |
|   /// Waits [startDelay] and then invokes critical section with mutex.
 | |
|   ///
 | |
|   /// Writes to [_operationSequences]. If the readwrite locks are respected
 | |
|   /// then the final state of the list will be in ascending order.
 | |
|   Future<void> writing(int startDelay, int sequence, int endDelay) async {
 | |
|     await Future<void>.delayed(Duration(milliseconds: startDelay));
 | |
| 
 | |
|     await mutex.protectWrite(() async {
 | |
|       final op = ++_operation;
 | |
|       _debugPrint('[$op] write start: <- $_operationSequences');
 | |
|       final tmp = _operationSequences;
 | |
|       expect(mutex.isWriteLocked, isTrue);
 | |
|       expect(_operationSequences, orderedEquals(tmp));
 | |
|       // Add the position of operation to the list of operations.
 | |
|       _operationSequences.add(sequence); // add position to list
 | |
|       expect(mutex.isWriteLocked, isTrue);
 | |
|       await Future<void>.delayed(Duration(milliseconds: endDelay));
 | |
|       _debugPrint('[$op] write finish: -> $_operationSequences');
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /// Waits [startDelay] and then invokes critical section with mutex.
 | |
|   ///
 | |
|   ///
 | |
|   Future<void> reading(int startDelay, int sequence, int endDelay) async {
 | |
|     await Future<void>.delayed(Duration(milliseconds: startDelay));
 | |
| 
 | |
|     await mutex.protectRead(() async {
 | |
|       final op = ++_operation;
 | |
|       _debugPrint('[$op] read start: <- $_operationSequences');
 | |
|       expect(mutex.isReadLocked, isTrue);
 | |
|       _operationSequences.add(sequence); // add position to list
 | |
|       await Future<void>.delayed(Duration(milliseconds: endDelay));
 | |
|       _debugPrint('[$op] read finish: <- $_operationSequences');
 | |
|     });
 | |
|   }
 | |
| }
 | |
| 
 | |
| //################################################################
 | |
| 
 | |
| //----------------------------------------------------------------
 | |
| 
 | |
| void main() {
 | |
|   final account = RWTester();
 | |
| 
 | |
|   setUp(account.reset);
 | |
| 
 | |
|   test('multiple read locks', () async {
 | |
|     await Future.wait([
 | |
|       account.reading(0, 1, 1000),
 | |
|       account.reading(0, 2, 900),
 | |
|       account.reading(0, 3, 800),
 | |
|       account.reading(0, 4, 700),
 | |
|       account.reading(0, 5, 600),
 | |
|       account.reading(0, 6, 500),
 | |
|       account.reading(0, 7, 400),
 | |
|       account.reading(0, 8, 300),
 | |
|       account.reading(0, 9, 200),
 | |
|       account.reading(0, 10, 100),
 | |
|     ]);
 | |
|     // The first future acquires the lock first and waits the longest to give it
 | |
|     // up. This should however not block any of the other read operations
 | |
|     // as such the reads should finish in ascending orders.
 | |
|     expect(
 | |
|       account.operationSequences,
 | |
|       orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   test('multiple write locks', () async {
 | |
|     await Future.wait([
 | |
|       account.writing(0, 1, 100),
 | |
|       account.writing(0, 2, 100),
 | |
|       account.writing(0, 3, 100),
 | |
|     ]);
 | |
|     // The first future writes first and holds the lock until 100 ms
 | |
|     // Even though the second future starts execution, the lock cannot be
 | |
|     // acquired until it is released by the first future.
 | |
|     // Therefore the sequence of operations will be in ascending order
 | |
|     // of the futures.
 | |
|     expect(
 | |
|       account.operationSequences,
 | |
|       orderedEquals(<int>[1, 2, 3]),
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   test('acquireWrite() before acquireRead()', () async {
 | |
|     const lockTimeout = Duration(milliseconds: 100);
 | |
| 
 | |
|     final mutex = ReadWriteMutex();
 | |
| 
 | |
|     await mutex.acquireWrite();
 | |
|     expect(mutex.isReadLocked, equals(false));
 | |
|     expect(mutex.isWriteLocked, equals(true));
 | |
| 
 | |
|     // Since there is a write lock existing, a read lock cannot be acquired.
 | |
|     final readLock = mutex.acquireRead().timeout(lockTimeout);
 | |
|     expect(
 | |
|       () async => readLock,
 | |
|       throwsA(isA<TimeoutException>()),
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   test('acquireRead() before acquireWrite()', () async {
 | |
|     const lockTimeout = Duration(milliseconds: 100);
 | |
| 
 | |
|     final mutex = ReadWriteMutex();
 | |
| 
 | |
|     await mutex.acquireRead();
 | |
|     expect(mutex.isReadLocked, equals(true));
 | |
|     expect(mutex.isWriteLocked, equals(false));
 | |
| 
 | |
|     // Since there is a read lock existing, a write lock cannot be acquired.
 | |
|     final writeLock = mutex.acquireWrite().timeout(lockTimeout);
 | |
|     expect(
 | |
|       () async => writeLock,
 | |
|       throwsA(isA<TimeoutException>()),
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   test('mixture of read write locks execution order', () async {
 | |
|     await Future.wait([
 | |
|       account.reading(0, 1, 100),
 | |
|       account.reading(10, 2, 100),
 | |
|       account.reading(20, 3, 100),
 | |
|       account.writing(30, 4, 100),
 | |
|       account.writing(40, 5, 100),
 | |
|       account.writing(50, 6, 100),
 | |
|     ]);
 | |
| 
 | |
|     expect(
 | |
|       account.operationSequences,
 | |
|       orderedEquals(<int>[1, 2, 3, 4, 5, 6]),
 | |
|     );
 | |
|   });
 | |
| 
 | |
|   group('protectRead', () {
 | |
|     test('lock obtained and released on success', () async {
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       await m.protectRead(() async {
 | |
|         // critical section
 | |
|         expect(m.isLocked, isTrue);
 | |
|       });
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('value returned from critical section', () async {
 | |
|       // These are the normal scenario of the critical section running
 | |
|       // successfully. It tests different return types from the
 | |
|       // critical section.
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       // returns Future<void>
 | |
|       await m.protectRead<void>(() async {});
 | |
| 
 | |
|       // returns Future<int>
 | |
|       final number = await m.protectRead<int>(() async => 42);
 | |
|       expect(number, equals(42));
 | |
| 
 | |
|       // returns Future<int?> completes with value
 | |
|       final optionalNumber = await m.protectRead<int?>(() async => 1024);
 | |
|       expect(optionalNumber, equals(1024));
 | |
| 
 | |
|       // returns Future<int?> completes with null
 | |
|       final optionalNumberNull = await m.protectRead<int?>(() async => null);
 | |
|       expect(optionalNumberNull, isNull);
 | |
| 
 | |
|       // returns Future<String>
 | |
|       final word = await m.protectRead<String>(() async => 'foobar');
 | |
|       expect(word, equals('foobar'));
 | |
| 
 | |
|       // returns Future<String?> completes with value
 | |
|       final optionalWord = await m.protectRead<String?>(() async => 'baz');
 | |
|       expect(optionalWord, equals('baz'));
 | |
| 
 | |
|       // returns Future<String?> completes with null
 | |
|       final optionalWordNull = await m.protectRead<String?>(() async => null);
 | |
|       expect(optionalWordNull, isNull);
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('exception in synchronous code', () async {
 | |
|       // Tests what happens when an exception is raised in the **synchronous**
 | |
|       // part of the critical section.
 | |
|       //
 | |
|       // Locks are correctly managed: the lock is obtained before executing
 | |
|       // the critical section, and is released when the exception is thrown
 | |
|       // by the _protect_ method.
 | |
|       //
 | |
|       // The exception is raised when waiting for the Future returned by
 | |
|       // _protect_ to complete. Even though the exception is synchronously
 | |
|       // raised by the critical section, it won't be thrown when _protect_
 | |
|       // is invoked. The _protect_ method always successfully returns a
 | |
|       // _Future_.
 | |
| 
 | |
|       Future<int> criticalSection() {
 | |
|         final c = Completer<int>()..complete(42);
 | |
| 
 | |
|         // synchronous exception
 | |
|         throw const FormatException('synchronous exception');
 | |
|         // ignore: dead_code
 | |
|         return c.future;
 | |
|       }
 | |
| 
 | |
|       // Check the criticalSection behaves as expected for the test
 | |
| 
 | |
|       try {
 | |
|         // ignore: unused_local_variable
 | |
|         final resultFuture = criticalSection();
 | |
|         fail('critical section did not throw synchronous exception');
 | |
|       } on FormatException {
 | |
|         // expected: invoking the criticalSection results in the exception
 | |
|       }
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       try {
 | |
|         // Invoke protect to get the Future (this should succeed)
 | |
|         final resultFuture = m.protectRead<int>(criticalSection);
 | |
|         expect(resultFuture, isA<Future<int>>());
 | |
| 
 | |
|         // Wait for the Future (this should fail)
 | |
|         final result = await resultFuture;
 | |
|         expect(result, isNotNull);
 | |
|         fail('exception not thrown');
 | |
|       } on FormatException catch (e) {
 | |
|         expect(m.isLocked, isFalse);
 | |
|         expect(e.message, equals('synchronous exception'));
 | |
|       }
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('exception in asynchronous code', () async {
 | |
|       // Tests what happens when an exception is raised in the **asynchronous**
 | |
|       // part of the critical section.
 | |
|       //
 | |
|       // Locks are correctly managed: the lock is obtained before executing
 | |
|       // the critical section, and is released when the exception is thrown
 | |
|       // by the _protect_ method.
 | |
|       //
 | |
|       // The exception is raised when waiting for the Future returned by
 | |
|       // _protect_ to complete.
 | |
| 
 | |
|       Future<int> criticalSection() async {
 | |
|         final c = Completer<int>()..complete(42);
 | |
| 
 | |
|         await Future.delayed(const Duration(seconds: 1), () {});
 | |
| 
 | |
|         // asynchronous exception (since it must wait for the above line)
 | |
|         throw const FormatException('asynchronous exception');
 | |
|         // ignore: dead_code
 | |
|         return c.future;
 | |
|       }
 | |
| 
 | |
|       // Check the criticalSection behaves as expected for the test
 | |
| 
 | |
|       final resultFuture = criticalSection();
 | |
|       expect(resultFuture, isA<Future<int>>());
 | |
|       // invoking the criticalSection does not result in the exception
 | |
|       try {
 | |
|         await resultFuture;
 | |
|         fail('critical section did not throw asynchronous exception');
 | |
|       } on FormatException {
 | |
|         // expected: exception happens on the await
 | |
|       }
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       try {
 | |
|         // Invoke protect to get the Future (this should succeed)
 | |
|         final resultFuture = m.protectRead<int>(criticalSection);
 | |
|         expect(resultFuture, isA<Future<int>>());
 | |
| 
 | |
|         // Even though the criticalSection throws the exception in synchronous
 | |
|         // code, protect causes it to become an asynchronous exception.
 | |
| 
 | |
|         // Wait for the Future (this should fail)
 | |
|         final result = await resultFuture;
 | |
|         expect(result, isNotNull);
 | |
|         fail('exception not thrown');
 | |
|       } on FormatException catch (e) {
 | |
|         expect(m.isLocked, isFalse);
 | |
|         expect(e.message, equals('asynchronous exception'));
 | |
|       }
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   group('protectWrite', () {
 | |
|     test('lock obtained and released on success', () async {
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       await m.protectWrite(() async {
 | |
|         // critical section
 | |
|         expect(m.isLocked, isTrue);
 | |
|       });
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('value returned from critical section', () async {
 | |
|       // These are the normal scenario of the critical section running
 | |
|       // successfully. It tests different return types from the
 | |
|       // critical section.
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       // returns Future<void>
 | |
|       await m.protectWrite<void>(() async {});
 | |
| 
 | |
|       // returns Future<int>
 | |
|       final number = await m.protectWrite<int>(() async => 42);
 | |
|       expect(number, equals(42));
 | |
| 
 | |
|       // returns Future<int?> completes with value
 | |
|       final optionalNumber = await m.protectWrite<int?>(() async => 1024);
 | |
|       expect(optionalNumber, equals(1024));
 | |
| 
 | |
|       // returns Future<int?> completes with null
 | |
|       final optionalNumberNull = await m.protectWrite<int?>(() async => null);
 | |
|       expect(optionalNumberNull, isNull);
 | |
| 
 | |
|       // returns Future<String>
 | |
|       final word = await m.protectWrite<String>(() async => 'foobar');
 | |
|       expect(word, equals('foobar'));
 | |
| 
 | |
|       // returns Future<String?> completes with value
 | |
|       final optionalWord = await m.protectWrite<String?>(() async => 'baz');
 | |
|       expect(optionalWord, equals('baz'));
 | |
| 
 | |
|       // returns Future<String?> completes with null
 | |
|       final optionalWordNull = await m.protectWrite<String?>(() async => null);
 | |
|       expect(optionalWordNull, isNull);
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('exception in synchronous code', () async {
 | |
|       // Tests what happens when an exception is raised in the **synchronous**
 | |
|       // part of the critical section.
 | |
|       //
 | |
|       // Locks are correctly managed: the lock is obtained before executing
 | |
|       // the critical section, and is released when the exception is thrown
 | |
|       // by the _protect_ method.
 | |
|       //
 | |
|       // The exception is raised when waiting for the Future returned by
 | |
|       // _protect_ to complete. Even though the exception is synchronously
 | |
|       // raised by the critical section, it won't be thrown when _protect_
 | |
|       // is invoked. The _protect_ method always successfully returns a
 | |
|       // _Future_.
 | |
| 
 | |
|       Future<int> criticalSection() {
 | |
|         final c = Completer<int>()..complete(42);
 | |
| 
 | |
|         // synchronous exception
 | |
|         throw const FormatException('synchronous exception');
 | |
|         // ignore: dead_code
 | |
|         return c.future;
 | |
|       }
 | |
| 
 | |
|       // Check the criticalSection behaves as expected for the test
 | |
| 
 | |
|       try {
 | |
|         // ignore: unused_local_variable
 | |
|         final resultFuture = criticalSection();
 | |
|         fail('critical section did not throw synchronous exception');
 | |
|       } on FormatException {
 | |
|         // expected: invoking the criticalSection results in the exception
 | |
|       }
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       try {
 | |
|         // Invoke protect to get the Future (this should succeed)
 | |
|         final resultFuture = m.protectWrite<int>(criticalSection);
 | |
|         expect(resultFuture, isA<Future<int>>());
 | |
| 
 | |
|         // Wait for the Future (this should fail)
 | |
|         final result = await resultFuture;
 | |
|         expect(result, isNotNull);
 | |
|         fail('exception not thrown');
 | |
|       } on FormatException catch (e) {
 | |
|         expect(m.isLocked, isFalse);
 | |
|         expect(e.message, equals('synchronous exception'));
 | |
|       }
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
| 
 | |
|     test('exception in asynchronous code', () async {
 | |
|       // Tests what happens when an exception is raised in the **asynchronous**
 | |
|       // part of the critical section.
 | |
|       //
 | |
|       // Locks are correctly managed: the lock is obtained before executing
 | |
|       // the critical section, and is released when the exception is thrown
 | |
|       // by the _protect_ method.
 | |
|       //
 | |
|       // The exception is raised when waiting for the Future returned by
 | |
|       // _protect_ to complete.
 | |
| 
 | |
|       Future<int> criticalSection() async {
 | |
|         final c = Completer<int>()..complete(42);
 | |
| 
 | |
|         await Future.delayed(const Duration(seconds: 1), () {});
 | |
| 
 | |
|         // asynchronous exception (since it must wait for the above line)
 | |
|         throw const FormatException('asynchronous exception');
 | |
|         // ignore: dead_code
 | |
|         return c.future;
 | |
|       }
 | |
| 
 | |
|       // Check the criticalSection behaves as expected for the test
 | |
| 
 | |
|       final resultFuture = criticalSection();
 | |
|       expect(resultFuture, isA<Future<int>>());
 | |
|       // invoking the criticalSection does not result in the exception
 | |
|       try {
 | |
|         await resultFuture;
 | |
|         fail('critical section did not throw asynchronous exception');
 | |
|       } on FormatException {
 | |
|         // expected: exception happens on the await
 | |
|       }
 | |
| 
 | |
|       final m = ReadWriteMutex();
 | |
| 
 | |
|       try {
 | |
|         // Invoke protect to get the Future (this should succeed)
 | |
|         final resultFuture = m.protectWrite<int>(criticalSection);
 | |
|         expect(resultFuture, isA<Future<int>>());
 | |
| 
 | |
|         // Even though the criticalSection throws the exception in synchronous
 | |
|         // code, protect causes it to become an asynchronous exception.
 | |
| 
 | |
|         // Wait for the Future (this should fail)
 | |
|         final result = await resultFuture;
 | |
|         expect(result, isNotNull);
 | |
|         fail('exception not thrown');
 | |
|       } on FormatException catch (e) {
 | |
|         expect(m.isLocked, isFalse);
 | |
|         expect(e.message, equals('asynchronous exception'));
 | |
|       }
 | |
| 
 | |
|       expect(m.isLocked, isFalse);
 | |
|     });
 | |
|   });
 | |
| }
 | 
