Пример #1
0
        public static async Task <IActionResult> OrderGetAll([HttpTrigger(AuthorizationLevel.Function, "get", Route = "user/{userId}/order")] HttpRequest req,
                                                             [DurableClient] IDurableClient client, ILogger log)
        {
            try
            {
                log.LogInformation($"Order Get is called");

                if (!req.Query.ContainsKey("userId"))
                {
                    throw new AppException(400, "User Id is missing from query");
                }

                var orderEntity = new EntityId(nameof(Order), req.Query["userId"]);

                var response = await client.ReadEntityStateAsync <Order>(orderEntity);

                if (!response.EntityExists)
                {
                    return(new NotFoundResult());
                }

                return(new OkObjectResult(response.EntityState.Items));
            }
            catch (Exception ex)
            {
                return(ex.GetResponse(log));
            }
        }
Пример #2
0
        public static async Task <HttpResponseMessage> DispatchCart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "mycart/dispatch")] HttpRequestMessage req,
            [DurableClient] IDurableClient client, ClaimsPrincipal claimsPrincipal,
            [ServiceBus("ordersQueue", Connection = "ServiceBusConnection")] IAsyncCollector <Cart> collector)
        {
            var username = claimsPrincipal.FindFirst("name").Value;
            var entityId = new EntityId("CartEntity", username);
            var state    = await client.ReadEntityStateAsync <CartEntity>(entityId);

            if (!state.EntityExists)
            {
                // we can't call dispatch on a non existing entity
                return(req.CreateErrorResponse(HttpStatusCode.BadRequest, "You can't call dispatch on a cart which doesn't exist"));
            }

            await collector.AddAsync(state.EntityState.Cart);

            var awaiter = client.GetDeletedAwaiter(entityId);

            // empty cart once it has been dispatched
            await client.SignalEntityAsync <ICartActions>(entityId, x => x.Delete());

            await client.CancelTimeoutAsync(entityId);

            await awaiter.SignalsProcessed();

            return(req.CreateResponse(HttpStatusCode.Accepted));
        }
Пример #3
0
        public static async Task <IActionResult> WaitForCount(
            [HttpTrigger(AuthorizationLevel.Function, methods: "post", Route = nameof(WaitForCount))] HttpRequest req,
            [DurableClient] IDurableClient client)
        {
            string    requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var       input       = JsonConvert.DeserializeObject <Input>(requestBody);
            var       entityId    = new EntityId("Counter", input.Key);
            Stopwatch stopwatch   = new Stopwatch();

            stopwatch.Start();

            // poll the entity until the expected count is reached
            while (stopwatch.Elapsed < TimeSpan.FromMinutes(5))
            {
                var response = await client.ReadEntityStateAsync <Counter>(entityId);

                if (response.EntityExists &&
                    response.EntityState.CurrentValue >= input.Expected)
                {
                    return(new OkObjectResult($"{JsonConvert.SerializeObject(response.EntityState)}\n"));
                }

                await Task.Delay(TimeSpan.FromSeconds(2));
            }

            return(new OkObjectResult("timed out.\n"));
        }
Пример #4
0
        public static async Task <IActionResult> CountSignals(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = nameof(CountSignals))] HttpRequest req,
            [DurableClient] IDurableClient client)
        {
            try
            {
                int numberSignals = int.Parse(await new StreamReader(req.Body).ReadToEndAsync());
                var entityId      = new EntityId("Counter", Guid.NewGuid().ToString("N"));

                DateTime startTime = DateTime.UtcNow;

                // send the specified number of signals to the entity
                await SendIncrementSignals(client, numberSignals, 50, (i) => entityId);

                // poll the entity until the expected count is reached
                while ((DateTime.UtcNow - startTime) < TimeSpan.FromMinutes(5))
                {
                    var response = await client.ReadEntityStateAsync <Counter>(entityId);

                    if (response.EntityExists &&
                        response.EntityState.CurrentValue == numberSignals)
                    {
                        return(new OkObjectResult($"received {numberSignals} signals in {(response.EntityState.LastModified - startTime).TotalSeconds:F1}s.\n"));
                    }

                    await Task.Delay(TimeSpan.FromSeconds(2));
                }

                return(new OkObjectResult($"timed out after {(DateTime.UtcNow - startTime)}.\n"));
            }
            catch (Exception e)
            {
                return(new OkObjectResult(e.ToString()));
            }
        }
        public static async Task <IActionResult> Run(
            [HttpTrigger(
                 AuthorizationLevel.Function,
                 nameof(HttpMethod.Post),
                 Route = null)] HttpRequestMessage message,
            [DurableClient] IDurableClient client,
            ILogger logger)
        {
            var phoneNumber = await message.Content.ReadAsAsync <string>();

            var entityId       = new EntityId(nameof(NotificationOrchestratorInstanceEntity), phoneNumber);
            var instanceEntity = await client.ReadEntityStateAsync <NotificationOrchestratorInstanceEntity>(entityId);

            if (instanceEntity.EntityExists)
            {
                await client.RaiseEventAsync(
                    instanceEntity.EntityState.InstanceId,
                    EventNames.Callback,
                    true);
            }
            else
            {
                logger.LogError($"=== No instanceId found for {phoneNumber}. ===");
            }

            return(new AcceptedResult());
        }
        public static async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] FraudResult fraudResult,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            var entityId = new EntityId(
                nameof(FraudDetectionOrchestratorEntity),
                fraudResult.RecordId);

            var entityStateResponse = await client.ReadEntityStateAsync <FraudDetectionOrchestratorEntity>(entityId);

            if (entityStateResponse.EntityExists)
            {
                await client.RaiseEventAsync(
                    entityStateResponse.EntityState.InstanceId,
                    Constants.FraudResultCompletedEvent,
                    fraudResult.IsSuspiciousTransaction);

                return(new AcceptedResult());
            }
            else
            {
                return(new BadRequestObjectResult($"Entity {entityId} does not exist."));
            }
        }
Пример #7
0
        public async Task <CircuitState> GetCircuitState(string circuitBreakerId, ILogger log, IDurableClient durableClient)
        {
            log?.LogCircuitBreakerMessage(circuitBreakerId, $"Getting circuit state for circuit-breaker = '{circuitBreakerId}'.");

            var readState = await durableClient.ReadEntityStateAsync <DurableCircuitBreaker>(DurableCircuitBreaker.GetEntityId(circuitBreakerId));

            // To keep the return type simple, we present a not-yet-initialized circuit-breaker as closed (it will be closed when first used).
            return(readState.EntityExists && readState.EntityState != null ? readState.EntityState.CircuitState : CircuitState.Closed);
        }
        public static async Task <HttpResponseMessage> CounterClientGet(
            [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequestMessage req,
            [DurableClient] IDurableClient client)
        {
            var entityId = new EntityId(nameof(Counter), "myCounter");
            //var currentState = await client.ReadEntityStateAsync<CounterClass>(entityId); //<--for classes, not functins
            var currentValue = await client.ReadEntityStateAsync <JObject>(entityId);

            return(req.CreateResponse(HttpStatusCode.OK, currentValue.EntityState));
        }
        public async Task <BreakerState> GetBreakerState(string circuitBreakerId, ILogger log,
                                                         IDurableClient durableClient)
        {
            log?.LogCircuitBreakerMessage(circuitBreakerId, $"Getting breaker state for circuit-breaker = '{circuitBreakerId}'.");

            var readState = await durableClient.ReadEntityStateAsync <BreakerState>(DurableCircuitBreakerEntity.GetEntityId(circuitBreakerId));

            // We present a not-yet-initialized circuit-breaker as null (it will be initialized when successes or failures are first posted against it).
            return(readState.EntityExists && readState.EntityState != null ? readState.EntityState : null);
        }
Пример #10
0
        public static async Task <EntityStateResponse <T> > ReadUserEntityAsync <T>(this IDurableClient client, string user)
        {
            var id     = user.AsEntityIdFor <T>();
            var result = await client.ReadEntityStateAsync <T>(id);

            if (result.EntityState is IHaveLists)
            {
                ((IHaveLists)result.EntityState).RestoreLists();
            }
            return(result);
        }
        public static async Task <IActionResult> HttpStart([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Tracker/{batchId}")] HttpRequest req,
                                                           [DurableClient] IDurableClient context,
                                                           string batchId)
        {
            var entityId = new EntityId("BatchTracker", batchId);

            context.SignalEntityAsync(entityId, "Add").Wait();

            var currentValue = await context.ReadEntityStateAsync <BatchTracker>(entityId);

            return(new OkObjectResult(currentValue));
        }
Пример #12
0
        public static async Task <IActionResult> StoreGet(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "store")] HttpRequest req,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            var target = new EntityId(nameof(InventoryEntity), "onestore");
            var store  = await client.ReadEntityStateAsync <InventoryEntity>(target);

            return(new JsonResult(store.EntityState?.Items ?? new List <InventoryItem>()));
        }
Пример #13
0
        public async Task CanInteractWithEntities()
        {
            IDurableClient client = await this.GetDurableClientAsync();

            var entityId = new EntityId(nameof(Functions.Counter), Guid.NewGuid().ToString("N"));
            EntityStateResponse <int> result = await client.ReadEntityStateAsync <int>(entityId);

            Assert.False(result.EntityExists);

            await Task.WhenAll(
                client.SignalEntityAsync(entityId, "incr"),
                client.SignalEntityAsync(entityId, "incr"),
                client.SignalEntityAsync(entityId, "incr"),
                client.SignalEntityAsync(entityId, "add", 4));

            await Task.Delay(TimeSpan.FromSeconds(5));

            result = await client.ReadEntityStateAsync <int>(entityId);

            Assert.True(result.EntityExists);
            Assert.Equal(7, result.EntityState);
        }
Пример #14
0
        public static async Task <HttpResponseMessage> RegionGet(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "region/{location}")] HttpRequestMessage req,
            [DurableClient] IDurableClient client,
            ILogger log,
            int location)
        {
            var regionEntity = new EntityId(nameof(RegionEntity), location.ToString());
            var response     = await client.ReadEntityStateAsync <RegionEntity>(regionEntity);

            return(response.EntityExists
                    ? req.CreateResponse(HttpStatusCode.OK, response.EntityState.Users)
                    : req.CreateResponse(HttpStatusCode.NotFound));
        }
Пример #15
0
        public static async Task <IActionResult> Stats(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            log.LogInformation("Request for stats.");

            // read the entity and return the state or an empty object
            // when not found
            var stats = await client.ReadEntityStateAsync <RegistryStats>(StatsId);

            return(new OkObjectResult(stats.EntityExists ?
                                      stats.EntityState : new RegistryStats()));
        }
        public static async Task <HttpResponseMessage> UserFollowsGet(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "user/{userId}/follows")] HttpRequestMessage req,
            [DurableClient] IDurableClient client,
            ILogger log,
            string userId)
        {
            Authenticate(req, userId);
            var target  = new EntityId(nameof(UserFollows), userId);
            var follows = await client.ReadEntityStateAsync <UserFollows>(target);

            return(follows.EntityExists
                    ? req.CreateResponse(HttpStatusCode.OK, follows.EntityState.FollowedUsers)
                    : req.CreateResponse(HttpStatusCode.NotFound));
        }
Пример #17
0
        public static async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            [DurableClient] IDurableClient durableClient,
            ILogger log)
        {
            var neoEventCountEntity = await durableClient.ReadEntityStateAsync <ProcessedNeoEventCounter>(
                EntityIdBuilder.BuildForProcessedNeoEventCounter());

            if (neoEventCountEntity.EntityExists)
            {
                var count = await neoEventCountEntity.EntityState.GetAsync();

                return(new OkObjectResult($"{count} NEO events have been processed."));
            }

            return(new NotFoundObjectResult("The IProcessedNeoEventCounter entity was not found."));
        }
Пример #18
0
        public static async Task <List <Inventory> > DeserializeListForUserWithClient(this InventoryList list, string user, IDurableClient client)
        {
            var result = new List <Inventory>();

            list.RestoreLists();
            foreach (var item in list.InventoryList)
            {
                var id        = user.AsEntityIdFor <Inventory>(item);
                var inventory = await client.ReadEntityStateAsync <Inventory>(id);

                if (inventory.EntityExists)
                {
                    result.Add(inventory.EntityState);
                }
            }
            return(result);
        }
        public async Task <IActionResult> GetHistory(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "bins/{binId}/history")] HttpRequest req,
            [DurableClient] IDurableClient client,
            string binId)
        {
            var bin = this._helper.GetBin(binId);

            var entity = await client.ReadEntityStateAsync <Bin>(bin);

            var payload = entity.EntityState;

            payload.Navigation = BinNavigation.Parse(binId, req.IsHttps, req.Host.ToString());

            var result = new JsonObjectContentResult(HttpStatusCode.OK, payload);

            return(result);
        }
        public async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous,
                         "get",
                         Route = "history/{binId}")] HttpRequest request,
            [DurableClient] IDurableClient client,
            string binId,
            ILogger log,
            ExecutionContext context)
        {
            try
            {
                log.LogInformation(new EventId(200), "{BinId}, {Message}", binId, $"A request to return request history for bin '{binId}' has been received.");
                if (!RequestBinService.IsBinIdValid(binId, out var validationMessage))
                {
                    log.LogError(new EventId(291), "{BinId}, {Message}", binId, $"Invalid Bin Id '{binId}'.");
                    return(NewHtmlContentResult(HttpStatusCode.BadRequest,
                                                RequestBinRenderer.RenderToString(binId, "Invalid", null, validationMessage)));
                }
                var binUrl              = $"http{(request.IsHttps ? "s" : "")}://{request.Host}{request.Path.ToString().Replace("/history", "")}";
                var encodedBinId        = RequestBinService.EncodeBinId(binId);
                var durableRequestBinId = new EntityId(nameof(RequestBin), encodedBinId);

                //Read the state of a Durable Entity
                var durableRequestBin = await client.ReadEntityStateAsync <RequestBin>(durableRequestBinId);

                var requestBinHistory = RequestBinRenderer.RenderToString(binId, binUrl, durableRequestBin.EntityState?.RequestHistoryItems);

                log.LogInformation(new EventId(210), "{BinId}, {Message}", binId, $"Request history for bin '{binId}' returned successfully.");
                return(NewHtmlContentResult(HttpStatusCode.OK, requestBinHistory));
            }
            catch (Exception ex)
            {
                log.LogError(new EventId(290), ex, "{BinId}", binId, $"Error occurred trying to return the request history for bin: '{binId}'");
                try
                {
                    return(NewHtmlContentResult(HttpStatusCode.InternalServerError,
                                                RequestBinRenderer.RenderToString(binId, "", null, $"500 Internal Server Error. Execution Id: '{context.InvocationId.ToString()}'")));
                }
                catch (Exception)
                {
                    //In case the custom Html render didn't work, return a message without format.
                    return(NewHtmlContentResult(HttpStatusCode.InternalServerError,
                                                $"500 Internal Server Error. Execution Id: '{context.InvocationId.ToString()}'"));
                }
            }
        }
Пример #21
0
        public static async Task <IActionResult> Peek(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Peek/{id}")] HttpRequest req,
            string id,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            log.LogInformation("Request to peek at registry {id}.", id);

            // id is required on route Peek/{id}
            if (string.IsNullOrWhiteSpace(id))
            {
                return(new BadRequestObjectResult("Id is required after Finish/ on the route."));
            }

            // default to open
            var status = "Open";

            // confirm a workflow exists
            var instance = await client.GetStatusAsync(id);

            // doesn't exist
            if (instance == null)
            {
                return(new BadRequestObjectResult($"Unable to find workflow with id {id}"));
            }

            // no longer running so set as "closed"
            if (instance.RuntimeStatus != OrchestrationRuntimeStatus.Running)
            {
                status = "Closed";
            }

            // get the registry
            var registry = await client.ReadEntityStateAsync <RegistryList>(
                id.AsRegistryId());

            // return status along with contents
            return(new OkObjectResult(new
            {
                status,
                registry = registry.EntityState
            }));
        }
Пример #22
0
        public static async Task <IActionResult> GameStatus(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "GameStatus/{username}")]
            HttpRequest req,
            [DurableClient] IDurableClient client,
            string username,
            ILogger log)
        {
            if (string.IsNullOrWhiteSpace(username))
            {
                log.LogWarning("No username passed.");
                return(new BadRequestObjectResult("Username is required."));
            }

            var userCheck = await client.ReadUserEntityAsync <User>(username);

            if (!userCheck.EntityExists)
            {
                log.LogWarning("Username {0} not found", username);
                return(new BadRequestObjectResult($"Username '{username}' does not exist."));
            }
            var monsterCheck = await client.ReadUserEntityAsync <Monster>(username);

            var inventoryCheck = await client.ReadUserEntityAsync <InventoryList>(username);

            if (inventoryCheck.EntityExists)
            {
                inventoryCheck.EntityState.RestoreLists();
            }
            var roomCheck = await client.ReadUserEntityAsync <Room>(username);

            var userCount = await client.ReadEntityStateAsync <int>(
                UserCounter.Id);

            return(new OkObjectResult(new
            {
                user = userCheck.EntityState,
                activeUsers = userCount.EntityState,
                monster = monsterCheck.EntityState,
                inventory = inventoryCheck.EntityState?.InventoryList,
                room = roomCheck.EntityState
            }));
        }
Пример #23
0
        /// <summary>
        /// Gets or sets the Steam App List by using Azure Durable Entities. These entities are stored in Azure Blob Storage for resiliency
        /// and re-use. Without these entities, we would need to re-query for this data evey time a command came in.
        /// </summary>
        /// <param name="client"></param>
        /// <param name="processor"></param>
        /// <returns></returns>
        private static async Task <IDictionary <uint, string> > GetOrSetAppListAsync(IDurableClient client, SteamCommandProcessor processor)
        {
            var entityId       = new EntityId(nameof(SteamAppList), "myAppList");
            var entityResponse = await client.ReadEntityStateAsync <SteamAppList>(entityId);

            IDictionary <uint, string> appList = new Dictionary <uint, string>();

            if (entityResponse.EntityExists)
            {
                appList = entityResponse.EntityState.CurrentList;
            }
            else
            {
                appList = await processor.GetSteamAppListAsync();

                await client.SignalEntityAsync <ISteamAppList>(entityId, proxy => proxy.Set(appList));
            }

            return(appList);
        }
Пример #24
0
        public static async Task <IActionResult> ShoppingCartGet([HttpTrigger(AuthorizationLevel.Function, "get", Route = "user/{userId}/shoppingCart")] HttpRequest req, [DurableClient] IDurableClient client, ILogger log)
        {
            try
            {
                if (!req.Query.ContainsKey("userId"))
                {
                    throw new AppException(400, "User Id is missing from query");
                }

                var userId = req.Query["userId"];
                log.LogInformation($"Get Basket for user is called, user Id: {userId}");

                var catalogEntity = new EntityId(nameof(ShoppingCart), userId);
                var response      = await client.ReadEntityStateAsync <IShoppingCart>(catalogEntity);

                return(new OkObjectResult(response.EntityState.GetItemsAsync()));
            }
            catch (Exception ex)
            {
                return(ex.GetResponse(log));
            }
        }
Пример #25
0
        public static async Task <IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "data/{userId}")] HttpRequest req,
            string userId,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            try
            {
                var cacheId = new EntityId(nameof(ByteCache), $"cache{userId}");
                var state   = await client.ReadEntityStateAsync <ByteCache>(cacheId);

                if (state.EntityExists)
                {
                    return(new OkObjectResult(state.EntityState.Value));
                }

                //simulate call to external service to retrieve data
                await Task.Delay(TimeSpan.FromSeconds(2));

                var data = new byte[10];
                new Random().NextBytes(data);

                await client.SignalEntityAsync <ICache <byte[]> >(cacheId, proxy => proxy.Set(data));

                //await client.SignalEntityAsync(cacheId, "Set", data);

                var orchestratorId = await client.StartNewAsync(nameof(CacheOrchestrator), cacheId);

                var managementPayload = client.CreateHttpManagementPayload(orchestratorId);

                return(new OkObjectResult(new { Management = managementPayload, Data = data }));
            }
            catch (Exception e)
            {
                log.LogError(e.Message);
                return(new ExceptionResult(e, true));
            }
        }
Пример #26
0
        public static async Task <IActionResult> StoreGet([HttpTrigger(AuthorizationLevel.Function, "get", Route = "store")] HttpRequest req, [DurableClient] IDurableClient client, ILogger log)
        {
            try
            {
                log.LogInformation($"Store Get is called");

                var catalogEntity = new EntityId(nameof(Inventory), Inventory.Id);

                var response = await client.ReadEntityStateAsync <Inventory>(catalogEntity);

                if (!response.EntityExists)
                {
                    return(new NotFoundResult());
                }

                return(new OkObjectResult(response.EntityState.Items));
            }

            catch (Exception ex)
            {
                return(ex.GetResponse(log));
            }
        }
Пример #27
0
        public static async Task <IActionResult> RunAsync(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "cart/{id}")] HttpRequestMessage req,
            Guid id,
            [DurableClient] IDurableClient client,
            ILogger log)
        {
            if (id == Guid.Empty)
            {
                return((ActionResult) new BadRequestObjectResult("The ID is required"));
            }

            var entityId = new EntityId(nameof(ShoppingCartEntity), id.ToString());

            var stateResponse = await client.ReadEntityStateAsync <ShoppingCartEntity>(entityId);

            if (!stateResponse.EntityExists)
            {
                return((ActionResult) new NotFoundObjectResult("No cart with this id"));
            }

            var response = stateResponse.EntityState.GetCartItems();

            return((ActionResult) new OkObjectResult(response.Result));
        }
Пример #28
0
        public static async Task <IActionResult> CountParallelSignals(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = nameof(CountParallelSignals))] HttpRequest req,
            [DurableClient] IDurableClient client)
        {
            try
            {
                // input is of the form "nnn,mmm"
                // nnn - number of signals to send
                // mmm - number of entities to distribute the signals over

                string input          = await new StreamReader(req.Body).ReadToEndAsync();
                int    commaPosition  = input.IndexOf(',');
                int    numberSignals  = int.Parse(input.Substring(0, commaPosition));
                int    numberEntities = int.Parse(input.Substring(commaPosition + 1));
                var    entityPrefix   = Guid.NewGuid().ToString("N");
                EntityId MakeEntityId(int i) => new EntityId("Counter", $"{entityPrefix}-{i/100:D6}!{i%100:D2}");

                DateTime startTime = DateTime.UtcNow;

                if (numberSignals % numberEntities != 0)
                {
                    throw new ArgumentException("numberSignals must be a multiple of numberEntities");
                }

                // send the specified number of signals to the entity
                await SendIncrementSignals(client, numberSignals, 50, (i) => MakeEntityId(i % numberEntities));

                // poll the entities until the expected count is reached
                async Task <double?> WaitForCount(int i)
                {
                    var random = new Random();

                    while ((DateTime.UtcNow - startTime) < TimeSpan.FromMinutes(5))
                    {
                        var response = await client.ReadEntityStateAsync <Counter>(MakeEntityId(i));

                        if (response.EntityExists &&
                            response.EntityState.CurrentValue == numberSignals / numberEntities)
                        {
                            return((response.EntityState.LastModified - startTime).TotalSeconds);
                        }

                        await Task.Delay(TimeSpan.FromSeconds(2 + random.NextDouble()));
                    }

                    return(null);
                };

                var waitTasks = Enumerable.Range(0, numberEntities).Select(i => WaitForCount(i)).ToList();

                await Task.WhenAll(waitTasks);

                var results = waitTasks.Select(t => t.Result);

                if (results.Any(result => result == null))
                {
                    return(new OkObjectResult($"timed out after {(DateTime.UtcNow - startTime)}.\n"));
                }

                return(new OkObjectResult($"received {numberSignals} signals on {numberEntities} entities in {results.Max():F1}s.\n"));
            }
            catch (Exception e)
            {
                return(new OkObjectResult(e.ToString()));
            }
        }