In a previous post, I discussed the concurrency issues with the initial MemoryChannel
implementation and how unit tests were insufficient to uncover them.
I came up with these basic requirements/invariants to guide my integration test design:
- Data from separately sent buffers must not be mixed or interleaved. That is, if sender 1 writes
"FFFF"
and sender 2 writes"CCCC"
, the receiver may only see"CCCCFFFF"
or"FFFFCCCC"
(depending on how the writes were serialized). - Assuming the channel is fully drained, all sent buffers must eventually be delivered; data must not be lost.
- The requirements above must hold given a single receiver and one or more senders operating concurrently.
The basic test flow would thus be as follows:
- Create
MemoryChannel
. - Start senders on background threads; run until canceled.
- Start async receiver; validate data after each receive operation.
- Run for some predetermined duration.
- Cancel senders; wait for tasks to complete.
- Dispose
MemoryChannel
; this will unblock the last receive operation. - Validate sent and received data size.
In the final refactoring, the main body of the test code ended up as follows:
MemoryChannel channel = new MemoryChannel(); int[] sentDataSizes = new int[] { 11, 19, 29, 41, 53, 71, 89, 101 }; using (CancellationTokenSource cts = new CancellationTokenSource()) { DataOracle oracle = new DataOracle(); Sender[] senders = this.CreateSenders(channel, sentDataSizes, oracle); ValidatingReceiver receiver = new ValidatingReceiver(channel, this.logger, this.receiveBufferSize, oracle); Task[] senderTasks = new Task[senders.Length]; Task receiverTask = this.StartSendersAndReceiver(cts.Token, senders, receiver, senderTasks); Thread.Sleep(this.duration); cts.Cancel(); Task.WaitAll(senderTasks); channel.Dispose(); receiverTask.Wait(); ValidateTransferredByteCount(senderTasks, receiverTask); }
My data validation strategy uses sent buffers of prime number sizes filled with a specific byte value per sender and a larger power of two for receive buffer size; for example, sender 1 uses a buffer of size 11 filled with 0x1
, sender 2 uses a buffer of size 19 filled with 0x2
and so on, with a receiver asking for 256 bytes at a time. My thinking was that it would be easier to detect state corruption if I used numbers like these (I admittedly have not done any rigorous mathematical proofs of this…). I implemented a ValidatingReceiver
which scans all received buffers and looks for runs of the same byte value. On each byte value change, the results are fed to a DataOracle
which knows the mapping of expected byte values to buffer length multiples:
public void VerifyLastSeen(byte lastSeen, int lastCount) { int expectedCountMultiple; if (!this.patterns.TryGetValue(lastSeen, out expectedCountMultiple)) { string message = string.Format( CultureInfo.InvariantCulture, "State corruption detected; byte 0x{0:X} was unexpected.", lastSeen); throw new InvalidOperationException(message); } if (lastCount % expectedCountMultiple != 0) { string message = string.Format( CultureInfo.InvariantCulture, "State corruption detected; count of {0} for byte 0x{1:X} is not a multiple of {2}.", lastCount, lastSeen, expectedCountMultiple); throw new InvalidOperationException(message); } }
By the final commit, I had a fully automated integration test app which gave me reasonable confidence that my logic was correct:
[ . . . ]
[0035.048/T01] Receive loop with 8 senders, 5.0 sec, send before receive=True, receive buffer=256...
[0035.048/T01] Sender B=11/F=0x1 starting...
[0035.048/T01] Sender B=19/F=0x2 starting...
[0035.048/T01] Sender B=29/F=0x3 starting...
[0035.049/T01] Sender B=41/F=0x4 starting...
[0035.049/T01] Sender B=53/F=0x5 starting...
[0035.049/T01] Sender B=71/F=0x6 starting...
[0035.049/T01] Sender B=89/F=0x7 starting...
[0035.050/T01] Sender B=101/F=0x8 starting...
[0035.050/T01] Receiver starting...
[0040.050/T35] Sender B=41/F=0x4 completed. Sent 409631 bytes.
[0040.050/T39] Sender B=101/F=0x8 completed. Sent 1009091 bytes.
[0040.051/T36] Sender B=53/F=0x5 completed. Sent 529470 bytes.
[0040.051/T38] Sender B=89/F=0x7 completed. Sent 889288 bytes.
[0040.051/T37] Sender B=71/F=0x6 completed. Sent 709290 bytes.
[0040.051/T33] Sender B=19/F=0x2 completed. Sent 189886 bytes.
[0040.051/T32] Sender B=11/F=0x1 completed. Sent 109956 bytes.
[0040.051/T34] Sender B=29/F=0x3 completed. Sent 289826 bytes.
[0040.053/T01] Receiver completed. Received 4136438 bytes.
[0040.053/T01] Done.