Zarządzanie współbieżnością w usłudze Azure AI Search

Podczas zarządzania zasobami usługi Azure AI Search, takimi jak indeksy i źródła danych, ważne jest, aby bezpiecznie aktualizować zasoby, zwłaszcza jeśli zasoby są dostępne współbieżnie przez różne składniki aplikacji. Gdy dwaj klienci jednocześnie aktualizują zasób bez koordynacji, możliwe są warunki wyścigu. Aby temu zapobiec, usługa Azure AI Search używa optymistycznego modelu współbieżności. Zasób nie ma blokad. Zamiast tego istnieje element ETag dla każdego zasobu, który identyfikuje wersję zasobu, dzięki czemu można sformułować żądania, które unikają przypadkowego zastąpienia.

Jak to działa

Optymistyczna współbieżność jest implementowana za pomocą kontroli warunków dostępu w wywołaniach interfejsu API zapisu do indeksów, indeksatorów, źródeł danych, zestawów umiejętności i zasobów synonimówMap.

Wszystkie zasoby mają tag jednostki (ETag), który udostępnia informacje o wersji obiektu. Najpierw sprawdzając element ETag, można uniknąć współbieżnych aktualizacji w typowym przepływie pracy (pobierz, zmodyfikuj lokalnie, aktualizuj), zapewniając, że element ETag zasobu jest zgodny z kopią lokalną.

  • Interfejs API REST używa elementu ETag w nagłówku żądania.

  • Zestaw Azure SDK dla platformy .NET ustawia element ETag za pośrednictwem obiektu accessCondition, ustawiając wartość If-Match | Nagłówek If-Match-None w zasobie. Obiekty używające elementów ETag, takich jak SynonymMap.ETag i SearchIndex.ETag, mają obiekt accessCondition.

Za każdym razem, gdy aktualizujesz zasób, jego element ETag zmienia się automatycznie. Podczas implementowania zarządzania współbieżnością wszystko, co robisz, to wprowadzenie warunku wstępnego dla żądania aktualizacji, które wymaga, aby zasób zdalny miał ten sam element ETag co kopia zasobu zmodyfikowanego na kliencie. Jeśli inny proces zmieni zasób zdalny, element ETag nie jest zgodny z warunkiem wstępnym, a żądanie zakończy się niepowodzeniem z protokołem HTTP 412. Jeśli używasz zestawu .NET SDK, ten błąd manifestuje się jako wyjątek, w którym IsAccessConditionFailed() metoda rozszerzenia zwraca wartość true.

Uwaga

Istnieje tylko jeden mechanizm współbieżności. Jest ona zawsze używana niezależnie od tego, który interfejs API lub zestaw SDK jest używany do aktualizacji zasobów.

Przykład

Poniższy kod demonstruje optymistyczną współbieżność dla operacji aktualizacji. Druga aktualizacja kończy się niepowodzeniem, ponieważ element ETag obiektu został zmieniony przez poprzednią aktualizację. Mówiąc dokładniej, gdy element ETag w nagłówku żądania nie pasuje już do elementu ETag obiektu, usługa wyszukiwania zwraca kod stanu 400 (nieprawidłowe żądanie), a aktualizacja kończy się niepowodzeniem.

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 } });
    }
}

Wzorzec projektowania

Wzorzec projektu na potrzeby implementowania optymistycznej współbieżności powinien zawierać pętlę, która ponawia próbę sprawdzenia warunku dostępu, test warunku dostępu i opcjonalnie pobiera zaktualizowany zasób przed podjęciem próby ponownego zastosowania zmian.

Ten fragment kodu ilustruje dodanie synonimuMap do indeksu, który już istnieje.

Fragment kodu pobiera indeks "hotels", sprawdza wersję obiektu operacji aktualizacji, zgłasza wyjątek w przypadku niepowodzenia warunku, a następnie ponawia próbę operacji (do trzech razy), począwszy od pobierania indeksu z serwera w celu uzyskania najnowszej wersji.

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;
}

Zobacz też