diff --git a/examples/tests/sdk_tests/service/callbacks.test.w b/examples/tests/sdk_tests/service/callbacks.test.w index 2220124ff12..3ed3b962a62 100644 --- a/examples/tests/sdk_tests/service/callbacks.test.w +++ b/examples/tests/sdk_tests/service/callbacks.test.w @@ -4,27 +4,21 @@ bring http; // hack: only supported in the "sim" target for now if util.env("WING_TARGET") == "sim" { - - let b = new cloud.Bucket(); let startCounter = new cloud.Counter(); let status = "status"; let started = "started"; let stopped = "stopped"; - class MyServiceHandler impl cloud.IServiceHandler { - pub inflight onStart() { - b.put(status, started); - startCounter.inc(); - } + let s = new cloud.Service(inflight () => { + b.put(status, started); + startCounter.inc(); - pub inflight onStop() { + return () => { b.put(status, stopped); startCounter.dec(); - } - } - - let s = new cloud.Service(new MyServiceHandler(), autoStart: false); + }; + }, autoStart: false); test "does not start automatically if autoStart is false" { assert(!b.tryGet(status)?); diff --git a/examples/tests/sdk_tests/service/minimal.test.w b/examples/tests/sdk_tests/service/minimal.test.w new file mode 100644 index 00000000000..c1a0544027d --- /dev/null +++ b/examples/tests/sdk_tests/service/minimal.test.w @@ -0,0 +1,20 @@ +bring cloud; +bring util; + +// hack: only supported in the "sim" target for now +if util.env("WING_TARGET") == "sim" { + + let s = new cloud.Service(inflight () => { + log("hello, service!"); + + return () => { + log("stopping!"); + }; + }); + + test "start and stop" { + assert(s.started()); + s.stop(); + assert(!s.started()); + } +} \ No newline at end of file diff --git a/examples/tests/sdk_tests/service/stateful.test.w b/examples/tests/sdk_tests/service/stateful.test.w index aa47ff9f960..d55fc93bb17 100644 --- a/examples/tests/sdk_tests/service/stateful.test.w +++ b/examples/tests/sdk_tests/service/stateful.test.w @@ -4,7 +4,7 @@ bring http; // hack: only supported in the "sim" target for now if util.env("WING_TARGET") == "sim" { - class MyService impl cloud.IServiceHandler { + class MyService { b: cloud.Bucket; body: str; @@ -15,41 +15,29 @@ if util.env("WING_TARGET") == "sim" { this.body = body; // it's idiomatic to just pass `this` here and implement the callbacks on the current object. - this.s = new cloud.Service(this); - } - - inflight var state: num; - - inflight init() { - this.state = 123; - } - - pub inflight onStart() { - log("starting service"); - util.sleep(1s); - this.b.put("ready", "true"); - let port = MyService.startServer(this.body); - log("listening on port ${port}"); - this.state = 456; - this.b.put("port", "${port}"); - } - - pub inflight onStop() { - log("stopping service"); - log("state is: ${this.state}"); - - // make sure inflight state is presistent across onStart/onStop - assert(this.state == 456); - MyService.stopServer(); + this.s = new cloud.Service(inflight () => { + log("starting service"); + util.sleep(1s); + this.b.put("ready", "true"); + let port = MyService.startServer(this.body); + log("listening on port ${port}"); + let state = 456; + this.b.put("port", "${port}"); + + return () => { + log("stopping service"); + log("state is: ${state}"); + + // make sure inflight state is presistent across onStart/onStop + assert(state == 456); + MyService.stopServer(); + }; + }); } pub inflight access() { // when access() is called we expect the service to have completed initialization this.b.get("ready"); - - // this state belongs to the inflight client created for the test cloud function - // and not to the service, so we expect it to stay 123. - assert(this.state == 123); } pub inflight port(): num { diff --git a/libs/wingsdk/src/cloud/service.md b/libs/wingsdk/src/cloud/service.md index 901924c2cfa..518be52de4a 100644 --- a/libs/wingsdk/src/cloud/service.md +++ b/libs/wingsdk/src/cloud/service.md @@ -16,60 +16,71 @@ sidebar_position: 1 The `cloud.Service` class represents a cloud service that has a start and optional stop lifecycle. -Services are a common way to define long running code, such as web servers and custom daemons. +Services are a common way to define long running code, such as microservices. ## Usage ### Creating a service +When defining a service, the first argument is an inflight closure that represents +the service handler. This handler is responsible to perform any initialization +activity and **return asynchronously** when initialization is complete. + ```js bring cloud; -// At minimum a service needs to have an onStart handler. -let service = new cloud.Service( - onStart: inflight() => { - log("Service started..."); - } -); +new cloud.Service(inflight () => { + // ... + // kick off any initialization activities asynchronously + // ... + log("Service started..."); +}); ``` ### Disable auto-start -By default the service resource will start automatically, however this can be disabled by -passing `autoStart: false` to the constructor. +By default the service resource will start automatically, however this can be disabled by passing +`autoStart: false` to the constructor. ```js bring cloud; -let service = new cloud.Service( - autoStart: false, - onStart: inflight() => { - log("Service started..."); - } -); +let handler = inflight () => { + log("service started..."); +}; + +let service = new cloud.Service(handler, autoStart: false); ``` -### Defining service with stop behavior +### Service cleanup + +Optionally, the service handler inflight closure can return another inflight closure which will be +called when the service is stopped. Using a return closure allows naturally passing context between +the async calls. ```js bring cloud; -let service = new cloud.Service( - onStart: inflight() => { - log("Service started..."); - }, - onStop: inflight() => { +new cloud.Service(inflight() => { + let server = startHttpServer(); + log("Service started..."); + return () => { log("Service stopped..."); - }, -); + server.close(); + }; +}); ``` ### Stopping and starting a service -The inflight methods `start` and `stop` are used exactly how they sound, to stop and start the service. -Here is an example of using a service that will track how often it is started and stopped using counters. -An important aspect to note is that consecutive starts and stops have no affect on a service. For example -if a `service.start()` is called on a service that is already started, nothing will happen. +The inflight methods `start()` and `stop()` are used exactly how they sound, to stop and start the +service. The method `started()` returns a `bool` indicating if the service is currently started. + +Here is an example of using a service that will track how often it is started and stopped using +counters. + +An important aspect to note is that consecutive starts and stops have no affect on a service. For +example, if a `service.start()` is called on a service that is already started, nothing will happen. ```js bring cloud; @@ -77,25 +88,26 @@ bring cloud; let startCounter = new cloud.Counter() as "start counter"; let stopCounter = new cloud.Counter() as "stop counter"; -let service = new cloud.Service( - autoStart: false, - onStart: inflight() => { - let i = startCounter.inc(); - log("Service started for the ${i}th time..."); - }, - onStop: inflight() => { +let handler = inflight() => { + let i = startCounter.inc(); + log("Service started for the ${i}th time..."); + return () => { let i = stopCounter.inc(); log("Service stopped for the ${i}th time..."); - }, -); + }; +}; + +let service = new cloud.Service(handler, autoStart: false); // Functions to stop and start the service new cloud.Function(inflight() => { service.start(); + assert(service.started()); }) as "start service"; new cloud.Function(inflight() => { service.stop(); + assert(!service.started()); }) as "stop service"; ``` diff --git a/libs/wingsdk/src/cloud/service.ts b/libs/wingsdk/src/cloud/service.ts index 727e16d5008..58384ff09ff 100644 --- a/libs/wingsdk/src/cloud/service.ts +++ b/libs/wingsdk/src/cloud/service.ts @@ -74,7 +74,7 @@ export abstract class Service extends Resource implements IInflightHost { // indicates that we are calling the inflight constructor and the // inflight "handle" method on the handler resource. - handler._registerBind(this, ["onStart", "onStop", "$inflight_init"]); + handler._registerBind(this, ["handle", "$inflight_init"]); const inflightClient = handler._toInflight(); const lines = new Array(); @@ -86,12 +86,8 @@ export abstract class Service extends Resource implements IInflightHost { lines.push(" return $obj;"); lines.push("};"); - lines.push("exports.onStart = async function() {"); - lines.push(" return (await $initOnce()).onStart();"); - lines.push("};"); - - lines.push("exports.onStop = async function() {"); - lines.push(` return (await $initOnce()).onStop();`); + lines.push("exports.handle = async function() {"); + lines.push(" return (await $initOnce()).handle();"); lines.push("};"); const assetName = ResourceNames.generateName(this, { @@ -130,7 +126,11 @@ export abstract class Service extends Resource implements IInflightHost { /** @internal */ public _getInflightOps(): string[] { - return [ServiceInflightMethods.START, ServiceInflightMethods.STOP]; + return [ + ServiceInflightMethods.START, + ServiceInflightMethods.STOP, + ServiceInflightMethods.STARTED, + ]; } } @@ -148,11 +148,18 @@ export interface IServiceClient { * @inflight */ start(): Promise; + /** * Stop the service * @inflight */ stop(): Promise; + + /** + * Indicates whether the service is started. + * @inflight + */ + started(): Promise; } /** @@ -172,16 +179,42 @@ export interface IServiceHandlerClient { * * DO NOT BLOCK! This handler should return as quickly as possible. If you need to run a long * running process, start it asynchronously. + * + * + * @returns an optional function that can be used to cleanup any resources when the service is + * stopped. + * + * @example + * + * bring cloud; + * + * new cloud.Service(inflight () => { + * log("starting service..."); + * return () => { + * log("stoping service..."); + * }; + * }); + * */ - onStart(): Promise; + handle(): Promise; +} + +/** + * @inflight `@winglang/sdk.cloud.IServiceStopHandlerClient` + */ +export interface IServiceStopHandler extends IResource {} +/** + * Inflight client for `IServiceStopHandler`. + */ +export interface IServiceStopHandlerClient { /** - * Handler to run in order to stop the service. This is where you implement the shutdown logic of - * the service, stop any activities, and clean up any resources. + * Handler to run when the service stops. This is where you implement the cleanup logic of + * the service, stop any activities asychronously. * - * @default - no special activity at shutdown + * @inflight */ - onStop?(): Promise; + handle(): Promise; } /** @@ -191,4 +224,5 @@ export interface IServiceHandlerClient { export enum ServiceInflightMethods { START = "start", STOP = "stop", + STARTED = "started", } diff --git a/libs/wingsdk/src/target-sim/service.inflight.ts b/libs/wingsdk/src/target-sim/service.inflight.ts index b3d80cf93c5..586599818b5 100644 --- a/libs/wingsdk/src/target-sim/service.inflight.ts +++ b/libs/wingsdk/src/target-sim/service.inflight.ts @@ -4,7 +4,7 @@ import { ServiceAttributes, ServiceSchema, } from "./schema-resources"; -import { IServiceClient } from "../cloud"; +import { IServiceClient, IServiceStopHandlerClient } from "../cloud"; import { Sandbox } from "../shared/sandbox"; import { ISimulatorContext, ISimulatorResourceInstance } from "../simulator"; import { TraceType } from "../std"; @@ -15,6 +15,7 @@ export class Service implements IServiceClient, ISimulatorResourceInstance { private readonly autoStart: boolean; private readonly sandbox: Sandbox; private running: boolean = false; + private onStop?: IServiceStopHandlerClient; constructor(props: ServiceSchema["props"], context: ISimulatorContext) { this.context = context; @@ -54,7 +55,7 @@ export class Service implements IServiceClient, ISimulatorResourceInstance { return; } - await this.sandbox.call("onStart"); + this.onStop = await this.sandbox.call("handle"); this.running = true; } @@ -64,6 +65,21 @@ export class Service implements IServiceClient, ISimulatorResourceInstance { return; } - await this.sandbox.call("onStop"); + if (this.onStop) { + // wing has a quirk where it will return either a function or an object that implements + // "handle", depending on whether the closure is defined in an inflight context or preflight + // context. so we need to handle both options here. (in wing this is handled by the compiler). + if (typeof(this.onStop) === "function") { + await (this.onStop as any)(); + } else { + await this.onStop.handle(); + } + } + + this.running = false; + } + + public async started(): Promise { + return this.running; } }