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
57 changes: 57 additions & 0 deletions SW.Serverless.Installer.UnitTests/PinnedMetadataCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Internal;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SW.PrimitiveTypes;
using SW.Serverless.Contract.Catalog;

namespace SW.Serverless.Installer.UnitTests;

[TestClass]
public class PinnedMetadataCacheTests
{
#pragma warning disable CS0618 // MemoryCacheOptions.Clock is how a test moves the cache's time
sealed class TestClock : ISystemClock
{
public DateTimeOffset UtcNow { get; set; } = DateTimeOffset.UtcNow;
}
#pragma warning restore CS0618

static Task Put(ObjectStore store, string key, string content) =>
store.WriteAsync(new MemoryStream(Encoding.UTF8.GetBytes(content)), new WriteFileSettings
{
Key = key,
Metadata = new Dictionary<string, string> { ["EntryAssembly"] = "Adapter.dll" }
});

[TestMethod]
public async Task A_pinned_version_is_resolved_once_and_a_current_package_again_after_the_cache_duration()
{
using var store = new ObjectStore();
var clock = new TestClock();
#pragma warning disable CS0618
var cache = new MemoryCache(new MemoryCacheOptions { Clock = clock });
#pragma warning restore CS0618
var options = new ServerlessOptions { AdapterRemotePath = "adapters", AdapterMetadataCacheDuration = 5 };
var installer = new AdapterInstaller(options, cache, store);
var versionKey = AdapterCatalogPaths.Version("adapters", "acme.x", "1.0.0");

await Put(store, "adapters/acme.x", "current 1");
await Put(store, versionKey, "version 1");
var current = await installer.GetMetadataAsync("acme.x");
var pinned = await installer.GetMetadataAsync("acme.x/1.0.0");

// A new current package; and, to make the cache observable, the version rewritten too
// (a published version never is).
await Put(store, "adapters/acme.x", "current 2");
await Put(store, versionKey, "version 1, rewritten");
clock.UtcNow += TimeSpan.FromMinutes(6);

Assert.AreNotEqual(current.Hash, (await installer.GetMetadataAsync("acme.x")).Hash, "the current package is read again");
Assert.AreEqual(pinned.Hash, (await installer.GetMetadataAsync("acme.x/1.0.0")).Hash, "the pinned version is not");
}
}
7 changes: 7 additions & 0 deletions SW.Serverless/Services/AdapterInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,13 @@ public async Task<InstalledAdapter> GetMetadataAsync(string adapterId)
?? throw new InvalidOperationException(
$"Adapter '{adapterId}' metadata at '{remotePath}' has an 'EntryAssembly' outside its package.");

// A pinned version never changes once published, so its resolution is kept for the life
// of the process. Only a plain id's current package can move, and it is read again after
// AdapterMetadataCacheDuration. A host whose agents are all pinned then makes no storage
// round trip per adapter every few minutes.
if (AdapterCatalogPaths.Split(adapterId).Version != null)
return memoryCache.Set($"{NamingPrefix}.{adapterId}", installed);

return memoryCache.Set($"{NamingPrefix}.{adapterId}", installed,
TimeSpan.FromMinutes(options.AdapterMetadataCacheDuration));
}
Expand Down
2 changes: 1 addition & 1 deletion docs/hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Rules to know:
|---|---|---|
| `AdapterRemotePath` | `"adapters"` | The root folder in storage. Must match the root adapters were published under; the CLI always publishes under `adapters`. |
| `AdapterLocalPath` | `"./adapters"` | Where packages are unpacked on this machine, one folder per package. |
| `AdapterMetadataCacheDuration` | `5` | Minutes the host remembers which package an adapter id resolves to. A newly promoted version is picked up once this expires. |
| `AdapterMetadataCacheDuration` | `5` | Minutes the host remembers which package an adapter id resolves to. A newly promoted version is picked up once this expires. A pinned ref (`{id}/{version}`) never changes, so it is resolved once and kept for the life of the process. |
| `CommandTimeout` | `30` | Seconds a classic session's command may take, unless the call passes its own. |
| `IdleTimeout` | `300` | Seconds a protocol 1 (.NET classic) adapter waits for its next command before it exits. |

Expand Down
Loading