Task <IList <TableResult> > ExecuteExportedMirrorBatchAsync(TableBatchOperation batch, IList <TableResult> originalResponse) { var exportedBatch = new TableBatchOperation(); var exportedOriginalResponse = new List <TableResult>(); for (int i = 0; i < batch.Count; i++) { TableOperation op = batch[i]; var mtableEntity = (MTableEntity)op.GetEntity(); if (MigratingTable.RowKeyIsInternal(mtableEntity.RowKey)) { continue; } exportedOriginalResponse.Add(originalResponse[i]); Debug.Assert(op.GetOperationType() == TableOperationType.InsertOrReplace); DynamicTableEntity exported = mtableEntity.Export <DynamicTableEntity>(); if (mtableEntity.deleted) { exported.ETag = ChainTable2Constants.ETAG_DELETE_IF_EXISTS; exportedBatch.Delete(exported); } else { exported.ETag = null; exportedBatch.InsertOrReplace(exported); } } return(referenceTable.ExecuteMirrorBatchAsync(exportedBatch, exportedOriginalResponse)); }
// XXX: Preserve the entity type without the caller having to specify it? public static TableOperation CopyOperation <TEntity>(TableOperation op) where TEntity : ITableEntity, new() { ITableEntity newEntity = CopyEntity <TEntity>(op.GetEntity()); switch (op.GetOperationType()) { case TableOperationType.Insert: return(TableOperation.Insert(newEntity)); case TableOperationType.Replace: return(TableOperation.Replace(newEntity)); case TableOperationType.Merge: return(TableOperation.Merge(newEntity)); case TableOperationType.Delete: return(TableOperation.Delete(newEntity)); case TableOperationType.InsertOrReplace: return(TableOperation.InsertOrReplace(newEntity)); case TableOperationType.InsertOrMerge: return(TableOperation.InsertOrMerge(newEntity)); default: throw new NotImplementedException(); } }
async Task <IList <TableResult> > ExecuteBatchOnNewTableAsync(MTableConfiguration config, TableBatchOperation batch, TableRequestOptions requestOptions, OperationContext operationContext) { string partitionKey = ChainTableUtils.GetBatchPartitionKey(batch); await EnsurePartitionSwitchedAsync(partitionKey, requestOptions, operationContext); if (config.state <= TableClientState.PREFER_NEW) { await EnsureAffectedRowsMigratedAsync(batch, requestOptions, operationContext); } Attempt: // Batch on new table. var query = GenerateQueryForAffectedRows(batch); IList <MTableEntity> newRows = await newTable.ExecuteQueryAtomicAsync(query, requestOptions, operationContext); Dictionary <string, MTableEntity> newDict = newRows.ToDictionary(ent => ent.RowKey); // NOTE! At this point, the read has not yet been annotated. It is annotated below. var newBatch = new TableBatchOperation(); var inputToNewTableIndexMapping = new List <int?>(); for (int i = 0; i < batch.Count; i++) { TableOperation op = batch[i]; ITableEntity passedEntity = op.GetEntity(); MTableEntity existingEntity = newDict.GetValueOrDefault(passedEntity.RowKey); TableOperation newOp = null; HttpStatusCode?errorCode = null; TranslateOperationForNewTable( op, existingEntity, config.state <= TableClientState.USE_NEW_WITH_TOMBSTONES, ref newOp, ref errorCode); if (errorCode != null) { Debug.Assert(newOp == null); await monitor.AnnotateLastBackendCallAsync(wasLinearizationPoint : true); throw ChainTableUtils.GenerateBatchException(errorCode.Value, i); } if (newOp != null) { inputToNewTableIndexMapping.Add(newBatch.Count); newBatch.Add(newOp); } else { inputToNewTableIndexMapping.Add(null); } } await monitor.AnnotateLastBackendCallAsync(); IList <TableResult> newResults; try { newResults = await newTable.ExecuteBatchAsync(newBatch, requestOptions, operationContext); } catch (ChainTableBatchException) { // XXX: Try to distinguish expected concurrency exceptions from unexpected exceptions? await monitor.AnnotateLastBackendCallAsync(); goto Attempt; } // We made it! var results = new List <TableResult>(); for (int i = 0; i < batch.Count; i++) { ITableEntity passedEntity = batch[i].GetEntity(); int? newTableIndex = inputToNewTableIndexMapping[i]; string newETag = (IsBugEnabled(MTableOptionalBug.TombstoneOutputETag) ? newTableIndex != null : batch[i].GetOperationType() == TableOperationType.Delete) ? null : newResults[newTableIndex.Value].Etag; if (newETag != null) { passedEntity.ETag = newETag; } results.Add(new TableResult { HttpStatusCode = (int)( (batch[i].GetOperationType() == TableOperationType.Insert) ? HttpStatusCode.Created : HttpStatusCode.NoContent), Etag = newETag, Result = passedEntity, }); } await monitor.AnnotateLastBackendCallAsync(wasLinearizationPoint : true, successfulBatchResult : results); return(results); }
public override Task <IList <TableResult> > ExecuteMirrorBatchAsync( TableBatchOperation originalBatch, IList <TableResult> originalResponse, TableRequestOptions requestOptions = null, OperationContext operationContext = null) { ChainTableUtils.GetBatchPartitionKey(originalBatch); // For validation; result ignored // Copy the table. Entities are aliased to the original table, so don't mutate them. var tmpTable = new SortedDictionary <PrimaryKey, DynamicTableEntity>(table); int tmpNextEtag = nextEtag; var results = new List <TableResult>(); for (int i = 0; i < originalBatch.Count; i++) { TableOperation op = originalBatch[i]; TableOperationType opType = op.GetOperationType(); ITableEntity passedEntity = op.GetEntity(); PrimaryKey key = passedEntity.GetPrimaryKey(); DynamicTableEntity oldEntity = tmpTable.GetValueOrDefault(key); DynamicTableEntity newEntity = null; HttpStatusCode statusCode = HttpStatusCode.NoContent; if (opType == TableOperationType.Insert) { if (oldEntity != null) { throw ChainTableUtils.GenerateBatchException(HttpStatusCode.Conflict, i); } else { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(passedEntity); statusCode = HttpStatusCode.Created; } } else if (opType == TableOperationType.InsertOrReplace) { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(passedEntity); } else if (opType == TableOperationType.InsertOrMerge) { if (oldEntity == null) { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(passedEntity); } else { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(oldEntity); Merge(newEntity, passedEntity); } } else if (opType == TableOperationType.Delete && passedEntity.ETag == ChainTable2Constants.ETAG_DELETE_IF_EXISTS) { tmpTable.Remove(key); } else if (oldEntity == null) { throw ChainTableUtils.GenerateBatchException(HttpStatusCode.NotFound, i); } else if (string.IsNullOrEmpty(passedEntity.ETag)) { // Enforce this because real Azure table will. // XXX Ideally do this up front. throw new ArgumentException(string.Format("Operation {0} requires an explicit ETag.", i)); } else if (passedEntity.ETag != ChainTable2Constants.ETAG_ANY && oldEntity.ETag != passedEntity.ETag) { throw ChainTableUtils.GenerateBatchException(HttpStatusCode.PreconditionFailed, i); } else if (opType == TableOperationType.Delete) { tmpTable.Remove(key); } else if (opType == TableOperationType.Replace) { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(passedEntity); } else if (opType == TableOperationType.Merge) { newEntity = ChainTableUtils.CopyEntity <DynamicTableEntity>(oldEntity); Merge(newEntity, passedEntity); } else { // IChainTable2 does not allow Retrieve in a batch. throw new NotImplementedException(); } if (newEntity != null) { newEntity.ETag = (originalResponse != null) ? originalResponse[i].Etag : (tmpNextEtag++).ToString(); newEntity.Timestamp = DateTimeOffset.MinValue; // Arbitrary, deterministic tmpTable[key] = newEntity; } results.Add(new TableResult { Result = passedEntity, HttpStatusCode = (int)statusCode, Etag = (newEntity != null) ? newEntity.ETag : null, }); } // If we got here, commit. table = tmpTable; nextEtag = tmpNextEtag; for (int i = 0; i < originalBatch.Count; i++) { if (results[i].Etag != null) // not delete { originalBatch[i].GetEntity().ETag = results[i].Etag; } } return(Task.FromResult((IList <TableResult>)results)); }
void TranslateOperationForNewTable( TableOperation op, MTableEntity existingEntity, bool leaveTombstones, ref TableOperation newOp, ref HttpStatusCode?errorCode) { ITableEntity passedEntity = op.GetEntity(); TableOperationType opType = op.GetOperationType(); switch (opType) { case TableOperationType.Insert: if (existingEntity == null) { newOp = TableOperation.Insert(ChainTableUtils.CopyEntity <MTableEntity>(passedEntity)); } else if (existingEntity.deleted) { newOp = TableOperation.Replace(ImportWithIfMatch(passedEntity, existingEntity.ETag)); } else { errorCode = HttpStatusCode.Conflict; } break; case TableOperationType.Replace: if ((errorCode = CheckExistingEntity(passedEntity, existingEntity)) == null) { newOp = TableOperation.Replace(ImportWithIfMatch(passedEntity, existingEntity.ETag)); } break; case TableOperationType.Merge: if ((errorCode = CheckExistingEntity(passedEntity, existingEntity)) == null) { newOp = TableOperation.Merge(ImportWithIfMatch(passedEntity, existingEntity.ETag)); } break; case TableOperationType.Delete: string buggablePartitionKey, buggableRowKey; if (IsBugEnabled(MTableOptionalBug.DeletePrimaryKey)) { buggablePartitionKey = buggableRowKey = null; } else { buggablePartitionKey = passedEntity.PartitionKey; buggableRowKey = passedEntity.RowKey; } if (leaveTombstones) { if (passedEntity.ETag == ChainTable2Constants.ETAG_DELETE_IF_EXISTS) { newOp = TableOperation.InsertOrReplace(new MTableEntity { PartitionKey = buggablePartitionKey, RowKey = buggableRowKey, deleted = true }); } else if ((errorCode = CheckExistingEntity(passedEntity, existingEntity)) == null) { newOp = TableOperation.Replace(new MTableEntity { PartitionKey = buggablePartitionKey, RowKey = buggableRowKey, deleted = true, ETag = existingEntity.ETag }); } } else { if (passedEntity.ETag == ChainTable2Constants.ETAG_DELETE_IF_EXISTS) { if (existingEntity != null) { newOp = TableOperation.Delete(new MTableEntity { PartitionKey = buggablePartitionKey, RowKey = buggableRowKey, // It's OK to delete the entity and return success whether or not // the entity is a tombstone by the time it is actually deleted. ETag = IsBugEnabled(MTableOptionalBug.DeleteNoLeaveTombstonesETag) ? null : ChainTable2Constants.ETAG_ANY }); } // Otherwise generate nothing. // FIXME: This is not linearizable! It can also generate empty batches. } else if ((errorCode = CheckExistingEntity(passedEntity, existingEntity)) == null) { // Another client in USE_NEW_WITH_TOMBSTONES could concurrently replace the // entity with a tombstone, in which case we need to return 404 to the caller, // hence this needs to be conditioned on the existing ETag. newOp = TableOperation.Delete(new MTableEntity { PartitionKey = buggablePartitionKey, RowKey = buggableRowKey, ETag = IsBugEnabled(MTableOptionalBug.DeleteNoLeaveTombstonesETag) ? null : existingEntity.ETag }); } } break; case TableOperationType.InsertOrReplace: newOp = TableOperation.InsertOrReplace(ChainTableUtils.CopyEntity <MTableEntity>(passedEntity)); break; case TableOperationType.InsertOrMerge: newOp = TableOperation.InsertOrMerge(ChainTableUtils.CopyEntity <MTableEntity>(passedEntity)); break; default: throw new NotImplementedException(); } }