Hantera samtidighet i Azure AI Search

När du hanterar Azure AI Search-resurser som index och datakällor är det viktigt att uppdatera resurserna på ett säkert sätt, särskilt om resurser används samtidigt av olika komponenter i ditt program. När två klienter samtidigt uppdaterar en resurs utan samordning är konkurrensvillkor möjliga. För att förhindra detta använder Azure AI Search en optimistisk samtidighetsmodell. Det finns inga lås på en resurs. I stället finns det en ETag för varje resurs som identifierar resursversionen så att du kan formulera begäranden som undviker oavsiktliga överskrivningar.

Hur det fungerar

Optimistisk samtidighet implementeras via kontroller av åtkomstvillkor i API-anrop som skriver till index, indexerare, datakällor, kompetensuppsättningar och synonymMappresurser.

Alla resurser har en entitetstagg (ETag) som innehåller information om objektversion. Genom att kontrollera ETag först kan du undvika samtidiga uppdateringar i ett typiskt arbetsflöde (hämta, ändra lokalt, uppdatera) genom att se till att resursens ETag matchar din lokala kopia.

Varje gång du uppdaterar en resurs ändras dess ETag automatiskt. När du implementerar samtidighetshantering gör du bara en förhandsvillkor för uppdateringsbegäran som kräver att fjärrresursen har samma ETag som kopian av resursen som du ändrade på klienten. Om en annan process ändrar fjärrresursen matchar ETag inte förhandsvillkoret och begäran misslyckas med HTTP 412. Om du använder .NET SDK visas det här felet som ett undantag där IsAccessConditionFailed() tilläggsmetoden returnerar true.

Kommentar

Det finns bara en mekanism för samtidighet. Det används alltid oavsett vilket API eller SDK som används för resursuppdateringar.

Exempel

Följande kod visar optimistisk samtidighet för en uppdateringsåtgärd. Den andra uppdateringen misslyckas eftersom objektets ETag ändras av en tidigare uppdatering. Mer specifikt, när ETag i begärandehuvudet inte längre matchar objektets ETag returnerar söktjänsten statuskoden 400 (felaktig begäran) och uppdateringen misslyckas.

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

Designmönster

Ett designmönster för att implementera optimistisk samtidighet bör innehålla en loop som försöker utföra åtkomstvillkorskontrollen på nytt, ett test för åtkomstvillkoret och eventuellt hämtar en uppdaterad resurs innan du försöker tillämpa ändringarna igen.

Det här kodfragmentet illustrerar tillägget av en synonymMapp till ett index som redan finns.

Kodfragmentet hämtar indexet "hotels", kontrollerar objektversionen vid en uppdateringsåtgärd, utlöser ett undantag om villkoret misslyckas och försöker sedan utföra åtgärden igen (upp till tre gånger), från och med indexhämtning från servern för att hämta den senaste versionen.

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

Se även