public async Task <IHttpActionResult> Post(TaskRequest request) { Logger.InfoFormat("Received task {taskJson}", request.TaskJson); TaskBase taskObject = request.TaskJson.FromJson <TaskBase>(); // check error rate string cacheKey = $"error-rate:{taskObject.GetType().Name}"; if (CurrentHttpContext.Instance().Cache.Get(cacheKey) is not RateLimit cacheItem) { // allow 1 error per hour cacheItem = RateLimit.Create(cacheKey, 1, 1, 3600); CurrentHttpContext.Instance().Cache.Insert( cacheKey, cacheItem, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, (key, item, reason) => Logger.InfoFormat( "cache item '{key}' removed due to '{reason}'", key, reason)); } _ = cacheItem.UpdateAllowance(DateTime.Now); if (cacheItem.Allowance < 1) { Logger.InfoFormat( "throttling {cacheKey} due to allowance = {allowance}", cacheKey, cacheItem.Allowance.ToString("N2")); return(Ok( $"throttled due to unhandled exception (allowance = {cacheItem.Allowance:N2})")); } IDisposable scope = NLog.NestedDiagnosticsLogicalContext.Push(taskObject.BusinessKey); try { Logger.Info("begin"); IHttpActionResult result = await Transact <IHttpActionResult>(async databases => { // check for published task PublishedTask?publishedTask = await databases.Snittlistan.PublishedTasks.SingleOrDefaultAsync( x => x.MessageId == request.MessageId); if (publishedTask is null) { return(BadRequest($"no published task found with message id {request.MessageId}")); } if (publishedTask.HandledDate.HasValue) { return(Ok($"task with message id {publishedTask.MessageId} already handled")); } bool handled = await HandleMessage( taskObject, request.CorrelationId ?? default, request.MessageId ?? default, databases); if (handled) { publishedTask.MarkHandled(DateTime.Now); } return(Ok()); }); return(result); } catch (Exception ex) { Logger.WarnFormat( ex, "decreasing allowance for {cacheKey}", cacheKey); _ = cacheItem.DecreaseAllowance(); throw; } finally { Logger.Info("end"); scope.Dispose(); } }
public async Task Handle(HandlerContext <TCommand> context) { (string key, int rate, int perSeconds) = GetRate(context); // check cache if (CurrentHttpContext.Instance().Cache.Get(key) is not RateLimit cacheItem) { cacheItem = RateLimit.Create(key, 1, rate, perSeconds); Logger.InfoFormat( "add {key} to cache: {@cacheItem}", key, cacheItem); CurrentHttpContext.Instance().Cache.Insert( key, cacheItem, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, (key, item, reason) => Logger.InfoFormat( "cache item '{key}' removed due to '{reason}'", key, reason)); } // check database KeyValueProperty?rateLimitProperty = await CompositionRoot.Databases.Snittlistan.KeyValueProperties .SingleOrDefaultAsync(x => x.Key == key); if (rateLimitProperty == null) { KeyValueProperty keyValueProperty = new( context.Tenant.TenantId, key, RateLimit.Create(key, 1, rate, perSeconds)); rateLimitProperty = CompositionRoot.Databases.Snittlistan.KeyValueProperties.Add(keyValueProperty); } else { rateLimitProperty.ModifyValue <RateLimit>( r => r.SetRate(rate).SetPerSeconds(perSeconds), x => Logger.InfoFormat("before: {@x}", x), x => Logger.InfoFormat("after: {@x}", x)); } DateTime now = DateTime.Now; rateLimitProperty.ModifyValue <RateLimit>( x => x.UpdateAllowance(now), x => Logger.InfoFormat("before: {@x}", x), x => Logger.InfoFormat("after: {@x}", x)); _ = cacheItem.UpdateAllowance(now); double allowance = rateLimitProperty.GetValue <RateLimit, double>(x => x.Allowance); if (allowance < 1 || cacheItem.Allowance < 1) { string message = $"(db) allowance = {allowance:N2}, (cache) allowance = {cacheItem.Allowance:N2}, wait to reach 1"; throw new HandledException(message); } TEmail email = await CreateEmail(context); EmailState state = email.State; _ = CompositionRoot.Databases.Snittlistan.SentEmails.Add(new( email.From, email.To, email.Bcc, email.Subject, state)); Logger.InfoFormat("sending email {@email}", email); await CompositionRoot.EmailService.SendAsync(email); rateLimitProperty.ModifyValue <RateLimit>( x => x.DecreaseAllowance(), x => Logger.InfoFormat("before: {@x}", x), x => Logger.InfoFormat("after: {@x}", x)); _ = cacheItem.DecreaseAllowance(); }