public async Task <TReply> RequestTransaction(TRequest rq) { // Generate our request ID. Could be used for idempotency. // Should be provided by our calling client so that also that part can be made idempotent var requestId = "API-" + Guid.NewGuid().ToString("B"); rq.RequestId = requestId; // Set up receiving our response pendingRequests[requestId] = null; // Note: you can carry over information in headers like this. An example would be to put the reply topic in a header, // so that you can let consumers know where to expect back the reply. Or add the correlation IDs in the headers. // For example, this would be the place where you want to think of distributed tracing. In this example, we will carry over // our Jaeger correlation ID, so that consumers can be part of the vary same trace. // Specifically, we are going to carry over our reply group ID, which the consumer must communicate back to use. // Our Tracing identifiers are taken care of in the KafkaSender class. var rqHeaders = new List <Tuple <string, byte[]> > { new Tuple <string, byte[]>("reply-group-id", Encoding.ASCII.GetBytes(replyGroup.MyUniqueConsumerGroup)) }; // Send our request var sendSuccess = await sender.SendToBusWithoutRetries(rq, "transaction-requests", rqHeaders); if (!sendSuccess) { return(null); } // Wait for response and return it. var traceBuilder = tracer.BuildSpan("Wait for Kafka reply"); using (traceBuilder.StartActive(true)) { try { using var ts = new CancellationTokenSource(); ts.CancelAfter(TimeSpan.FromSeconds(5)); await WaitForReplyById(requestId, ts.Token); } catch (TaskCanceledException ex) { // If we fail, return null logger.LogError("Task cancelled waiting for response", ex); return(null); } } if (pendingRequests.TryRemove(requestId, out var reply)) { return(reply); } // we failed for some weird reason return(null); }
private async Task ProcessMessage(ConsumeResult <Ignore, TransactionRequest> cr) { var rq = cr.Message.Value; logger.LogInformation($"Consumed message '{rq}' (amount: {rq.AmountCents})'."); var replyGroupId = cr.Message.Headers.SingleOrDefault(p => p.Key == "reply-group-id"); // We are going to pretend that we do some processing here and then send out two events: // * Reply to the Request that it has been processesed // * Publish the fact that the transaction has been created onto the bus // This is obviously not a real world implementation. We're sending two messages independently, without transactions. // We're also not concerned about commits and idempotency upon reprocessing // Also, if we are doing other work, such as databases, things can get complex because you will have multiple transactions, each // of which can fail independently. // Do our 'processing' var reply = new TransactionReply { RequestId = rq.RequestId, Status = $"Transaction of value {rq.AmountCents} has been processed" }; var replyHeaders = new List <Tuple <string, byte[]> >(); if (replyGroupId != null) { replyHeaders.Add(new Tuple <string, byte[]>("reply-group-id", replyGroupId.GetValueBytes())); } var createdEvent = new TransactionCreated { AmountCents = rq.AmountCents, FromAccount = rq.FromAccount, ToAccount = rq.ToAccount, CreatedAt = DateTime.UtcNow, TransactionId = Guid.NewGuid().ToString("B") }; await createdSender.SendToBusWithoutRetries(createdEvent, "transactions"); await replySender.SendToBusWithoutRetries(reply, "transaction-replies", replyHeaders); }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Beginning direct transaction producer"); while (!stoppingToken.IsCancellationRequested) { try { var rq = GenerateNewRandomTransaction(); var rqHeaders = new List <Tuple <string, byte[]> > { new Tuple <string, byte[]>("reply-group-id", Encoding.ASCII.GetBytes("irrelevant-for-me")) }; await sender.SendToBusWithoutRetries(rq, "transaction-requests", rqHeaders); await Task.Delay(250, stoppingToken); } catch (Exception e) { logger.LogInformation("Failure in producer: " + e.Message, e); await Task.Delay(1000, stoppingToken); } } logger.LogInformation("Stopping direct transaction producer"); }