/// <summary> /// Updates the checkpoint using the given information for the associated partition and consumer group in the storage blob service. /// </summary> /// /// <param name="checkpoint">The checkpoint containing the information to be stored.</param> /// /// <returns>A task to be resolved on when the operation has completed.</returns> /// public override async Task UpdateCheckpointAsync(Checkpoint checkpoint) { var blobName = $"{ checkpoint.FullyQualifiedNamespace }/{ checkpoint.EventHubName }/{ checkpoint.ConsumerGroup }/{ checkpoint.PartitionId }"; BlobClient blobClient = _containerClient.GetBlobClient(blobName); BlobProperties currentBlob; try { currentBlob = (await blobClient.GetPropertiesAsync().ConfigureAwait(false)).Value; } catch (StorageRequestFailedException ex) when(ex.ErrorCode == BlobErrorCode.BlobNotFound) { Log($"Checkpoint with partition id = '{ checkpoint.PartitionId }' could not be updated because no associated ownership was found."); return; } // In case this key does not exist, ownerIdentifier is set to null. The OwnerIdentifier in Checkpoint cannot // be null as well, so we won't be able to update the associated ownership. currentBlob.Metadata.TryGetValue(BlobMetadataKey.OwnerIdentifier, out var ownerIdentifier); if (ownerIdentifier == checkpoint.OwnerIdentifier) { var metadata = new Dictionary <string, string>() { { BlobMetadataKey.OwnerIdentifier, checkpoint.OwnerIdentifier }, { BlobMetadataKey.Offset, checkpoint.Offset.ToString() }, { BlobMetadataKey.SequenceNumber, checkpoint.SequenceNumber.ToString() } }; var accessConditions = new BlobAccessConditions { HttpAccessConditions = new HttpAccessConditions { IfMatch = currentBlob.ETag } }; try { await blobClient.SetMetadataAsync(metadata, accessConditions).ConfigureAwait(false); Log($"Checkpoint with partition id = '{ checkpoint.PartitionId }' updated."); } catch (StorageRequestFailedException ex) when(ex.ErrorCode == BlobErrorCode.ConditionNotMet) { Log($"Checkpoint with partition id = '{ checkpoint.PartitionId }' could not be updated because eTag has changed."); } } else { Log($"Checkpoint with partition id = '{ checkpoint.PartitionId }' could not be updated because owner has changed."); } }
/// <summary> /// Attempts to claim ownership of partitions for processing. /// </summary> /// /// <param name="partitionOwnership">An enumerable containing all the ownership to claim.</param> /// /// <returns>An enumerable containing the successfully claimed ownership.</returns> /// public override async Task <IEnumerable <PartitionOwnership> > ClaimOwnershipAsync(IEnumerable <PartitionOwnership> partitionOwnership) { var claimedOwnership = new List <PartitionOwnership>(); var metadata = new Dictionary <string, string>(); Response <BlobContentInfo> contentInfoResponse; Response <BlobInfo> infoResponse; foreach (var ownership in partitionOwnership) { metadata[BlobMetadataKey.OwnerIdentifier] = ownership.OwnerIdentifier; metadata[BlobMetadataKey.Offset] = ownership.Offset?.ToString() ?? String.Empty; metadata[BlobMetadataKey.SequenceNumber] = ownership.SequenceNumber?.ToString() ?? String.Empty; var blobAccessConditions = new BlobAccessConditions(); var blobName = $"{ ownership.EventHubName }/{ ownership.ConsumerGroup }/{ ownership.PartitionId }"; var blobClient = ContainerClient.GetBlobClient(blobName); try { // Even though documentation states otherwise, we cannot use UploadAsync when the blob already exists in // the current storage SDK. For this reason, we are using the specified ETag as an indication of what // method to use. if (ownership.ETag == null) { blobAccessConditions.HttpAccessConditions = new HttpAccessConditions { IfNoneMatch = new ETag("*") }; MemoryStream blobContent = null; try { blobContent = new MemoryStream(new byte[0]); contentInfoResponse = await blobClient.UploadAsync(blobContent, metadata : metadata, blobAccessConditions : blobAccessConditions).ConfigureAwait(false); } catch (StorageRequestFailedException ex) when(ex.ErrorCode == BlobErrorCode.BlobAlreadyExists) { // A blob could have just been created by another Event Processor that claimed ownership of this // partition. In this case, there's no point in retrying because we don't have the correct ETag. Log($"Ownership with partition id = '{ ownership.PartitionId }' is not claimable."); continue; } finally { blobContent?.Dispose(); } ownership.LastModifiedTime = contentInfoResponse.Value.LastModified; ownership.ETag = contentInfoResponse.Value.ETag.ToString(); } else { blobAccessConditions.HttpAccessConditions = new HttpAccessConditions { IfMatch = new ETag(ownership.ETag) }; try { infoResponse = await blobClient.SetMetadataAsync(metadata, blobAccessConditions).ConfigureAwait(false); } catch (StorageRequestFailedException ex) when(ex.ErrorCode == BlobErrorCode.BlobNotFound) { // No ownership was found, which means the ETag should have been set to null in order to // claim this ownership. For this reason, we consider it a failure and don't try again. Log($"Ownership with partition id = '{ ownership.PartitionId }' is not claimable."); continue; } ownership.LastModifiedTime = infoResponse.Value.LastModified; ownership.ETag = infoResponse.Value.ETag.ToString(); } // Small workaround to retrieve the eTag. The current storage SDK returns it enclosed in // double quotes ('"ETAG_VALUE"' instead of 'ETAG_VALUE'). var match = DoubleQuotesExpression.Match(ownership.ETag); if (match.Success) { ownership.ETag = match.Groups[1].ToString(); } claimedOwnership.Add(ownership); Log($"Ownership with partition id = '{ ownership.PartitionId }' claimed."); } catch (StorageRequestFailedException ex) when(ex.ErrorCode == BlobErrorCode.ConditionNotMet) { Log($"Ownership with partition id = '{ ownership.PartitionId }' is not claimable."); } } return(claimedOwnership); }