A distributed coordination system based on the Linda coordination language, featuring remote execution via Roslyn scripting and C# Source Generators.
The system consists of four main components:
- TupleServer: A TCP-based server that manages multiple named "tuple spaces". It supports atomic coordination primitives like
IN(consume),OUT(produce), andRD(read). - TupleClient: A library providing the
TupleSpaceClientfor interacting with the server. - ExpressionRunner: A distributed worker that polls an
expressionstuple space for C# code strings to execute against specific tuple spaces. - TupleClient.Generators: A C# Source Generator that allows developers to write type-safe code for remote execution.
The TupleSpaceClient provides the following operations. Both string-based and generic type-safe APIs are supported.
| Operation | Description |
|---|---|
OutAsync(params string[] tuple) |
Produce: Adds a tuple to the space. |
InAsync(params string[] pattern) |
Consume: Blocks until a tuple matching the pattern is available, then removes and returns it. |
RdAsync(params string[] pattern) |
Read: Blocks until a tuple matching the pattern is available, then returns it without removing it. |
InpAsync(params string[] pattern) |
Probe Consume: Non-blocking version of IN. |
RdpAsync(params string[] pattern) |
Probe Read: Non-blocking version of RD. |
Patterns support wildcards using the "*" string.
The generic API uses C# structs to represent tuples. When using generics, the name of the struct is automatically prepended as the first element of the tuple in the tuple space.
[TupleDefinition]
public struct TaskTuple
{
public string Id { get; set; }
public int Priority { get; set; }
}
// Writes ("TaskTuple", "task-1", "10") to the space
await client.OutAsync(new TaskTuple { Id = "task-1", Priority = 10 });
// Reads back a TaskTuple with exact values
var task = await client.InAsync<TaskTuple>("task-1", 10);
// Use Wildcard.Any for pattern matching
var anyTask = await client.InAsync<TaskTuple>(Wildcard.Any, Wildcard.Any);Generic methods include OutAsync<T>, InAsync<T>, RdAsync<T>, InpAsync<T>, and RdpAsync<T>. Structs used with the generic API should be marked with the [TupleDefinition] attribute if they need to be available for remote evaluation.
The EvalAsync operation creates a Live Tuple. A live tuple contains a mixture of normal values and expressions (delegates) to be evaluated.
The evaluation happens concurrently on background threads. Once all expressions are evaluated, the results replace the expressions in the tuple, and the final "normal" tuple is published to the tuple space.
[TupleDefinition]
public struct MyTuple {
public string Name { get; set; }
public object Value { get; set; }
}
// Start concurrent evaluation
await client.EvalAsync(new MyTuple {
Name = "Task1",
Value = new Func<Task<int>>(async () => {
await Task.Delay(100);
return 42;
})
});
// This will block until the evaluation is complete and the tuple is published
var result = await client.InAsync<MyTuple>("Task1", Wildcard.Any);
// result.Value will be "42"For pattern matching in generic methods, use Wildcard.Any instead of the string "*". This provides type safety and avoids ambiguity when a tuple value might actually be the literal string "*".
// Match any TaskTuple regardless of Id or Priority
var task = await client.InAsync<TaskTuple>(Wildcard.Any, Wildcard.Any);
// Match a specific Id with any Priority
var task = await client.InAsync<TaskTuple>("task-1", Wildcard.Any);The TupleClient.Generators package includes a Roslyn analyzer that validates generic tuple method calls at compile time:
- TUPLE001: Reports an error if the number of pattern parameters doesn't match the number of properties in the tuple struct.
- TUPLE002: Reports an error if a parameter type doesn't match the corresponding property type (unless
Wildcard.Anyis used).
[TupleDefinition]
public struct TaskTuple
{
public string Id { get; set; }
public int Priority { get; set; }
}
// ✓ Correct: 2 parameters matching 2 properties with correct types
await client.InAsync<TaskTuple>("task-1", 10);
// ✓ Correct: Wildcard.Any can match any type
await client.InAsync<TaskTuple>(Wildcard.Any, Wildcard.Any);
// ✗ Error TUPLE001: Wrong number of parameters
await client.InAsync<TaskTuple>("task-1");
// ✗ Error TUPLE002: Type mismatch - Priority expects int, not string
await client.InAsync<TaskTuple>("task-1", "high");To improve performance when adding many tuples, use the BulkOutScope:
using (var scope = new BulkOutScope(client))
{
await client.OutAsync("key", "value1");
await client.OutAsync("key", "value2");
// Only OUT operations are allowed in this scope
}The system supports a powerful RunRemotelyAsync mechanism that allows code to be shipped to a remote ExpressionRunner for execution close to the data.
Mark a static method with the [RemoteEval] attribute. The method must take a TupleSpaceClient as its first argument (usually named c).
public static class MyLogic
{
[RemoteEval]
public static async Task PerformWork(TupleSpaceClient c)
{
var data = await c.InAsync("data", "*");
// ... process data ...
await c.OutAsync("result", "processed");
}
}The Source Generator automatically extracts the method body as a string and puts it in a RemoteCode class. Use RunRemotelyAsync to send this code to the runner:
await client.RunRemotelyAsync(RemoteCode.PerformWork);dotnet run --project TupleServer/TupleServer.csprojYou can start multiple runners to handle parallel workloads.
dotnet run --project ExpressionRunner/Program.csThe Addition project demonstrates summing numbers 1 to 1000 by distributing addition tasks across the runners.
dotnet run --project Addition/Program.csTupleServer/: The core TCP server.TupleClient/: The shared library andTupleSpaceClient.TupleClient.Generators/: The Roslyn Source Generator.ExpressionRunner/: The worker process usingMicrosoft.CodeAnalysis.CSharp.Scripting.Addition/: An example application utilizing all components.TupleServer.Tests/: Integration tests for the core protocol.