How to manage concurrency in Azure Cognitive Search
When managing Azure Cognitive Search resources such as indexes and data sources, it's important to update resources safely, especially if resources are accessed concurrently by different components of your application. When two clients concurrently update a resource without coordination, race conditions are possible. To prevent this, Azure Cognitive Search offers an optimistic concurrency model. There are no locks on a resource. Instead, there is an ETag for every resource that identifies the resource version so that you can formulate requests that avoid accidental overwrites.
Tip
Conceptual code in a sample C# solution explains how concurrency control works in Azure Cognitive Search. The code creates conditions that invoke concurrency control. Reading the code fragment below might be sufficient for most developers, but if you want to run it, edit appsettings.json to add the service name and an admin api-key. Given a service URL of http://myservice.search.windows.net
, the service name is myservice
.
How it works
Optimistic concurrency is implemented through access condition checks in API calls writing to indexes, indexers, data sources, skillsets, and synonymMap resources.
All resources have an entity tag (ETag) that provides object version information. By checking the ETag first, you can avoid concurrent updates in a typical workflow (get, modify locally, update) by ensuring the resource's ETag matches your local copy.
The REST API uses an ETag on the request header.
The .NET SDK sets the ETag through an accessCondition object, setting the If-Match | If-Match-None header on the resource. Objects that use ETags, such as SynonymMap.ETag and SearchIndex.ETag, have an accessCondition object.
Every time you update a resource, its ETag changes automatically. When you implement concurrency management, all you're doing is putting a precondition on the update request that requires the remote resource to have the same ETag as the copy of the resource that you modified on the client. If a concurrent process has changed the remote resource already, the ETag will not match the precondition and the request will fail with HTTP 412. If you're using the .NET SDK, this manifests as a CloudException
where the IsAccessConditionFailed()
extension method returns true.
Note
There is only one mechanism for concurrency. It's always used regardless of which API is used for resource updates.
Use cases and sample code
The following code demonstrates accessCondition checks for key update operations:
- Fail an update if the resource no longer exists
- Fail an update if the resource version changes
Sample code from DotNetETagsExplainer program
class Program
{
// This sample shows how ETags work by performing conditional updates and deletes
// on an Azure Cognitive Search index.
static void Main(string[] args)
{
IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
IConfigurationRoot configuration = builder.Build();
SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);
Console.WriteLine("Deleting index...\n");
DeleteTestIndexIfExists(serviceClient);
// Every top-level resource in Azure Cognitive Search has an associated ETag that keeps track of which version
// of the resource you're working on. When you first create a resource such as an index, its ETag is
// empty.
Index index = DefineTestIndex();
Console.WriteLine(
$"Test index hasn't been created yet, so its ETag should be blank. ETag: '{index.ETag}'");
// Once the resource exists in Azure Cognitive Search, its ETag will be populated. Make sure to use the object
// returned by the SearchServiceClient! Otherwise, you will still have the old object with the
// blank ETag.
Console.WriteLine("Creating index...\n");
index = serviceClient.Indexes.Create(index);
Console.WriteLine($"Test index created; Its ETag should be populated. ETag: '{index.ETag}'");
// ETags let you do some useful things you couldn't do otherwise. For example, by using an If-Match
// condition, we can update an index using CreateOrUpdate and be guaranteed that the update will only
// succeed if the index already exists.
index.Fields.Add(new Field("name", AnalyzerName.EnMicrosoft));
index =
serviceClient.Indexes.CreateOrUpdate(
index,
accessCondition: AccessCondition.GenerateIfExistsCondition());
Console.WriteLine(
$"Test index updated; Its ETag should have changed since it was created. ETag: '{index.ETag}'");
// More importantly, ETags protect you from concurrent updates to the same resource. If another
// client tries to update the resource, it will fail as long as all clients are using the right
// access conditions.
Index indexForClient1 = index;
Index indexForClient2 = serviceClient.Indexes.Get("test");
Console.WriteLine("Simulating concurrent update. To start, both clients see the same ETag.");
Console.WriteLine($"Client 1 ETag: '{indexForClient1.ETag}' Client 2 ETag: '{indexForClient2.ETag}'");
// Client 1 successfully updates the index.
indexForClient1.Fields.Add(new Field("a", DataType.Int32));
indexForClient1 =
serviceClient.Indexes.CreateOrUpdate(
indexForClient1,
accessCondition: AccessCondition.IfNotChanged(indexForClient1));
Console.WriteLine($"Test index updated by client 1; ETag: '{indexForClient1.ETag}'");
// Client 2 tries to update the index, but fails, thanks to the ETag check.
try
{
indexForClient2.Fields.Add(new Field("b", DataType.Boolean));
serviceClient.Indexes.CreateOrUpdate(
indexForClient2,
accessCondition: AccessCondition.IfNotChanged(indexForClient2));
Console.WriteLine("Whoops; This shouldn't happen");
Environment.Exit(1);
}
catch (CloudException e) when (e.IsAccessConditionFailed())
{
Console.WriteLine("Client 2 failed to update the index, as expected.");
}
// You can also use access conditions with Delete operations. For example, you can implement an
// atomic version of the DeleteTestIndexIfExists method from this sample like this:
Console.WriteLine("Deleting index...\n");
serviceClient.Indexes.Delete("test", accessCondition: AccessCondition.GenerateIfExistsCondition());
// This is slightly better than using the Exists method since it makes only one round trip to
// Azure Cognitive Search instead of potentially two. It also avoids an extra Delete request in cases where
// the resource is deleted concurrently, but this doesn't matter much since resource deletion in
// Azure Cognitive Search is idempotent.
// And we're done! Bye!
Console.WriteLine("Complete. Press any key to end application...\n");
Console.ReadKey();
}
private static SearchServiceClient CreateSearchServiceClient(IConfigurationRoot configuration)
{
string searchServiceName = configuration["SearchServiceName"];
string adminApiKey = configuration["SearchServiceAdminApiKey"];
SearchServiceClient serviceClient =
new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
return serviceClient;
}
private static void DeleteTestIndexIfExists(SearchServiceClient serviceClient)
{
if (serviceClient.Indexes.Exists("test"))
{
serviceClient.Indexes.Delete("test");
}
}
private static Index DefineTestIndex() =>
new Index()
{
Name = "test",
Fields = new[] { new Field("id", DataType.String) { IsKey = true } }
};
}
}
Design pattern
A design pattern for implementing optimistic concurrency should include a loop that retries the access condition check, a test for the access condition, and optionally retrieves an updated resource before attempting to re-apply the changes.
This code snippet illustrates the addition of a synonymMap to an index that already exists. This code is from the Synonym C# example for Azure Cognitive Search.
The snippet gets the "hotels" index, checks the object version on an update operation, throws an exception if the condition fails, and then retries the operation (up to three times), starting with index retrieval from the server to get the latest version.
private static void EnableSynonymsInHotelsIndexSafely(SearchServiceClient serviceClient)
{
int MaxNumTries = 3;
for (int i = 0; i < MaxNumTries; ++i)
{
try
{
Index index = serviceClient.Indexes.Get("hotels");
index = AddSynonymMapsToFields(index);
// The IfNotChanged condition ensures that the index is updated only if the ETags match.
serviceClient.Indexes.CreateOrUpdate(index, accessCondition: AccessCondition.IfNotChanged(index));
Console.WriteLine("Updated the index successfully.\n");
break;
}
catch (CloudException e) when (e.IsAccessConditionFailed())
{
Console.WriteLine($"Index update failed : {e.Message}. Attempt({i}/{MaxNumTries}).\n");
}
}
}
private static Index AddSynonymMapsToFields(Index index)
{
index.Fields.First(f => f.Name == "category").SynonymMaps = new[] { "desc-synonymmap" };
index.Fields.First(f => f.Name == "tags").SynonymMaps = new[] { "desc-synonymmap" };
return index;
}
Next steps
Try modifying other samples to exercise ETags or AccessCondition objects.
search-dotnet-getting-started on GitHub. This repository includes the "DotNetEtagsExplainer" project.
azure-search-dotnet-samples on GitHub contains additional C# samples.
See also
Feedback
Submit and view feedback for