Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions SW.Serverless.Contract/Protos/adapter.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ message Ready {
int32 max_in_flight = 1;
map<string, string> startup_values = 2;
map<string, string> adapter_values = 3;

// The host's ServerlessOptions as JSON: what a classic adapter receives on argv[1] and reads
// through Runner.ServerlessOptions (its cloud-files settings, to read an attachment from the
// host's storage). Empty from a host built before this field, and the SDK then falls back to
// defaults, as the classic runner does when argv[1] can't be read.
string serverless_options = 4;
}

message Invoke {
Expand Down
7 changes: 7 additions & 0 deletions SW.Serverless.Samples.Ticker/Handler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ public Task<object> ReadThroughRunner(string name) => Task.FromResult<object>(ne
value = SW.Serverless.Sdk.Runner.StartupValueOf(name)
});

/// <summary>What a classic adapter reads from argv[1], read the same way once resident.</summary>
public Task<object> ReadServerlessOptions() => Task.FromResult<object>(new
{
remotePath = SW.Serverless.Sdk.Runner.ServerlessOptions?.AdapterRemotePath,
bucket = SW.Serverless.Sdk.Runner.ServerlessOptions?.CloudFilesOptions?.BucketName
});

/// <summary>A result too big to send in one frame.</summary>
public Task<string> Huge(int megabytes) =>
Task.FromResult(new string('x', megabytes * 1024 * 1024));
Expand Down
1 change: 1 addition & 0 deletions SW.Serverless.Sdk/Resident/ResidentRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ Task OnReadyAsync(Ready ready)
adapterValues = new Dictionary<string, string>(ready.AdapterValues, StringComparer.OrdinalIgnoreCase);
inFlight = new SemaphoreSlim(Math.Max(1, ready.MaxInFlight));
Runner.UseResident(startupValues, adapterValues, ValueOf);
Runner.UseResidentOptions(ready.ServerlessOptions);

// Now, and only now, is it safe to construct the handler.
handler = handlerFactory(this);
Expand Down
15 changes: 15 additions & 0 deletions SW.Serverless.Sdk/Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ internal static void UseResident(IReadOnlyDictionary<string, string> values,
residentValueOf = valueOf;
}

/// <summary>
/// The host's ServerlessOptions from Ready, so <see cref="ServerlessOptions"/> isn't null in a
/// resident adapter. Defaults when the host sent none, as the classic runner falls back.
/// </summary>
internal static void UseResidentOptions(string serverlessOptionsJson)
{
ServerlessOptions = null;
if (!string.IsNullOrEmpty(serverlessOptionsJson))
{
try { ServerlessOptions = JsonConvert.DeserializeObject<ServerlessOptions>(serverlessOptionsJson); }
catch (Exception exception) { AdapterLogger.LogWarning(exception, "Failed to parse ServerlessOptions, using defaults instead."); }
}
ServerlessOptions ??= new ServerlessOptions();
}

/// <summary>The default declared with <see cref="Expect(string, string, bool, string)"/>, if any.</summary>
internal static string ExpectedDefault(string name) =>
expectedStartupValues.TryGetValue(name, out var expected) ? expected.Default : null;
Expand Down
17 changes: 17 additions & 0 deletions SW.Serverless.UnitTests/ResidentAdapterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,23 @@ public async Task Per_invocation_properties_reach_the_command()
await adapters.StopAsync(TickerId, "percall", drain: false);
}

/// <summary>
/// A classic adapter reads the host's ServerlessOptions (its cloud-files settings, say, to
/// fetch an attachment) from argv[1]. Resident, the same Runner.ServerlessOptions comes from
/// Ready; before, it stayed null and such an adapter threw the first time it read it.
/// </summary>
[TestMethod]
public async Task The_hosts_serverless_options_reach_the_runner()
{
var instance = await StartTicker("options");

var options = await instance.InvokeAsync<Dictionary<string, string>>("ReadServerlessOptions");

Assert.AreEqual("adapters", options["remotePath"]);

await adapters.StopAsync(TickerId, "options", drain: false);
}

/// <summary>
/// A per-call value wins over the process default, and an adapter called WITHOUT properties
/// still sees the startup value — which is what keeps every existing adapter working.
Expand Down
8 changes: 7 additions & 1 deletion SW.Serverless/Resident/ResidentAdapterHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SW.PrimitiveTypes;

namespace SW.Serverless.Resident
{
Expand Down Expand Up @@ -43,10 +44,14 @@ public class ResidentAdapterHost : IResidentAdapterHost, IHostedService, IAsyncD

readonly IResidentAdapterLocator locator;

// What classic adapters get on argv[1]; resident ones get it in Ready.
readonly string serverlessOptionsJson;

public ResidentAdapterHost(ResidentOptions options, IAdapterEventSink sink,
IAdapterStateStore stateStore, IResidentAdapterLocator locator, ILoggerFactory loggerFactory,
Runtimes.AdapterRuntimes runtimes = null)
Runtimes.AdapterRuntimes runtimes = null, ServerlessOptions serverlessOptions = null)
{
serverlessOptionsJson = serverlessOptions == null ? null : Newtonsoft.Json.JsonConvert.SerializeObject(serverlessOptions);
this.options = options;
this.sink = sink;
this.stateStore = stateStore;
Expand Down Expand Up @@ -211,6 +216,7 @@ async Task SpawnAsync(Supervised supervised, CancellationToken cancellationToken
spec.StartupValues ?? new Dictionary<string, string>()),
AdapterValues = new Dictionary<string, string>(
spec.AdapterValues ?? new Dictionary<string, string>()),
ServerlessOptionsJson = serverlessOptionsJson,
RestartCount = supervised.RestartCount
};

Expand Down
6 changes: 5 additions & 1 deletion SW.Serverless/Resident/ResidentAdapterInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ internal ResidentAdapterInstance(string adapterId, string instanceKey, string to
public int ProtocolVersion { get; internal set; }
public IReadOnlyDictionary<string, string> AdapterValues { get; internal set; }

/// <summary>The host's ServerlessOptions as JSON, sent in Ready; null sends none.</summary>
internal string ServerlessOptionsJson { get; set; }

public IReadOnlyCollection<string> Diagnostics => diagnostics.ToArray();

/// <summary>
Expand Down Expand Up @@ -211,7 +214,8 @@ internal async Task RunStreamAsync(IAsyncStreamReader<AdapterFrame> input,
{
MaxInFlight = options.MaxInFlight,
StartupValues = { WithoutNulls(StartupValues) },
AdapterValues = { WithoutNulls(AdapterValues) }
AdapterValues = { WithoutNulls(AdapterValues) },
ServerlessOptions = ServerlessOptionsJson ?? string.Empty
}
});

Expand Down
1 change: 1 addition & 0 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ The host answers with `Ready`:
| `max_in_flight` | How many events the adapter may have waiting for an EventAck at once. |
| `startup_values` | The settings, name to value. Includes `CorrelationId` for classic sessions. |
| `adapter_values` | Extra values from the package's storage metadata and the host. |
| `serverless_options` | The host's `ServerlessOptions` as JSON, what a classic adapter gets on argv[1] (its cloud-files settings, for one). The .NET SDK exposes it as `Runner.ServerlessOptions`. Empty from older hosts; the SDK then uses defaults. |

After Ready the host considers the instance ready and may send Invoke at once. An SDK should build
the adapter and run its start hook on Ready, before running commands, and must keep reading frames
Expand Down
Loading