Gérer la concurrence dans Recherche Azure AI
Lors de la gestion de ressources Recherche Azure AI telles que des index et des sources de données, il est important de mettre à jour les ressources de manière sécurisée, surtout si elles sont accessibles simultanément par différents composants de votre application. Lorsque deux clients mettent à jour une ressource en même temps sans coordination, cela peut créer des conditions de concurrence. Pour éviter ce problème, Recherche Azure AI offre un modèle d’accès concurrentiel optimiste. Aucun verrou n’est appliqué aux ressources. Au lieu de cela, chaque ressource dispose d’une étiquette d’entité (ETag) qui identifie la version de la ressource afin que vous puissiez formuler des requêtes sans risquer de remplacements accidentels.
Fonctionnement
L’accès concurrentiel optimiste est implémenté via des contrôles de conditions d’accès dans les appels d’API écrivant dans des index, des indexeurs, des sources de données, des ensembles de compétences et des ressources synonymMap.
Toutes les ressources présentent une étiquette d’entité (ETag) qui fournit des informations sur la version de l’objet. En vérifiant d’abord l’ETag, vous pouvez éviter les mises à jour simultanées dans un flux de travail classique (obtention, modification locale, mise à jour) en vous assurant que l’ETag de la ressource correspond à celui de votre copie locale.
L’API REST utilise un ETag sur l’en-tête de demande.
Le kit de développement logiciel (SDK) .NET spécifie l’ETag via un objet accessCondition en définissant l’en-tête If-Match | If-Match-None sur la ressource. Les objets qui utilisent des ETags, tels que SynonymMap.ETag et SearchIndex.ETag, ont un objet accessCondition.
À chaque fois que vous mettez à jour une ressource, son ETag change automatiquement. Lorsque vous implémentez la gestion de l’accès concurrentiel, vous placez simplement sur la requête de mise à jour une condition préalable qui exige que la ressource distante présente le même ETag que la copie de la ressource que vous avez modifiée sur le client. Si un autre processus modifie la ressource distante, l’ETag ne correspond pas à la condition préalable et la requête échoue avec HTTP 412. Si vous utilisez le kit de développement logiciel (SDK) .NET, cette défaillance se présente comme une exception où la méthode d’extension IsAccessConditionFailed()
retourne la valeur true.
Remarque
Il n’existe qu’un seul mécanisme pour l’accès concurrentiel. Il est utilisé systématiquement, indépendamment de l’API ou du kit de développement logiciel (SDK) employés pour les mises à jour de ressources.
Exemple
Le code suivant illustre l’accès concurrentiel optimiste pour une opération de mise à jour. La deuxième mise à jour échoue, car l’ETag de l’objet est modifié par une mise à jour précédente. Plus précisément, lorsque l’ETag dans l’en-tête de demande ne correspond plus à l’ETag de l’objet, le service de recherche retourne un code d’état 400 (requête incorrecte) et la mise à jour échoue.
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using System;
using System.Net;
using System.Threading.Tasks;
namespace AzureSearch.SDKHowTo
{
class Program
{
// This sample shows how ETags work by performing conditional updates and deletes
// on an Azure Search index.
static void Main(string[] args)
{
string serviceName = "PLACEHOLDER FOR YOUR SEARCH SERVICE NAME";
string apiKey = "PLACEHOLDER FOR YOUR SEARCH SERVICE ADMIN API KEY";
// Create a SearchIndexClient to send create/delete index commands
Uri serviceEndpoint = new Uri($"https://{serviceName}.search.windows.net/");
AzureKeyCredential credential = new AzureKeyCredential(apiKey);
SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
// Delete index if it exists
Console.WriteLine("Check for index and delete if it already exists...\n");
DeleteTestIndexIfExists(adminClient);
// Every top-level resource in Azure 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.
SearchIndex index = DefineTestIndex();
Console.WriteLine(
$"Test searchIndex hasn't been created yet, so its ETag should be blank. ETag: '{index.ETag}'");
// Once the resource exists in Azure Search, its ETag is populated. Make sure to use the object
// returned by the SearchIndexClient. Otherwise, you will still have the old object with the
// blank ETag.
Console.WriteLine("Creating index...\n");
index = adminClient.CreateIndex(index);
Console.WriteLine($"Test index created; Its ETag should be populated. ETag: '{index.ETag}'");
// ETags prevent 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.
SearchIndex indexForClientA = index;
SearchIndex indexForClientB = adminClient.GetIndex("test-idx");
Console.WriteLine("Simulating concurrent update. To start, clients A and B see the same ETag.");
Console.WriteLine($"ClientA ETag: '{indexForClientA.ETag}' ClientB ETag: '{indexForClientB.ETag}'");
// indexForClientA successfully updates the index.
indexForClientA.Fields.Add(new SearchField("a", SearchFieldDataType.Int32));
indexForClientA = adminClient.CreateOrUpdateIndex(indexForClientA);
Console.WriteLine($"Client A updates test-idx by adding a new field. The new ETag for test-idx is: '{indexForClientA.ETag}'");
// indexForClientB tries to update the index, but fails due to the ETag check.
try
{
indexForClientB.Fields.Add(new SearchField("b", SearchFieldDataType.Boolean));
adminClient.CreateOrUpdateIndex(indexForClientB);
Console.WriteLine("Whoops; This shouldn't happen");
Environment.Exit(1);
}
catch (RequestFailedException e) when (e.Status == 400)
{
Console.WriteLine("Client B failed to update the index, as expected.");
}
// Uncomment the next line to remove test-idx
//adminClient.DeleteIndex("test-idx");
Console.WriteLine("Complete. Press any key to end application...\n");
Console.ReadKey();
}
private static void DeleteTestIndexIfExists(SearchIndexClient adminClient)
{
try
{
if (adminClient.GetIndex("test-idx") != null)
{
adminClient.DeleteIndex("test-idx");
}
}
catch (RequestFailedException e) when (e.Status == 404)
{
//if an exception occurred and status is "Not Found", this is working as expected
Console.WriteLine("Failed to find index and this is because it's not there.");
}
}
private static SearchIndex DefineTestIndex() =>
new SearchIndex("test-idx", new[] { new SearchField("id", SearchFieldDataType.String) { IsKey = true } });
}
}
Modèle de conception
Un modèle de conception pour l’implémentation de l’accès concurrentiel optimiste doit inclure une boucle qui retente la vérification de la condition d’accès et un test pour la condition d’accès, et récupère éventuellement une ressource mise à jour avant de tenter de réappliquer les modifications.
Cet extrait de code illustre l’ajout d’une ressource synonymMap à un index existant.
L’extrait de code obtient l’index « hotel », vérifie la version de l’objet pour une opération de mise à jour, lève une exception si la condition échoue, puis retente l’opération (jusqu’à trois fois), en commençant par extraire l’index du serveur pour obtenir sa dernière 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 (Exception 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;
}