Migrera från Orleans 3.x till 7.0

Orleans 7.0 introducerar flera fördelaktiga ändringar, inklusive förbättringar av värdtjänster, anpassad serialisering, oföränderlighet och kornabstraktioner.

Migrering

Befintliga program som använder påminnelser, strömmar eller kornbeständighet kan inte enkelt migreras till Orleans 7.0 på grund av ändringar i hur Orleans identifierar korn och strömmar. Vi planerar att stegvis erbjuda en migreringssökväg för dessa program.

Program som kör tidigare versioner av Orleans kan inte uppgraderas smidigt via en löpande uppgradering till Orleans 7.0. Därför måste en annan uppgraderingsstrategi användas, till exempel att distribuera ett nytt kluster och inaktivera det tidigare klustret. Orleans 7.0 ändrar trådprotokollet på ett inkompatibelt sätt, vilket innebär att kluster inte kan innehålla en blandning av Orleans 7.0-värdar och värdar som kör tidigare versioner av Orleans.

Vi har undvikit sådana icke-bakåtkompatibla förändringar i många år, även i större versioner, så varför nu? Det finns två huvudsakliga orsaker: identiteter och serialisering. När det gäller identiteter består korn- och strömidentiteter nu av strängar, vilket gör det möjligt för korn att koda allmän typinformation korrekt och göra det lättare för strömmar att mappas till programdomänen. Korntyper identifierades tidigare med hjälp av en komplex datastruktur som inte kunde representera generiska korn, vilket ledde till hörnfall. Flöden identifierades av ett string namnområde och en Guid nyckel, vilket var svårt för utvecklare att mappa till sin programdomän, hur effektivt det än var. Serialiseringen är nu versionstolerant, vilket innebär att du kan ändra dina typer på vissa kompatibla sätt, följa en uppsättning regler och vara säker på att du kan uppgradera programmet utan serialiseringsfel. Detta var särskilt problematiskt när programtyperna bevarades i strömmar eller kornlagring. Följande avsnitt beskriver de viktigaste ändringarna och diskuterar dem mer detaljerat.

Paketeringsändringar

Om du uppgraderar ett projekt till Orleans 7.0 måste du utföra följande åtgärder:

Dricks

Orleans Alla exempel har uppgraderats till Orleans 7.0 och kan användas som referens för vilka ändringar som har gjorts. Mer information finns i Orleans problem #8035 som specificerar de ändringar som görs i varje exempel.

Orleansglobal using Direktiv

Alla Orleans projekt refererar antingen direkt eller indirekt till NuGet-paketet Microsoft.Orleans.Sdk . När ett Orleans projekt har konfigurerats för att aktivera implicit användning (till exempel <ImplicitUsings>enable</ImplicitUsings>) Orleans används båda namnrymderna och Orleans.Hosting implicit. Det innebär att appkoden inte behöver dessa direktiv.

Mer information finns i Implicitaanvändningar och dotnet/orleans/src/Orleans. Sdk/build/Microsoft.Orleans. Sdk.targets.

Värd

Typen ClientBuilder har ersatts med en UseOrleansClient tilläggsmetod på IHostBuilder. Typen IHostBuilder kommer från NuGet-paketet Microsoft.Extensions.Hosting . Det innebär att du kan lägga till en Orleans klient i en befintlig värd utan att behöva skapa en separat container för beroendeinmatning. Klienten ansluter till klustret under starten. När IHost.StartAsync klienten har slutförts ansluts den automatiskt. Tjänster som läggs till IHostBuilder i startas i registreringsordningen, så anropa UseOrleansClient innan anropet ConfigureWebHostDefaults kommer att säkerställa Orleans att startas innan ASP.NET Core startar, till exempel så att du kan komma åt klienten från ditt ASP.NET Core-program omedelbart.

Om du vill emulera det tidigare ClientBuilder beteendet kan du skapa en separat HostBuilder och konfigurera den med en Orleans klient. IHostBuilder kan ha antingen en Orleans klient eller en Orleans silo konfigurerad. Alla silor registrerar en instans av IGrainFactory och IClusterClient som programmet kan använda, så att konfigurera en klient separat är onödigt och stöds inte.

OnActivateAsync och OnDeactivateAsync signaturändring

Orleans låter korn köra kod under aktivering och inaktivering. Detta kan användas för att utföra uppgifter som att läsa tillstånd från lagrings- eller logglivscykelmeddelanden. I Orleans 7.0 ändrades signaturen för dessa livscykelmetoder:

  • OnActivateAsync() accepterar nu en CancellationToken parameter. När avbryts CancellationToken bör aktiveringsprocessen avbrytas.
  • OnDeactivateAsync() accepterar nu en DeactivationReason parameter och en CancellationToken parameter. DeactivationReason Anger varför aktiveringen inaktiveras. Utvecklare förväntas använda den här informationen i loggnings- och diagnostiksyfte. När avbryts CancellationToken bör inaktiveringsprocessen slutföras omgående. Observera att eftersom alla värdar kan misslyckas när som helst rekommenderar vi inte att du förlitar dig på OnDeactivateAsync att utföra viktiga åtgärder som att bevara kritiskt tillstånd.

Tänk på följande exempel på ett korn som åsidosätter dessa nya metoder:

public sealed class PingGrain : Grain, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(ILogger<PingGrain> logger) =>
        _logger = logger;

    public override Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

POCO-korn och IGrainBase

Korn i Orleans behöver inte längre ärva från basklassen Grain eller någon annan klass. Den här funktionen kallas POCO-korn . Så här kommer du åt tilläggsmetoder som något av följande:

Ditt korn måste antingen implementera IGrainBase eller ärva från Grain. Här är ett exempel på implementering IGrainBase på en kornklass:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    public PingGrain(IGrainContext context) => GrainContext = context;

    public IGrainContext GrainContext { get; }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

IGrainBase definierar OnActivateAsync och OnDeactivateAsync med standardimplementeringar, vilket gör att ditt korn kan delta i livscykeln om så önskas:

public sealed class PingGrain : IGrainBase, IPingGrain
{
    private readonly ILogger<PingGrain> _logger;

    public PingGrain(IGrainContext context, ILogger<PingGrain> logger)
    {
        _logger = logger;
        GrainContext = context;
    }

    public IGrainContext GrainContext { get; }

    public Task OnActivateAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("OnActivateAsync()");
        return Task.CompletedTask;
    }

    public Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token)
    {
        _logger.LogInformation("OnDeactivateAsync({Reason})", reason);
        return Task.CompletedTask;
    }

    public ValueTask Ping() => ValueTask.CompletedTask;
}

Serialization

Den mest betungande förändringen i Orleans 7.0 är introduktionen av den versionstoleranta serialiseraren. Den här ändringen gjordes eftersom program tenderar att utvecklas och detta ledde till en betydande fallgrop för utvecklare, eftersom den tidigare serialiseraren inte kunde tolerera att egenskaper lades till i befintliga typer. Å andra sidan var serialiseraren flexibel, vilket gjorde det möjligt för utvecklare att representera de flesta .NET-typer utan ändringar, inklusive funktioner som generiska läkemedel, polymorfism och referensspårning. En ersättning borde ha ersatts för länge sedan, men användarna behöver fortfarande en återgivningsrepresentation av sina typer. Därför introducerades en ersättnings serialiserare i Orleans 7.0 som stöder återgivning av .NET-typer samtidigt som typer kan utvecklas. Den nya serialiseraren är mycket effektivare än den tidigare serialiseraren, vilket resulterar i upp till 170 % högre dataflöde från slutpunkt till slutpunkt.

Mer information finns i följande artiklar om Orleans 7.0:

Korniga identiteter

Korn har var och en en unik identitet som består av korntypen och dess nyckel. Tidigare versioner av Orleans använde en sammansatt typ för s för GrainIdatt stödja kornnycklar för något av följande:

Detta innebär viss komplexitet när det gäller att hantera kornnycklar. Korniga identiteter består av två komponenter: en typ och en nyckel. Typkomponenten bestod tidigare av en numerisk typkod, en kategori och 3 byte allmän typinformation.

Korniga identiteter har nu formatet type/key där både type och key är strängar. Det vanligaste kornnyckelgränssnittet är IGrainWithStringKey. Detta förenklar avsevärt hur kornidentitet fungerar och förbättrar stödet för generiska korntyper.

Korngränssnitt representeras nu också med ett läsbart namn för människor, i stället för en kombination av en hashkod och en strängrepresentation av alla generiska typparametrar.

Det nya systemet är mer anpassningsbart och dessa anpassningar kan styras av attribut.

  • GrainTypeAttribute(String) på ett korn class anger typdelen av dess korn-ID.
  • DefaultGrainTypeAttribute(String) på ett korn interface anger typen av korn som ska matchas som IGrainFactory standard när en kornreferens hämtas. När du till exempel anropar IGrainFactory.GetGrain<IMyGrain>("my-key")returnerar kornfabriken en referens till kornigheten "my-type/my-key" om IMyGrain det ovan nämnda attributet har angetts.
  • GrainInterfaceTypeAttribute(String) tillåter åsidosättande av gränssnittsnamnet. Om du anger ett namn explicit med den här mekanismen kan du byta namn på gränssnittstypen utan att bryta kompatibiliteten med befintliga kornreferenser. Observera att gränssnittet också bör ha AliasAttribute i det här fallet, eftersom dess identitet kan serialiseras. Mer information om hur du anger ett typalias finns i avsnittet om serialisering.

Som nämnts ovan kan du genom att åsidosätta standardnamnen för kornklass och gränssnitt för dina typer byta namn på de underliggande typerna utan att bryta kompatibiliteten med befintliga distributioner.

Strömma identiteter

När Orleans strömmar först släpptes kunde strömmar bara identifieras med hjälp av en Guid. Detta var effektivt när det gäller minnesallokering, men det var svårt för användare att skapa meningsfulla strömidentiteter, vilket ofta krävde viss kodning eller indirektion för att fastställa lämplig strömidentitet för ett visst syfte.

I Orleans 7.0 identifieras strömmar nu med hjälp av strängar. Innehåller Orleans.Runtime.StreamIdstruct tre egenskaper: en StreamId.Namespace, en StreamId.Keyoch en StreamId.FullKey. Dessa egenskapsvärden är kodade UTF-8-strängar. Exempel: StreamId.Create(String, String)

Ersättning av SimpleMessage Flöden med BroadcastChannel

SimpleMessageStreams (kallas även SMS) togs bort i 7.0. SMS hade samma gränssnitt som Orleans.Providers.Streams.PersistentStreams, men dess beteende var mycket annorlunda, eftersom det förlitade sig på direkta korn-till-korn-anrop. För att undvika förvirring togs SMS bort och en ny ersättning med namnet Orleans.BroadcastChannel introducerades.

BroadcastChannel stöder endast implicita prenumerationer och kan vara en direkt ersättning i det här fallet. Om du behöver explicita prenumerationer eller behöver använda PersistentStream gränssnittet (till exempel om du använde SMS i tester när du använde EventHub i produktion) är det MemoryStream den bästa kandidaten för dig.

BroadcastChannel kommer att ha samma beteenden som SMS, medan MemoryStream kommer att bete sig som andra strömprovidrar. Tänk på följande exempel på sändningskanalanvändning:

// Configuration
builder.AddBroadcastChannel(
    "my-provider",
    options => options.FireAndForgetDelivery = false);

// Publishing
var grainKey = Guid.NewGuid().ToString("N");
var channelId = ChannelId.Create("some-namespace", grainKey);
var stream = provider.GetChannelWriter<int>(channelId);

await stream.Publish(1);
await stream.Publish(2);
await stream.Publish(3);

// Simple implicit subscriber example
[ImplicitChannelSubscription]
public sealed class SimpleSubscriberGrain : Grain, ISubscriberGrain, IOnBroadcastChannelSubscribed
{
    // Called when a subscription is added to the grain
    public Task OnSubscribed(IBroadcastChannelSubscription streamSubscription)
    {
        streamSubscription.Attach<int>(
          item => OnPublished(streamSubscription.ChannelId, item),
          ex => OnError(streamSubscription.ChannelId, ex));

        return Task.CompletedTask;

        // Called when an item is published to the channel
        static Task OnPublished(ChannelId id, int item)
        {
            // Do something
            return Task.CompletedTask;
        }

        // Called when an error occurs
        static Task OnError(ChannelId id, Exception ex)
        {
            // Do something
            return Task.CompletedTask;
        }
    }
}

Migreringen till MemoryStream blir enklare eftersom endast konfigurationen behöver ändras. Överväg följande MemoryStream konfiguration:

builder.AddMemoryStreams<DefaultMemoryMessageBodySerializer>(
    "in-mem-provider",
    _ =>
    {
        // Number of pulling agent to start.
        // DO NOT CHANGE this value once deployed, if you do rolling deployment
        _.ConfigurePartitioning(partitionCount: 8);
    });

OpenTelemetry

Telemetrisystemet har uppdaterats i Orleans 7.0 och det tidigare systemet har tagits bort till förmån för standardiserade .NET-API:er som .NET Metrics för mått och ActivitySource för spårning.

Som en del av detta har de befintliga Microsoft.Orleans.TelemetryConsumers.* paketen tagits bort. Vi överväger att införa en ny uppsättning paket för att effektivisera processen med att integrera måtten som genereras av Orleans i valfri övervakningslösning. Som alltid är feedback och bidrag välkomna.

Verktyget dotnet-counters har prestandaövervakning för ad hoc-hälsoövervakning och prestandaundersökning på första nivån. För Orleans räknare kan verktyget dotnet-counters användas för att övervaka dem:

dotnet counters monitor -n MyApp --counters Microsoft.Orleans

På samma sätt kan OpenTelemetry-mått lägga till mätarna Microsoft.Orleans , enligt följande kod:

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddPrometheusExporter()
        .AddMeter("Microsoft.Orleans"));

Om du vill aktivera distribuerad spårning konfigurerar du OpenTelemetry enligt följande kod:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(serviceName: "ExampleService", serviceVersion: "1.0"));

        tracing.AddAspNetCoreInstrumentation();
        tracing.AddSource("Microsoft.Orleans.Runtime");
        tracing.AddSource("Microsoft.Orleans.Application");

        tracing.AddZipkinExporter(options =>
        {
            options.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
        });
    });

I föregående kod konfigureras OpenTelemetry för övervakning:

  • Microsoft.Orleans.Runtime
  • Microsoft.Orleans.Application

Om du vill sprida aktivitet anropar du AddActivityPropagation:

builder.Host.UseOrleans((_, clientBuilder) =>
{
    clientBuilder.AddActivityPropagation();
});

Omstrukturera funktioner från kärnpaketet till separata paket

I Orleans 7.0 har vi gjort ett försök att dela in tillägg i separata paket som inte är beroende Orleans.Coreav . Orleans.StreamingNämligen , Orleans.Reminders, och Orleans.Transactions har separerats från kärnan. Det innebär att dessa paket helt betalar för det du använder och ingen kod i kärnan av Orleans är dedikerad till dessa funktioner. Detta bantar ner api-kärnytan och sammansättningsstorleken, förenklar kärnan och förbättrar prestandan. När det gäller prestanda krävde transaktioner i Orleans tidigare viss kod som kördes för varje metod för att samordna potentiella transaktioner. Det har sedan dess flyttats till per metod.

Det här är en kompileringsbrytande ändring. Du kan ha befintlig kod som interagerar med påminnelser eller strömmar genom att anropa till metoder som tidigare har definierats i basklassen Grain men som nu är tilläggsmetoder. Sådana anrop som inte anger this (till exempel GetReminders) måste uppdateras för att inkludera this (till exempel this.GetReminders()) eftersom tilläggsmetoder måste vara kvalificerade. Det uppstår ett kompileringsfel om du inte uppdaterar dessa anrop och den nödvändiga kodändringen kanske inte är uppenbar om du inte vet vad som har ändrats.

Transaktionsklient

Orleans 7.0 introducerar en ny abstraktion för samordning av transaktioner, Orleans.ITransactionClient. Tidigare kunde transaktioner endast samordnas av korn. Med ITransactionClient, som är tillgängligt via beroendeinmatning, kan klienter också samordna transaktioner utan att behöva ett mellanliggande korn. I följande exempel tas krediter ut från ett konto och de sätts in i ett annat inom en enda transaktion. Den här koden kan anropas från ett korn eller från en extern klient som har hämtat ITransactionClient från containern för beroendeinmatning.

await transactionClient.RunTransaction(
  TransactionOption.Create,
  () => Task.WhenAll(from.Withdraw(100), to.Deposit(100)));

För transaktioner som samordnas av klienten måste klienten lägga till de nödvändiga tjänsterna under konfigurationstiden:

clientBuilder.UseTransactions();

BankAccount-exemplet visar användningen av ITransactionClient. Mer information finns i Orleans transaktioner.

Återaktivering av samtalskedja

Korn är entrådade och bearbetar begäranden en i taget från början till slut som standard. Med andra ord är korn inte reentrant som standard. ReentrantAttribute Genom att lägga till i en kornklass kan flera begäranden bearbetas samtidigt, på ett interfolierande sätt, samtidigt som de fortfarande är enkeltrådade. Detta kan vara användbart för korn som inte har något internt tillstånd eller utför många asynkrona åtgärder, till exempel att utfärda HTTP-anrop eller skriva till en databas. Extra försiktighet måste iakttas när begäranden kan interleave: det är möjligt att tillståndet för ett korn observeras innan en await instruktion har ändrats när den asynkrona åtgärden slutförs och metoden återupptar körningen.

Följande korn representerar till exempel en räknare. Det har markerats Reentrant, vilket gör att flera anrop kan interleave. Metoden Increment() bör öka den interna räknaren och returnera det observerade värdet. Men eftersom metodtexten Increment() observerar kornets tillstånd före en await punkt och uppdaterar den efteråt, är det möjligt att flera interleaving-körningar av Increment() kan resultera i ett _value mindre än det totala antalet Increment() mottagna anrop. Det här är ett fel som uppstår vid felaktig användning av återaktivering.

ReentrantAttribute Det räcker med att ta bort problemet.

[Reentrant]
public sealed class CounterGrain : Grain, ICounterGrain
{
    int _value;
    
    /// <summary>
    /// Increments the grain's value and returns the previous value.
    /// </summary>
    public Task<int> Increment()
    {
        // Do not copy this code, it contains an error.
        var currentVal = _value;
        await Task.Delay(TimeSpan.FromMilliseconds(1_000));
        _value = currentVal + 1;
        return currentValue;
    }
}

För att förhindra sådana fel är korn som standard icke-reentrant. Nackdelen med detta är minskat dataflöde för korn som utför asynkrona åtgärder i implementeringen, eftersom andra begäranden inte kan bearbetas medan kornet väntar på att en asynkron åtgärd ska slutföras. För att lindra detta Orleans erbjuder flera alternativ för att tillåta återaktivering i vissa fall:

  • För en hel klass: om du placerar ReentrantAttribute på kornet kan alla begäranden till kornet interleave med andra begäranden.
  • För en delmängd av metoder: genom att placera AlwaysInterleaveAttribute metoden på korngränssnittet kan begäranden till den metoden interfolieras med andra begäranden och begäranden till den metoden interfolieras av andra begäranden.
  • För en delmängd av metoder: genom att placera ReadOnlyAttribute metoden på korngränssnittet kan begäranden till den metoden interfolieras med andra ReadOnly begäranden och begäranden till den metoden interfolieras av andra ReadOnly begäranden. I den meningen är det en mer begränsad form av AlwaysInterleave.
  • För alla förfrågningar i en anropskedja: RequestContext.AllowCallChainReentrancy() och <xref:Orleans. Runtime.RequestContext.SuppressCallChainReentrancy?displayProperty=nameWithType tillåter att du väljer in och ut från att tillåta underordnade begäranden att gå tillbaka till kornet. Anropen returnerar båda ett värde som måste tas bort när begäran avslutas. Därför är rätt användning enligt följande:
public Task<int> OuterCall(IMyGrain other)
{
    // Allow call-chain reentrancy for this grain, for the duration of the method.
    using var _ = RequestContext.AllowCallChainReentrancy();
    await other.CallMeBack(this.AsReference<IMyGrain>());
}

public Task CallMeBack(IMyGrain grain)
{
    // Because OuterCall allowed reentrancy back into that grain, this method 
    // will be able to call grain.InnerCall() without deadlocking.
    await grain.InnerCall();
}

public Task InnerCall() => Task.CompletedTask;

Återaktivering av samtalskedja måste väljas per korn, per samtalskedja. Överväg till exempel två korn, korn A & korn B. Om korn A aktiverar återaktivering av anropskedja innan du anropar korn B, kan korn B anropa tillbaka till korn A i det anropet. Korn A kan dock inte anropa tillbaka till korn B om korn B inte också har aktiverat återaktivering av anropskedjan. Det är per korn, per anropskedja.

Korn kan också förhindra återaktiveringsinformation för anropskedjan från att flöda ned i en anropskedja med hjälp av using var _ = RequestContext.SuppressCallChainReentrancy(). Detta förhindrar att efterföljande anrop försöker igen.

ADO.NET migreringsskript

För att säkerställa vidarekompatibilitet med Orleans klustring, beständighet och påminnelser som är beroende av ADO.NET behöver du rätt SQL-migreringsskript:

Välj filerna för databasen som används och tillämpa dem i ordning.