Developing Cloud Applications with Windows Azure Storage: Blobs

  • 3/15/2013

Conditional operations

It is often desirable to perform an operation on data only when particular conditions can be satisfied. It’s better still when such operations can be conditionally performed by the data storage service rather than burden the client application, because doing so reduces the time and costs associated with transporting data to the application. Filtration operations limit the consumable data to a subset of the complete set of data available, so transporting all of the data across the wire simply to discard portions of that data upon evaluation wastes time, bandwidth, and ultimately money.

Although the evaluation can often be done by the application on the client side of the wire, multiple requests may be necessary to retrieve and evaluate data. In addition to the cost and time of transmitting large volumes of unnecessary data, the elapsed time also increases the probability of a data collision when one application attempts to perform an operation on an entity, but before that operation can take place, another application performs a successful operation on it, which renders the first application’s copy of the entity as stale. Most blob operations can be performed conditionally based on their date of modification, or their ETag.

Conditional operations using REST

Conditional operations are implemented in blob storage by including one of four optional HTTP headers in the request, including a corresponding date and time or ETag value, as shown in Table 5-4.

Table 5-4 Conditional operation HTTP headers

HTTP header

Specified value

If-Modified-Since

DateTime

If-Unmodified-Since

DateTime

If-Match

ETag or wildcard (*)

If-None-Match

ETag or wildcard (*)

Reading data conditionally using the If-Modified-Since header can save unnecessary network bandwidth and data processing time (as well as associated costs for data transmission) by only transmitting the data when it’s modified. When the condition cannot be met, an HTTP status code is returned that indicates the reason the condition was not met. Table 5-5 lists these HTTP status codes.

Table 5-5 HTTP response codes returned for unsatisfied conditions

Conditional header

HTTP response codes when condition is not met

If-Modified-Since

304

Not Modified

If-Unmodified-Since

412

Precondition Failed

If-Match

412

Precondition Failed

If-None-Match

304

Not Modified

Conditional operations using the Windows Azure client library

The Windows Azure client library provides a convenient programming grammar for performing conditional operations. This grammar abstracts the setting of the underlying HTTP header to give a more comfortable and intuitive programming model to the developer. To explore conditional operations, you first need a blob in storage. Given a CloudStorageAccount credential object, the following code snippet sets up a proxy to blob storage, creates a blob named Data.txt, and uploads some data (the string “Data”) into that blob. It also sets up a retry policy, which you will see later in this chapter, and establishes timeouts.

// Create a blob and attach some metadata to it:
CloudBlobClient client = account.CreateCloudBlobClient();

// No retry for 306 (Unused), 4xx, 501 (Not Implemented), 505 (HTTP Version Not Supported)
client.RetryPolicy = new ExponentialRetry();

// Time server can process a request (default = 90 secs)
client.ServerTimeout = TimeSpan.FromSeconds(90);

// Time client can wait for response across all retries (default = disabled)
client.MaximumExecutionTime = TimeSpan.FromSeconds(5);

CloudBlobContainer container = client.GetContainerReference("demo").EnsureExists();
CloudBlockBlob blob = container.GetBlockBlobReference("Data.txt");
using (var stream = new MemoryStream("Data".Encode())) {
    blob.UploadFromStream(stream);
}

Conditional reads

Now that your test blob has been uploaded to storage, you can create an instance of BlobRequestOptions and set its AccessCondition property to an appropriate value to try various conditional means of retrieving it. The values of this property correspond directly with the HTTP headers shown in Table 5-4. Let’s say that you want to retrieve the contents of the blob but only if that content has been updated. You may have a copy of the blob you’ve cached, and you don’t want to waste valuable resources continuously re-fetching the same data you already have. You want to expend resources only when there is something new to retrieve. You can accomplish this using the IfModifiedSince method. First, you want to simulate what happens when the blob has not been updated by another party, so you pass the LastModifiedUtc property of the blob to the IfModifiedSince method, knowing that this condition could never be met and that you will deliberately fail, as depicted in the following code.

// Download blob content if newer than what we have:
try {
    blob.DownloadText(
        AccessCondition.GenerateIfModifiedSinceCondition(
            blob.Properties.LastModified.Value)); // Fails
}
catch (StorageException ex) {
    Console.WriteLine(String.Format("Failure: Status={0}({0:D}), Msg={1}",
       (HttpStatusCode)ex.RequestInformation.HttpStatusCode,
        ex.RequestInformation.HttpStatusMessage));
}

You can do the inverse of the previous example by reading a blob only if its contents have not been modified since a specified date using the IfNotModifiedSince static method of the AccessCondition class. You might do this as part of a process for archiving an older date. Here is the code for this.

// Download blob content if more than 1 day old:
try {
    blob.DownloadText(
        AccessCondition.GenerateIfNotModifiedSinceCondition(
            DateTimeOffset.Now.AddDays(-1))); // Fails
}
catch (StorageException ex) {
    Console.WriteLine(String.Format("Failure: Status={0}({0:D}), Msg={1}",
       (HttpStatusCode)ex.RequestInformation.HttpStatusCode,
        ex.RequestInformation.HttpStatusMessage));
}

Conditional updates

You can perform updates conditionally, too. For example, many applications require optimistic concurrency when updating data. You want to replace an existing blob’s contents, but only if someone hasn’t updated the blob since you last retrieved it. If the blob was updated by another party, consider your copy of the blob to be stale and handle it according to your application’s business logic for a concurrency collision. You accomplish this by using the static IfMatch method of the AccessCondition class to conditionally perform an action only if the properties match. If no updates are made to the target blob, the ETag properties of two blobs are identical and the update succeeds. If an update has occurred to the targeted blob (for example, the ETag properties do not match), a StorageClientException exception is thrown.

// Upload new content if the blob wasn't changed behind your back:
try {
    blob.UploadText("Succeeds",
        AccessCondition.GenerateIfMatchCondition(blob.Properties.ETag)); // Succeeds
}
catch (StorageException ex) {
    Console.WriteLine(String.Format("Failure: Status={0}({0:D}), Msg={1}",
         (HttpStatusCode)ex.RequestInformation.HttpStatusCode,
          ex.RequestInformation.HttpStatusMessage));
}

When contention is encountered in an optimistic concurrency scenario, the usual countermeasure is to catch the exception, notify the requesting user or application of the contention, and then offer the option of fetching a fresh copy of the data. Generally, this means that the user has lost his revisions and must reapply his edits to the fresh copy before re-attempting to save his changes.

Another common application requirement is to create a blob in storage, but only if the blob doesn’t already exist. You can use the asterisk wildcard character to match on any value. In the following code, when no properties match anything (for example, the blob does not already exist), you proceed with uploading. If the blob already exists, a StorageClientException is thrown. Generally, in production code, you should catch this exception and handle the situation according to the specific requirements of your application.

// Upload your content if it doesn't already exist:
try {
    // Fails
    blob.UploadText("Fails", AccessCondition.GenerateIfNoneMatchCondition("*"));
}
catch (StorageException ex) {
    Console.WriteLine(String.Format("Failure: Status={0}({0:D}), Msg={1}",
        (HttpStatusCode)ex.RequestInformation.HttpStatusCode,
         ex.RequestInformation.HttpStatusMessage));
}