Gestire la concorrenza in Azure AI Search
Quando si gestiscono le risorse di Azure AI Search, ad esempio indici e origini dati, è importante aggiornare le risorse in modo sicuro, soprattutto se componenti diversi dell'applicazione accedono simultaneamente alle risorse. Quando due client aggiornano simultaneamente un risorsa senza coordinazione, si possono verificare race condition. Per evitare questo problema, Azure AI Search usa un modello di concorrenza ottimistica. Non sono presenti blocchi su una risorsa, Per ogni risorsa esiste, invece, un ETag che identifica la versione della risorsa in modo che sia possibile formulare richieste che evitano sovrascritture accidentali.
Funzionamento
La concorrenza ottimistica viene implementata tramite i controlli delle condizioni di accesso nelle chiamate API che scrivono in indici, indicizzatori, origini dati set di competenze e risorse synonymMap.
Tutte le risorse hanno un tag di entità (ETag) che fornisce informazioni sulla versione dell'oggetto. Controllando prima l'ETag, è possibile evitare aggiornamenti simultanei in un flusso di lavoro tipico (acquisizione, modifica locale, aggiornamento) assicurandosi che l'ETag della risorsa corrisponda alla copia locale.
L'API REST usa un ETag nell'intestazione della richiesta.
L'SDK di Azure per .NET imposta l'ETag tramite un oggetto accessCondition, impostando l'intestazione If-Match | If-Match-None per la risorsa. Gli oggetti che usano ETag, ad esempio SynonymMap.ETag e SearchIndex.ETag, hanno un oggetto accessCondition.
Ogni volta che si aggiorna una risorsa, l'ETag cambia automaticamente. Quando si implementa la gestione della concorrenza, si inserisce semplicemente una precondizione nella richiesta di aggiornamento, che obbliga la risorsa remota ad avere lo stesso ETag della copia della risorsa modificata nel client. Se un altro processo modifica la risorsa remota, l'ETag non corrisponde alla precondizione e la richiesta ha esito negativo con HTTP 412. Se si usa .NET SDK, l'errore viene generato come eccezione in cui il metodo di estensione IsAccessConditionFailed()
restituisce true.
Nota
Esiste un solo meccanismo per la concorrenza, Viene sempre usato indipendentemente dall'API o dall'SDK usati per gli aggiornamenti delle risorse.
Esempio
Il codice seguente illustra la concorrenza ottimistica per un'operazione di aggiornamento. Fa sì che il secondo aggiornamento non vada a buon fine perché l'ETag dell'oggetto viene modificato da un aggiornamento precedente. In particolare, quando l'ETag nell'intestazione della richiesta non corrisponde più all'ETag dell'oggetto, il servizio di ricerca restituisce un codice di stato 400 (richiesta non valida) e l'aggiornamento ha esito negativo.
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 } });
}
}
Schema progettuale
Uno schema progettuale per l'implementazione della concorrenza ottimistica deve includere un ciclo che recupera il controllo della condizione di accesso, un test per la condizione di accesso e, facoltativamente, una risorsa aggiornata prima di provare ad applicare di nuovo le modifiche.
Questo frammento di codice illustra l'aggiunta di synonymMap a un indice già esistente.
Il frammento ottiene l'indice "hotels", controlla la versione dell'oggetto in un'operazione di aggiornamento, genera un'eccezione se la condizione ha esito negativo e quindi esegue un nuovo tentativo di operazione (fino a tre volte), iniziando con il recupero dell'indice dal server per ottenere la versione più recente.
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;
}
Vedi anche
- ETag Struct
- Common HTTP request and response headers (Intestazioni della risposta e della richiesta HTTP comuni)
- Codici di stato HTTP