/// <summary> /// Thread-safe. Signals the thread blocked on 'LoopOut' to wake up. /// </summary> public void Send(Span <byte> response) { if (!_outbox.TryWrite(response)) { if (!_tokenSource.IsCancellationRequested) { _log?.Log(LogSeverity.Warning, $"Connection({ClientId}): outbox full."); } // If outbox is full, terminate the connection. Stop(); } _mre.Set(); }
public void RemoveDeadClients() { var socket = new MockSocket(); var dispatcherInbox = new BoundedInbox(); var client = new ConnectionController(dispatcherInbox, socket, ClientId.Next()); client.OnRequestReceived = () => { }; socket.ExpectConnected(() => false); var dispatcher = new DispatchController(dispatcherInbox, new BoundedInbox(), new BoundedInbox[4], new IdentityHash()); dispatcher.OnConnectionAccepted = (_) => { }; dispatcher.AddConnection(client); dispatcher.HandleNewConnection(); var firstMessage = new byte[100]; var message = new Message(firstMessage); message.Header.ClientId = client.ClientId; message.Header.MessageSizeInBytes = 100; message.Header.MessageKind = MessageKind.CloseConnection; Assert.True(dispatcherInbox.TryWrite(firstMessage)); dispatcher.HandleRequest(); socket.ExpectAllDone(); }
public void TryWriteCoinEmptyPayload() { var sozu = new VolatileCoinStore(); var inbox = new BoundedInbox(); var outbox = new BoundedInbox(); var controller = new CoinController(inbox, outbox, sozu, _hash); controller.Lineage = MakeLineage(); var clientId = new ClientId(); var reqId = new RequestId(1); var coin = GetCoin(_rand); var writeCoinRequest = ProduceCoinRequest.From(reqId, clientId, coin.Outpoint, (OutpointFlags)coin.OutpointFlags, new BlockAlias(3), 100, 50, Span <byte> .Empty, clientId.Mask); inbox.TryWrite(writeCoinRequest.Span); controller.HandleRequest(); var raw = outbox.Peek(); Assert.Equal(ProduceCoinResponse.SizeInBytes, raw.Length); var response = new ProduceCoinResponse(raw.Span); // verify response contents Assert.Equal(reqId, response.MessageHeader.RequestId); Assert.Equal(clientId, response.MessageHeader.ClientId); Assert.Equal(MessageKind.ProduceCoinResponse, response.MessageHeader.MessageKind); Assert.Equal(ChangeCoinStatus.InvalidContext, response.Status); }
public void TryReadingNonExistentCoin() { var sozu = new VolatileCoinStore(); var inbox = new BoundedInbox(); var outbox = new BoundedInbox(); var controller = new CoinController(inbox, outbox, sozu, _hash); var clientId = new ClientId(); var reqId = new RequestId(1); var coin = GetCoin(_rand); var readCoinRequest = GetCoinRequest.From(reqId, clientId, coin.Outpoint, new BlockAlias(3), clientId.Mask); inbox.TryWrite(readCoinRequest.Span); controller.HandleRequest(); var raw = outbox.Peek(); var response = new GetCoinResponse(raw.Span); // verify response contents Assert.Equal(reqId, response.MessageHeader.RequestId); Assert.Equal(clientId, response.MessageHeader.ClientId); Assert.Equal(GetCoinStatus.OutpointNotFound, response.Status); }
public void TestOpenFirstBlock() { _controller = new ControllerThread(_chain, default(OptimizedLineage), _inbox = BoundedInbox.Create(), _outbox = BoundedInbox.Create()); var messageAllocation = new Span <byte>(new byte[OpenBlock.SizeInBytes]); var openBlock = new OpenBlock(1, 5, BlockAlias.GenesisParent); // Serialize message to put it into the inbox MessageSerializers.ClientSerializeOpenBlock(openBlock, messageAllocation); _inbox.TryWrite(messageAllocation); _controller.DoWork(); var result = _outbox.Peek(); Assert.Equal(MessageType.OpenedBlock, ClientServerMessage.GetMessageType(result)); var response = MessageSerializers.ClientDeserializeOpenedBlock(result); Assert.Equal(1U, response.RequestId); Assert.Equal(5U, response.ClientId); Assert.Equal(_1, response.Alias); var tmpId = response.UncommittedBlockId; Console.WriteLine(tmpId); }
public void TestIsAncestor() { Setup(); var messageAllocation = new Span <byte>(new byte[IsAncestor.SizeInBytes]); var isAncestorReq = new IsAncestor(1, 5, _3, _1); // Serialize message to put it into the inbox MessageSerializers.ClientSerializeIsAncestor(isAncestorReq, messageAllocation); _inbox.TryWrite(messageAllocation); _controller.DoWork(); var result = _outbox.Peek(); Assert.Equal(MessageType.AncestorResponse, ClientServerMessage.GetMessageType(result)); Assert.True(ClientServerMessage.TryGetLength(result, out int responseSize)); Assert.Equal(AncestorResponse.SizeInBytes, responseSize); var response = MessageSerializers.ClientDeserializeAncestorResponse(result); Assert.Equal(1U, response.RequestId); Assert.Equal(5U, response.ClientId); Assert.True(response.Answer); // get a wrong ancestor isAncestorReq = new IsAncestor(1, 5, _3, _5); // Serialize message to put it into the inbox MessageSerializers.ClientSerializeIsAncestor(isAncestorReq, messageAllocation); _inbox.TryWrite(messageAllocation); _controller.DoWork(); _outbox.Next(); result = _outbox.Peek(); var response2 = MessageSerializers.ClientDeserializeAncestorResponse(result); Assert.Equal(1U, response2.RequestId); Assert.Equal(5U, response2.ClientId); Assert.Equal(MessageType.AncestorResponse, response2.ResponseType); Assert.False(response2.Answer); }
/// <summary> /// Return 'true' if at least one request has been processed, 'false' otherwise. /// </summary> public bool HandleRequest() { if (!_inbox.CanPeek) { return(false); } var next = _inbox.Peek().Span; try { var message = new Message(next); var kind = message.Header.MessageKind; var mask = message.Header.ClientId.Mask; Span <byte> response; switch (kind) { case MessageKind.OpenBlock: response = OpenBlock(new OpenBlockRequest(message.Span, mask)).Span; break; case MessageKind.GetBlockHandle: response = GetBlockHandle(new GetBlockHandleRequest(message.Span, mask)).Span; break; case MessageKind.CommitBlock: response = CommitBlock(new CommitBlockRequest(message.Span, mask)).Span; break; case MessageKind.GetBlockInfo: response = GetBlockInfo(new GetBlockInfoRequest(message.Span, mask)); break; default: throw new NotSupportedException(); } while (!_outbox.TryWrite(response)) { _log?.Log(LogSeverity.Warning, $"ChainController can't write the response to {kind}."); // Pathological situation, we don't want to overflow the logger. Thread.Sleep(1000); } } finally { _pool.Reset(); _inbox.Next(); } return(true); }
public void LoopIn() { if (OnRequestReceived == null) { throw new InvalidOperationException("OnRequestReceived is null."); } try { while (!_tokenSource.Token.IsCancellationRequested && _socket.Connected) { // Blocking call. if (HandleRequest()) { // Notify the dispatch controller. OnRequestReceived(); } } } catch (SocketException ex) { _log?.Log(LogSeverity.Info, $"ConnectionController({ClientId}) closed in LoopIn(): {ex}."); } catch (ObjectDisposedException ex) { _log?.Log(LogSeverity.Info, $"ConnectionController({ClientId}) closed in LoopIn(): {ex}."); } finally { Stop(); // Notify the dispatcher that the connection has been closed. var buffer = new byte[CloseConnectionResponse.SizeInBytes]; // allocation required because of 'finally' if (!_dispatchInbox.TryWrite( new CloseConnectionResponse(buffer, RequestId.MinRequestId, _clientId).Span)) { _log?.Log(LogSeverity.Error, $"ConnectionController({ClientId}) can't notify dispatch controller of its own termination."); } } }
public void RemoveProductionAndConsumption() { var sozu = new VolatileCoinStore(); var inbox = new BoundedInbox(); var outbox = new BoundedInbox(); var controller = new CoinController(inbox, outbox, sozu, _hash); controller.Lineage = new MockLineage(); var clientId = new ClientId(); var reqId = new RequestId(1); var coin = GetCoin(_rand); sozu.AddProduction( _hash.Hash(ref coin.Outpoint), ref coin.Outpoint, false, coin.Payload, new BlockAlias(2), null); sozu.AddConsumption( _hash.Hash(ref coin.Outpoint), ref coin.Outpoint, new BlockAlias(2), null); var buffer = new byte[RemoveCoinRequest.SizeInBytes]; var removeCoinRequest = new RemoveCoinRequest( buffer, reqId, ref coin.Outpoint, new BlockAlias(2).ConvertToBlockHandle(clientId.Mask), true, true); inbox.TryWrite(removeCoinRequest.Span); controller.HandleRequest(); var raw = outbox.Peek(); Assert.Equal(RemoveCoinResponse.SizeInBytes, raw.Length); var response = new RemoveCoinResponse(raw.Span); // verify response contents Assert.Equal(reqId, response.MessageHeader.RequestId); Assert.Equal(clientId, response.MessageHeader.ClientId); Assert.Equal(MessageKind.RemoveCoinResponse, response.MessageHeader.MessageKind); Assert.Equal(ChangeCoinStatus.Success, response.Status); sozu.TryGet(_hash.Hash(ref coin.Outpoint), ref coin.Outpoint, new BlockAlias(2), null, out var coin2, out var pe, out var ce); Assert.False(pe.IsDefined); Assert.False(ce.IsDefined); }
public void ReadExistingCoin() { var sozu = new VolatileCoinStore(); var inbox = new BoundedInbox(); var outbox = new BoundedInbox(); var controller = new CoinController(inbox, outbox, sozu, _hash); var coin = GetCoin(_rand); sozu.AddProduction( _hash.Hash(ref coin.Outpoint), ref coin.Outpoint, false, coin.Payload, new BlockAlias(3), null); // lineage is not used in VolatileCoinStore var clientId = new ClientId(); var reqId = new RequestId(1); var context = new BlockAlias(3); var readCoinRequest = GetCoinRequest.From(reqId, clientId, coin.Outpoint, context, clientId.Mask); inbox.TryWrite(readCoinRequest.Span); controller.HandleRequest(); var raw = outbox.Peek(); var response = new GetCoinResponse(raw.Span); Assert.Equal(response.MessageHeader.MessageSizeInBytes, raw.Length); // verify response contents Assert.Equal(reqId, response.MessageHeader.RequestId); Assert.Equal(clientId, response.MessageHeader.ClientId); Assert.Equal(MessageKind.GetCoinResponse, response.MessageHeader.MessageKind); Assert.Equal(coin.Outpoint, response.Outpoint); Assert.Equal(OutpointFlags.None, response.OutpointFlags); Assert.Equal(context, response.Context.ConvertToBlockAlias(clientId.Mask)); Assert.Equal(context, response.Production.ConvertToBlockAlias(clientId.Mask)); Assert.Equal(BlockAlias.Undefined.ConvertToBlockHandle(clientId.Mask), response.Consumption); Assert.Equal(coin.Payload.Satoshis, response.Satoshis); Assert.Equal(coin.Payload.NLockTime, response.NLockTime); Assert.True(coin.Payload.Script.SequenceEqual(response.Script)); }
public void WriteCoinCons() { var sozu = new VolatileCoinStore(); var inbox = new BoundedInbox(); var outbox = new BoundedInbox(); var controller = new CoinController(inbox, outbox, sozu, _hash); controller.Lineage = new MockLineage(); var clientId = new ClientId(); var reqId = new RequestId(1); var coin = GetCoin(_rand); sozu.AddProduction( _hash.Hash(ref coin.Outpoint), ref coin.Outpoint, false, coin.Payload, new BlockAlias(2), null); var pool = new SpanPool <byte>(4096); var buffer = new byte[ConsumeCoinRequest.SizeInBytes]; var writeCoinRequest = new ConsumeCoinRequest( reqId, ref coin.Outpoint, new BlockAlias(2).ConvertToBlockHandle(clientId.Mask), pool); inbox.TryWrite(writeCoinRequest.Span); controller.HandleRequest(); var raw = outbox.Peek(); Assert.Equal(ConsumeCoinResponse.SizeInBytes, raw.Length); var response = new ConsumeCoinResponse(raw.Span); // verify response contents Assert.Equal(reqId, response.MessageHeader.RequestId); Assert.Equal(clientId, response.MessageHeader.ClientId); Assert.Equal(MessageKind.ConsumeCoinResponse, response.MessageHeader.MessageKind); Assert.Equal(ChangeCoinStatus.Success, response.Status); }
public bool HandleRequest() { if (!_inbox.CanPeek) { return(false); } var next = _inbox.Peek().Span; try { var message = new Message(next); var mask = message.Header.ClientId.Mask; var kind = message.Header.MessageKind; var requestId = message.Header.RequestId; var clientId = message.Header.ClientId; if (!_connections.TryGetValue(clientId, out var connection)) { // Outdated message, drop and move on. return(true); } if (!connection.Connected || kind == MessageKind.CloseConnection) { // Client disconnected, drop message and remove connection. _connections.Remove(clientId); return(true); } if (kind.IsResponse()) { // Forward responses to their respective 'ConnectionController'. connection.Send(next); return(true); } if (kind.IsForCoinController()) { // Multiple coin controllers Outpoint outpoint; switch (kind) { case MessageKind.GetCoin: outpoint = new GetCoinRequest(next, mask).Outpoint; break; case MessageKind.ProduceCoin: outpoint = new ProduceCoinRequest(next, mask).Outpoint; break; case MessageKind.ConsumeCoin: outpoint = new ConsumeCoinRequest(next, mask).Outpoint; break; case MessageKind.RemoveCoin: outpoint = new RemoveCoinRequest(next, mask).Outpoint; break; default: throw new NotSupportedException(); } // Sharding based on the outpoint hash // Beware: the factor 'BigPrime' is used to avoid accidental factor collision // between the sharding performed at the dispatch controller level, and the // sharding performed within the Sozu table. // PERF: hashing the outpoint is repeated in the CoinController itself const ulong BigPrime = 1_000_000_007; var controllerIndex = (int)((_hash.Hash(ref outpoint) % BigPrime) % (ulong)_coinControllerBoxes.Length); var written = _coinControllerBoxes[controllerIndex].TryWrite(next); if (written) { OnCoinMessageDispatched[controllerIndex](); } else { // Coin controller is saturated. Span <byte> buffer = stackalloc byte[ProtocolErrorResponse.SizeInBytes]; var errorMessage = new ProtocolErrorResponse( buffer, requestId, clientId, ProtocolErrorStatus.ServerBusy); connection.Send(errorMessage.Span); } return(true); } { // Block controller var written = _chainControllerBox.TryWrite(next); if (written) { OnBlockMessageDispatched(); } else { // Block controller is saturated. Span <byte> buffer = stackalloc byte[ProtocolErrorResponse.SizeInBytes]; var errorMessage = new ProtocolErrorResponse( buffer, requestId, clientId, ProtocolErrorStatus.ServerBusy); connection.Send(errorMessage.Span); } } } finally { _inbox.Next(); } return(true); }
public void SendAnswers() { var dispatcherInbox = new BoundedInbox(); var socket1 = new MockSocket(); var socket2 = new MockSocket(); var client1 = new ConnectionController(dispatcherInbox, socket1, ClientId.Next()); var client2 = new ConnectionController(dispatcherInbox, socket2, ClientId.Next()); client1.OnRequestReceived = () => { }; client2.OnRequestReceived = () => { }; var dispatcher = new DispatchController(dispatcherInbox, new BoundedInbox(), new BoundedInbox[2], new IdentityHash()); // Nil handling of notifications dispatcher.OnBlockMessageDispatched = () => { }; for (var i = 0; i < dispatcher.OnCoinMessageDispatched.Length; i++) { dispatcher.OnCoinMessageDispatched[i] = () => { } } ; dispatcher.OnConnectionAccepted = (_) => { }; var firstMessage = new byte[Constants.MaxRequestSize]; var message = new Message(firstMessage); message.Header.ClientId = client2.ClientId; message.Header.MessageSizeInBytes = Constants.MaxRequestSize; message.Header.MessageKind = MessageKind.GetCoinResponse; var secondMessage = new byte[Constants.MaxRequestSize]; message = new Message(secondMessage); message.Header.ClientId = client1.ClientId; message.Header.MessageSizeInBytes = Constants.MaxRequestSize; message.Header.MessageKind = MessageKind.GetCoinResponse; var thirdMessage = new byte[50]; message = new Message(thirdMessage); message.Header.ClientId = client1.ClientId; message.Header.MessageSizeInBytes = 50; message.Header.MessageKind = MessageKind.GetCoinResponse; var fourthMessage = new byte[50]; message = new Message(fourthMessage); message.Header.ClientId = client2.ClientId; message.Header.MessageSizeInBytes = 50; message.Header.MessageKind = MessageKind.GetCoinResponse; dispatcher.AddConnection(client1); dispatcher.AddConnection(client2); dispatcher.HandleNewConnection(); dispatcher.HandleNewConnection(); // Write messages into dispatcher buffer for both clients for (var i = 0; i < Constants.SocketSendBufferSize / Constants.MaxRequestSize; i++) { socket1.ExpectConnected(() => true); socket2.ExpectConnected(() => true); Assert.True(dispatcherInbox.TryWrite(firstMessage)); Assert.True(dispatcherInbox.TryWrite(secondMessage)); // Try sending the answers, fails dispatcher.HandleRequest(); dispatcher.HandleRequest(); } // Try sending message to first client, socket gets closed socket1.ExpectConnected(() => true); socket1.ExpectClose(); // Try sending message to second client, socket gets closed socket2.ExpectConnected(() => true); socket2.ExpectClose(); Assert.True(dispatcherInbox.TryWrite(thirdMessage)); Assert.True(dispatcherInbox.TryWrite(fourthMessage)); dispatcher.HandleRequest(); dispatcher.HandleRequest(); socket1.ExpectAllDone(); socket2.ExpectAllDone(); } }
private void WritingToEmptyInbox() { Assert.True(_inbox.TryWrite(_bytes)); Assert.Equal(_bytes, _inbox.Peek().ToArray()); }
public void DoWork() // TODO: [vermorel] method should return 'true' if any work has been done { var nextMessage = _inbox.Peek(); if (nextMessage.Length != 0) { // get message type and deserialize accordingly var reqType = ClientServerMessage.GetMessageType(nextMessage); switch (reqType) { // TODO: [vermorel] I don't like to too much the code below, it's highly repetitive, factorization is needed. case MessageType.OpenBlock: var openBlock = MessageSerializers.DeserializeOpenBlock(nextMessage); UncommittedBlock block; if (openBlock.ParentHandle == BlockAlias.GenesisParent && _chain.BlockchainLength == 0) { block = _chain.OpenFirstBlock(); } else { block = _chain.OpenBlock(openBlock.ParentHandle); } var blockHandle = new OpenedBlock(openBlock.RequestId, openBlock.ClientId, block.BlockId, block.Alias); MessageSerializers.SerializeOpenedBlock(blockHandle, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, OpenedBlock.SizeInBytes)); break; case MessageType.GetBlockHandle: var getBlockHandle = MessageSerializers.DeserializeGetBlockHandle(nextMessage); var retrievedHandle = _chain.RetrieveAlias(getBlockHandle.BlockId); var returnMessage = new BlockHandleResponse(getBlockHandle.RequestId, getBlockHandle.ClientId, retrievedHandle); MessageSerializers.SerializeBlockHandleResponse(returnMessage, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, BlockHandleResponse.SizeInBytes)); break; case MessageType.GetUncommittedBlockHandle: var getUBlockHandle = MessageSerializers.DeserializeGetUncommittedBlockHandle(nextMessage); var retrievedUHandle = _chain.RetrieveAlias(getUBlockHandle.UncommittedBlockId); var returnMes = new BlockHandleResponse(getUBlockHandle.RequestId, getUBlockHandle.ClientId, retrievedUHandle); MessageSerializers.SerializeBlockHandleResponse(returnMes, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, BlockHandleResponse.SizeInBytes)); break; case MessageType.CommitBlock: var commitBlock = MessageSerializers.DeserializeCommitBlock(nextMessage); _chain.CommitBlock(commitBlock.BlockHandle, commitBlock.BlockId); var committedMessage = new EverythingOkResponse(commitBlock.RequestId, commitBlock.ClientId); MessageSerializers.SerializeEverythingOk(committedMessage, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, EverythingOkResponse.SizeInBytes)); break; case MessageType.IsAncestor: var isAncestor = MessageSerializers.DeserializeIsAncestor(nextMessage); var res = _opti.IsAncestor(isAncestor.BlockHandle, isAncestor.MaybeAncestorHandle); var isAncestorMessage = new AncestorResponse(isAncestor.RequestId, isAncestor.ClientId, res); MessageSerializers.SerializeAncestorResponse(isAncestorMessage, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, AncestorResponse.SizeInBytes)); break; case MessageType.IsPruneable: var isPruneable = MessageSerializers.DeserializeIsPruneable(nextMessage); var result = _opti.IsPruneable(isPruneable.BlockHandle); var isPruneableMessage = new PruneableResponse(isPruneable.RequestId, isPruneable.ClientId, result); MessageSerializers.SerializePruneableResponse(isPruneableMessage, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, PruneableResponse.SizeInBytes)); break; case MessageType.GetBlockInfo: var blockInfoReq = MessageSerializers.DeserializeGetBlockInfo(nextMessage); // TODO: [vermorel] clarify the purpose of this test, unclear if (_chain.RetrieveCommittedBlock(blockInfoReq.BlockHandle, out var blockInfoC)) { var blockInfo = new CommittedBlockInformation(blockInfoReq.RequestId, blockInfoReq.ClientId, blockInfoC.BlockId, blockInfoC.Alias, blockInfoC.BlockHeight, blockInfoC.Parent); MessageSerializers.SerializeCommittedBlockInfo(blockInfo, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, CommittedBlockInformation.SizeInBytes)); break; } else { _chain.RetrieveUncommittedBlock(blockInfoReq.BlockHandle, out var blockInfoU); var blockInfo = new UncommittedBlockInformation(blockInfoReq.RequestId, blockInfoReq.ClientId, blockInfoU.BlockId, blockInfoU.Alias, blockInfoU.BlockHeight, blockInfoU.Parent); MessageSerializers.SerializeUncommittedBlockInfo(blockInfo, _responseBuffer); _outbox.TryWrite(new Span <byte>(_responseBuffer, 0, UncommittedBlockInformation.SizeInBytes)); break; } // TODO: [vermorel] the 'default:' case should be covered with an NotSupportedException. } _inbox.Next(); // TODO: errors are not yet handled. } }