async Task <ResourceResponse <Document> > SaveQueueItem(InternalCosmosDBQueueItem queueItem) { try { var uri = UriFactory.CreateDocumentCollectionUri(this.settings.QueueCollectionDefinition.DbName, this.settings.QueueCollectionDefinition.CollectionName); var reqOptions = new RequestOptions() { AccessCondition = new AccessCondition() { Condition = queueItem.etag, Type = AccessConditionType.IfMatch } }; var res = await queueCollectionClient.UpsertDocumentAsync(uri, queueItem, reqOptions); this.logger?.LogTrace($"Queue item {queueItem.id} updated, costs: {res.RequestCharge}, latency: {res.RequestLatency.TotalMilliseconds}ms"); return(res); } catch (Exception ex) { this.logger?.LogError(ex, $"Error persisting queue item {queueItem.id}"); } return(null); }
/// <summary> /// Tries to adquire lock in queue item to be the actual processor /// </summary> /// <remarks> /// Successfully adquiring a lock means: /// - Document was updated with { status: 'InProgress', workerId: '[this_worker_id]', processStartTime: currentTime }, if the ETag did not change (optimistic locking) /// </remarks> /// <param name="documents"></param> /// <returns></returns> private async Task <IList <InternalCosmosDBQueueItem> > TryAdquireLock(IEnumerable <Document> documents) { // TODO: Replace with stored procedure to batch lock items var result = new List <InternalCosmosDBQueueItem>(); foreach (var doc in documents) { try { this.logger?.LogTrace($"Trying to lock item {doc.Id}"); var updatedDocument = new InternalCosmosDBQueueItem { status = QueueItemStatus.InProgress, data = doc.GetPropertyValue <object>("data"), id = doc.Id, processStartTime = doc.GetPropertyValue <long>(nameof(InternalCosmosDBQueueItem.processStartTime)), queuedTime = doc.GetPropertyValue <long>(nameof(InternalCosmosDBQueueItem.queuedTime)), errors = doc.GetPropertyValue <int>(nameof(InternalCosmosDBQueueItem.errors)), completedTime = doc.GetPropertyValue <long>(nameof(InternalCosmosDBQueueItem.completedTime)), }; updatedDocument.SetWorker(this.workerId).SetWorkerExpiration(settings.ProcessingItemTimeout).SetProcessStartTime(); var res = await SaveQueueItem(updatedDocument); this.logger?.LogTrace($"Queue item {updatedDocument.id} adquired lock succeeded, costs: {res.RequestCharge}, latency: {res.RequestLatency.TotalMilliseconds}ms"); var savedDocument = JsonConvert.DeserializeObject <InternalCosmosDBQueueItem>(res.Resource.ToString()); savedDocument.etag = res.ResponseHeaders["ETag"]; result.Add(savedDocument); } catch (DocumentClientException documentClientException) { if (documentClientException.StatusCode != System.Net.HttpStatusCode.PreconditionFailed) { this.logger?.LogError(documentClientException, $"Error during queue item {doc.Id} adquired lock attempt"); } } catch (Exception ex) { this.logger?.LogError(ex, $"Error during queue item {doc.Id} adquired lock attempt"); } } return(result); }
/// <summary> /// Processes a single queue item /// </summary> /// <param name="queueItem"></param> /// <param name="persistDocument"></param> /// <returns></returns> async Task ProcessSingleItem(InternalCosmosDBQueueItem queueItem, bool persistDocument) { // Persist document only if Auto complete is enabled persistDocument = persistDocument & this.settings.AutoComplete; try { var externalQueueItem = new CosmosDBQueueItem(this, queueItem); var handlerStatus = await OnMessage(externalQueueItem); if (handlerStatus) { if (this.settings.AutoComplete) { queueItem.SetAsComplete(); } this.logger?.LogTrace($"Processing item {queueItem.id} succeeded"); } else { if (this.settings.AutoComplete) { queueItem.SetAsPending(); queueItem.errors++; } this.logger?.LogTrace($"Processing item {queueItem.id} failed"); } } catch (Exception ex) { this.logger?.LogError(ex, $"Error processing queue item {queueItem.id}"); queueItem.errors++; queueItem.SetAsPending(); // If the message handler failed we saved as failed persistDocument = true; } if (persistDocument) { await SaveQueueItem(queueItem); } }
/// <summary> /// Queue item returning queue item identifier /// </summary> /// <param name="payload"></param> /// <returns></returns> public async Task <string> Queue(string id, object payload) { if (!this.initialized) { throw new InvalidOperationException("Must call Initialize() first"); } var queueItem = new InternalCosmosDBQueueItem() { id = id, data = payload }; var uri = UriFactory.CreateDocumentCollectionUri(this.settings.QueueCollectionDefinition.DbName, this.settings.QueueCollectionDefinition.CollectionName); var res = await this.queueCollectionClient.CreateDocumentAsync(uri, queueItem); this.logger?.LogTrace($"Item {res.Resource.Id} queued, costs: {res.RequestCharge}, latency: {res.RequestLatency.TotalMilliseconds}ms"); return(res.Resource.Id); }
/// <summary> /// Completes the message queue /// </summary> /// <param name="queueItem"></param> /// <returns></returns> public async Task Abandon(InternalCosmosDBQueueItem queueItem) { queueItem.SetAsPending(); queueItem.errors++; await SaveQueueItem(queueItem); }
/// <summary> /// Completes the message queue /// </summary> /// <param name="queueItem"></param> /// <returns></returns> public async Task Complete(InternalCosmosDBQueueItem queueItem) { queueItem.SetAsComplete(); await SaveQueueItem(queueItem); }
/// <summary> /// Constructor /// </summary> /// <param name="consumer"></param> /// <param name="queueItem"></param> protected internal CosmosDBQueueItem(CosmosDBQueueConsumer consumer, InternalCosmosDBQueueItem queueItem) { this.consumer = consumer; this.queueItem = queueItem; }