How-to guide
Write a worker
Implement method handlers with the worker SDKs, declare parameters, send progress feedback and read platform-delivered settings.
Prerequisites
- A service and a worker profile (see the [quickstart](../getting-started/quickstart.md))
- The worker SDK for your language: .NET, Java, Node.js or Python
Note
Kiwi Distro is currently in private preview. Interfaces and behaviour described here can change before general availability. Contact Kiwify Labs for access.
A worker is a process that registers for one service and executes its methods. SDKs exist for .NET, Java, Node.js and Python; they follow the same wire contract (Worker protocol).
Define a method#
Choose your language. The choice applies to every sample on the site and is remembered in your browser.
[MethodName("charge")]
public class ChargeMethod : MethodHandlerBase
{
public decimal Amount { get; set; }
public string Currency { get; set; } = "USD";
protected override void Execute() => SetResult(new { charged = Amount, currency = Currency });
}
worker.method("charge",
List.of(
ParameterSpec.of("amount").type(ParameterSpec.Type.NUMBER).required(),
ParameterSpec.of("currency").type(ParameterSpec.Type.STRING)
.withDefault("USD").pattern("^[A-Z]{3}$")),
ctx -> Map.of("charged", ctx.param("amount"), "currency", ctx.param("currency")));
worker.method(
"charge",
{
parameters: [
{ name: "amount", type: "number", required: true },
{ name: "currency", type: "string", default: "USD", pattern: "^[A-Z]{3}$" },
],
},
(ctx) => ({ charged: ctx.param("amount"), currency: ctx.param("currency") }),
);
worker.method(
"charge",
handler, # sync or async; return a value to succeed, raise to fail
parameters=[
ParameterSpec("amount", type="number", required=True),
ParameterSpec("currency", type="string", default="USD", pattern="^[A-Z]{3}$"),
],
)
In Java, Node.js and Python a handler returns a value to succeed and throws to fail. In .NET a handler is a class marked [MethodName]: request arguments are bound to its public properties (matched case-insensitively), and SetResult sets the result. The worker object is created as in the quickstart.
In Java, Node.js and Python a parameter has a name, a type, required, a default and a pattern. type is string, number, boolean, object, array or any. Names bind case-insensitively, defaults apply only when a value is absent, and pattern is checked on strings. If validation fails, the caller receives an error response and your handler is not run.
Class-style handlers#
A handler can also be a class. In .NET this is the only style: handlers are discovered by AddMethodHandlers().
[MethodName("greet")]
public class GreetMethod : MethodHandlerBase
{
public string Name { get; set; } = "";
protected override void Execute() => SetResult(new { message = $"hi {Name}" });
}
public final class GreetHandler extends MethodHandler {
public String methodName() { return "greet"; }
public List<ParameterSpec> parameters() {
return List.of(ParameterSpec.of("name").type(ParameterSpec.Type.STRING).required());
}
public Object execute() {
return Map.of("message", "hi " + requireParam("name"));
}
}
worker.register(GreetHandler.class); // needs a public no-arg constructor
class GreetHandler extends MethodHandler {
static override methodName = "greet";
static override parameters = [{ name: "name", type: "string" as const, required: true }];
override async execute() {
return { message: `hi ${this.requireParam("name")}` };
}
}
worker.register(GreetHandler);
class GreetHandler(MethodHandler):
method_name = "greet"
parameters = [ParameterSpec("name", type="string", required=True)]
async def execute(self):
return {"message": f"hi {self.require_param('name')}"}
worker.register(GreetHandler)
Send progress#
While a handler runs it can emit feedback, which is stored and can be queried later with kiwi distro list-feedback:
[MethodName("progress")]
public class ProgressMethod : MethodHandlerBase
{
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await Feedback.SendStartNotificationAsync(cancellationToken: cancellationToken);
for (var pct = 20; pct <= 100; pct += 20)
{
await Feedback.SendCompletionPercentageAsync(pct, cancellationToken: cancellationToken);
}
await Feedback.SendCompletionNotificationAsync(cancellationToken);
SetResult(new { done = true });
}
}
worker.method("progress", ctx -> {
ctx.feedback().sendStartNotification();
for (int i = 1; i <= 5; i++) ctx.feedback().sendCompletionPercentage(i * 20);
ctx.feedback().sendCompletionNotification();
return Map.of("done", true);
});
worker.method("progress", async (ctx) => {
await ctx.feedback.sendStartNotification();
for (let i = 1; i <= 5; i++) {
await ctx.feedback.sendCompletionPercentage(i * 20);
}
await ctx.feedback.sendCompletionNotification();
return { done: true };
});
async def progress(ctx):
await ctx.feedback.send_start_notification()
for i in range(1, 6):
await ctx.feedback.send_completion_percentage(i * 20)
await ctx.feedback.send_completion_notification()
return {"done": True}
Feedback types: start notification, periodic status, completion percentage (0 to 100), completion notification, event log, error report and custom (an object). Feedback is fire-and-forget: a failed feedback post never fails the method.
Settings and secrets#
Secrets attached to a service (kiwi distro add-service-secrets) are delivered to the worker at registration as application settings. Read them inside handlers, never at import or start-up, because they do not exist until the worker has registered:
// inside a MethodHandlerBase handler (or inject IWorkerApplicationSettings elsewhere)
var apiKey = Settings.GetRequired("api-key");
String apiKey = ctx.requireSetting("api-key");
const apiKey = ctx.requireSetting("api-key");
api_key = ctx.require_setting("api-key")
Values are a live view, so a rotated secret is picked up after re-registration. In .NET you can also await settings.WhenReady outside a handler.
Choose a delivery mode#
Work is always pulled: the middleware only signals that work exists.
execution_mode |
How the worker learns of work | Use when |
|---|---|---|
eager (default) |
A hanging GET /worker/messages?wait=N long-poll (long_poll_wait, default 25 s) |
The worker has no inbound port, or sits behind a load balancer. The balancer's idle timeout must exceed long_poll_wait |
lazy |
One pull at start-up, then a TCP wake-up probe on wakeup_probe_service_port |
The middleware can reach the worker's ip:port directly |
connected |
A persistent SignalR connection | .NET workers; needs session affinity and WebSocket support |
Configuration#
Settings resolve, lowest to highest precedence: appsettings.json, KIWI_-prefixed environment variables, --key value command-line arguments, then options passed in code. Required: auth_secret and middleware_url. See Worker and client settings.
Give workers tags ({"os": "linux", "group": "infra"}) so clients can route to a subset of workers with filters.
Concurrency#
Each SDK bounds concurrent requests (.NET defaults to a value derived from the CPU count; Java, Node.js and Python default to 32). See Capacity planning.
Shut down gracefully#
On SIGINT or SIGTERM the worker notifies the middleware, finishes in-flight requests and exits. run_forever() handles this for you.
Expected result
- A worker registers, handles requests with validated parameters and reports progress