Skip to content

Commit

Permalink
Simplify silo/services configuration and streamstone config
Browse files Browse the repository at this point in the history
  • Loading branch information
kzu committed Jun 13, 2024
1 parent 40bb8f2 commit f52c7c8
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 136 deletions.
22 changes: 11 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,11 @@ var builder = WebApplication.CreateSlimBuilder(args);
builder.Host.UseOrleans(silo =>
{
silo.UseLocalhostClustering();
silo.AddMemoryGrainStorageAsDefault();
silo.AddCloudActors(); // 👈 registers generated grains
// 👇 registers generated grains, actor bus and activation features
silo.AddCloudActors();
});
```

Finally, you need to hook up the `IActorBus` service and related functionality with:

```csharp
builder.Services.AddCloudActors(); // 👈 registers bus and activation features
```

## How it works

The library uses source generators to generate the grain classes. It's easy to inspect the
Expand Down Expand Up @@ -224,7 +218,7 @@ public partial class AccountGrain : Grain, IActorGrain
}
catch
{
await storage.ReadStateAsync(); // 👈 rollback state on failure
await storage.ReadStateAsync();
throw;
}
break;
Expand All @@ -236,7 +230,7 @@ public partial class AccountGrain : Grain, IActorGrain
}
catch
{
await storage.ReadStateAsync(); // 👈 rollback state on failure
await storage.ReadStateAsync();
throw;
}
break;
Expand All @@ -248,7 +242,7 @@ public partial class AccountGrain : Grain, IActorGrain
}
catch
{
await storage.ReadStateAsync(); // 👈 rollback state on failure
await storage.ReadStateAsync();
throw;
}
break;
Expand Down Expand Up @@ -284,6 +278,12 @@ namespace Orleans.Runtime
options.Classes.Add(typeof(Tests.AccountGrain));
});

builder.ConfigureServices(services =>
{
// 👇 registers IActorBus and actor activation features
services.AddCloudActors();
});

return builder;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/CloudActors.CodeAnaysis/ActorMessageAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public class ActorMessageAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(SingleInterfaceRequired, MustBeSerializable);

#if DEBUG
#pragma warning disable RS1026 // Enable concurrent execution: we only turn this on in release builds
#endif
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
Expand Down
12 changes: 2 additions & 10 deletions src/CloudActors.CodeAnaysis/CloudActors.CodeAnaysis.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Devlooped.CloudActors.CodeAnalysis</AssemblyName>
Expand All @@ -13,11 +13,7 @@
</PropertyGroup>

<ItemGroup>
<None Remove="ActorSnapshot.sbntxt" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NuGetizer" Version="1.2.0" />
<PackageReference Include="NuGetizer" Version="1.0.5" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Pack="false" Version="4.5.0" />
<PackageReference Include="PolySharp" PrivateAssets="All" Version="1.13.2" />

Expand All @@ -30,10 +26,6 @@

<ItemGroup>
<EmbeddedResource Include="@(None -&gt; WithMetadataValue('Extension', '.sbntxt'))" Kind="text" />
<EmbeddedResource Include="ActorSnapshot.sbntxt">
<Generator></Generator>
<Kind>text</Kind>
</EmbeddedResource>
</ItemGroup>

</Project>
9 changes: 9 additions & 0 deletions src/CloudActors.CodeAnaysis/SiloBuilder.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ namespace Orleans.Runtime
[CompilerGenerated]
public static class CloudActorsExtensions
{
/// <summary>
/// Adds Cloud Actors support to the silo, as well as all discovered
/// actor types in the current assembly and its dependencies.
/// </summary>
public static ISiloBuilder AddCloudActors(this ISiloBuilder builder)
{
builder.Configure<GrainTypeOptions>(options =>
Expand All @@ -30,6 +34,11 @@ namespace Orleans.Runtime
{{~ end ~}}
});

builder.ConfigureServices(services =>
{
services.AddCloudActors();
});

return builder;
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/CloudActors.Orleans/ActorActivatorFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using Orleans;
using Orleans.Core;
using Orleans.Runtime;

interface IActorStateFactory : IPersistentStateFactory { }

class ActorStateFactory(IPersistentStateFactory factory) : IActorStateFactory
{
public IPersistentState<TState> Create<TState>(IGrainContext context, IPersistentStateConfiguration config)
{
var state = factory.Create<TState>(context, config);
// We're super conservative here, only replacing if all conditions are met.
// We check for a state with no parameterless constructor up-front, then go from there.
if (typeof(TState).GetConstructor(Type.EmptyTypes) is null &&
// We only know how the state bridge works
state is StateStorageBridge<TState> bridge &&
// We only support a ctor that receives the grain id as a string
context.GrainId.Key.ToString() is object id &&
typeof(TState).GetConstructor(new[] { typeof(string) }) is not null &&
// Even so, we might fail activation
Activator.CreateInstance(typeof(TState), id) is TState actor)
{
// In this case, we can force custom creation of the state object.
// Internally, this causes the StateStorageBridge<T>._grainState to not be
// forcedly constructed via Activator.CreateInstance with a parameterless constructor,
// and instead our "rehydrated" type is used instead.
// NOTE: this causes the OnStart on the PersistentState<TState> class to skip invoking
// ReadStateAsync, as it assumes rehydration makes that unnecessary.
// However, the JournaledGrain base class overrides the OnActivateAsync method and
// forces a sync, so we're good since our generated grain does that too.
bridge.OnRehydrate(new ActivationContext(new GrainState<TState>(actor)));
}

return state;
}

class ActivationContext(object actor) : IRehydrationContext
{
public IEnumerable<string> Keys => throw new NotImplementedException();
public bool TryGetBytes(string key, out ReadOnlySequence<byte> value) => throw new NotImplementedException();
public bool TryGetValue<T>(string key, out T? value)
{
if (actor is T typed)
{
value = typed;
return true;
}

value = default;
return false;
}
}
}
109 changes: 33 additions & 76 deletions src/CloudActors.Orleans/CloudActorsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,96 +1,53 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Devlooped.CloudActors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Orleans;
using Orleans.Core;
using Orleans.Hosting;
using Orleans.Runtime;

[EditorBrowsable(EditorBrowsableState.Never)]
public static class CloudActorsExtensions
namespace Devlooped.CloudActors
{
/// <summary>
/// Adds the <see cref="IActorBus"/> service and custom actor activation logic.
/// </summary>
public static IServiceCollection AddCloudActors(this IServiceCollection services)
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class CloudActorsExtensions
{
services.TryAddSingleton<IActorBus>(sp => new OrleansActorBus(sp.GetRequiredService<IGrainFactory>()));

// Attempt to replace the OOB persistence so we don't require a parameterless constructor and always
// have actors initialized with a specific id from the grain.
if (services.FirstOrDefault(d => d.ServiceType == typeof(IPersistentStateFactory)) is { } descriptor)
/// <summary>
/// Adds the <see cref="IActorBus"/> service and actor activation logic.
/// </summary>
/// <remarks>
/// It's not necessary to invoke this method if you already invoked <c>AddCloudActors</c>
/// on the <see cref="ISiloBuilder"/> when configuring Orleans.
/// </remarks>
public static IServiceCollection AddCloudActors(this IServiceCollection services)
{
services.Remove(descriptor);
services.Replace(ServiceDescriptor.Describe(
typeof(IPersistentStateFactory),
s => new ActorActivatorFactory((IPersistentStateFactory)CreateInstance(s, descriptor)),
descriptor.Lifetime
));
}
return services;
}

static object CreateInstance(IServiceProvider services, ServiceDescriptor descriptor)
{
if (descriptor.ImplementationInstance is not null)
return descriptor.ImplementationInstance;

if (descriptor.ImplementationFactory is not null)
return descriptor.ImplementationFactory(services);

return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType!);
}

class ActorActivatorFactory(IPersistentStateFactory factory) : IPersistentStateFactory
{
public IPersistentState<TState> Create<TState>(IGrainContext context, IPersistentStateConfiguration config)
{
var state = factory.Create<TState>(context, config);
// We're super conservative here, only replacing if all conditions are met.
// We check for a state with no parameterless constructor up-front, then go from there.
if (typeof(TState).GetConstructor(Type.EmptyTypes) is null &&
// We only know how the state bridge works
state is StateStorageBridge<TState> bridge &&
// We only support a ctor that receives the grain id as a string
context.GrainId.Key.ToString() is object id &&
typeof(TState).GetConstructor(new[] { typeof(string) }) is not null &&
// Even so, we might fail activation
Activator.CreateInstance(typeof(TState), id) is TState actor)
services.TryAddSingleton<IActorBus>(sp => new OrleansActorBus(sp.GetRequiredService<IGrainFactory>()));

// Attempt to replace the OOB persistence so we don't require a parameterless constructor and always
// have actors initialized with a specific id from the grain.
if (services.FirstOrDefault(d => d.ServiceType == typeof(IActorStateFactory)) is null &&
services.FirstOrDefault(d => d.ServiceType == typeof(IPersistentStateFactory)) is { } descriptor)
{
// In this case, we can force custom creation of the state object.
// Internally, this causes the StateStorageBridge<T>._grainState to not be
// forcedly constructed via Activator.CreateInstance with a parameterless constructor,
// and instead our "rehydrated" type is used instead.
// NOTE: this causes the OnStart on the PersistentState<TState> class to skip invoking
// ReadStateAsync, as it assumes rehydration makes that unnecessary.
// However, the JournaledGrain base class overrides the OnActivateAsync method and
// forces a sync, so we're good since our generated grain does that too.
bridge.OnRehydrate(new ActivationContext(new GrainState<TState>(actor)));
services.Remove(descriptor);
services.Replace(ServiceDescriptor.Describe(
typeof(IPersistentStateFactory),
s => new ActorStateFactory((IPersistentStateFactory)CreateInstance(s, descriptor)),
descriptor.Lifetime
));
}

return state;
return services;
}

class ActivationContext(object actor) : IRehydrationContext
static object CreateInstance(IServiceProvider services, ServiceDescriptor descriptor)
{
public IEnumerable<string> Keys => throw new NotImplementedException();
public bool TryGetBytes(string key, out ReadOnlySequence<byte> value) => throw new NotImplementedException();
public bool TryGetValue<T>(string key, out T? value)
{
if (actor is T typed)
{
value = typed;
return true;
}
if (descriptor.ImplementationInstance is not null)
return descriptor.ImplementationInstance;

value = default;
return false;
}
if (descriptor.ImplementationFactory is not null)
return descriptor.ImplementationFactory(services);

return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType!);
}
}
}

}
7 changes: 4 additions & 3 deletions src/CloudActors.Streamstone/CloudActors.Streamstone.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<PackageId>Devlooped.CloudActors.Streamstone</PackageId>
Expand All @@ -8,15 +8,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NuGetizer" Version="1.2.0" />
<PackageReference Include="Microsoft.Orleans.Runtime" Version="7.2.1" />
<PackageReference Include="NuGetizer" Version="1.0.5" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.2.1" />
<PackageReference Include="Streamstone" Version="2.3.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CloudActors.Package\CloudActors.Package.msbuildproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\CloudActors.Streamstone.CodeAnalysis\CloudActors.Streamstone.CodeAnalysis.csproj" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\CloudActors\CloudActors.csproj" Pack="false" />
</ItemGroup>

Expand Down
33 changes: 33 additions & 0 deletions src/CloudActors.Streamstone/StreamstoneOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Devlooped.CloudActors;

public class StreamstoneOptions
{
static readonly JsonSerializerOptions options = new()
{
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
Converters = { new JsonStringEnumConverter() },
};

/// <summary>
/// Default options to use when creating a <see cref="StreamstoneStorage"/> instance.
/// </summary>
public static StreamstoneOptions Default { get; } = new();

/// <summary>
/// When true, will automatically create a snapshot of the state every <see cref="SnapshotThreshold"/> events.
/// In order to include properties with private setters in the snapshot, the type must be annotated with
/// [JsonInclude].
/// </summary>
public bool AutoSnapshot { get; set; } = true;

/// <summary>
/// The settings to use when serializing and deserializing events and snapshot
/// if <see cref="AutoSnapshot"/> is true.
/// </summary>
public JsonSerializerOptions JsonOptions { get; set; } = options;
}
Loading

0 comments on commit f52c7c8

Please sign in to comment.