diff --git a/docs/06-concepts/01-server-fundamentals/01-your-serverpod-project.md b/docs/06-concepts/01-server-fundamentals/01-your-serverpod-project.md index 0d7b0d80..118bb11d 100644 --- a/docs/06-concepts/01-server-fundamentals/01-your-serverpod-project.md +++ b/docs/06-concepts/01-server-fundamentals/01-your-serverpod-project.md @@ -94,7 +94,7 @@ void run(List args) async { } ``` -Endpoints need no registration: the generated `Endpoints` object passed to the constructor carries them all. Web routes, by contrast, are registered imperatively on `pod.webServer`, which is why the scaffolded `run()` contains route setup. When `pod.start()` runs, the server connects to the database, applies pending migrations when started with `--apply-migrations`, connects to Redis if enabled, brings up its servers, and starts background work such as [future calls](../scheduling/setup) and [health checks](../operations/health-checks). +Endpoints need no registration: the generated `Endpoints` object passed to the constructor carries them all. Web routes, by contrast, are registered imperatively on `pod.webServer`, which is why the scaffolded `run()` contains route setup. When `pod.start()` runs, the server connects to the database, applies pending migrations when started with `--apply-migrations`, connects to Redis if enabled, brings up its servers, and starts background work such as [future calls](../scheduling/overview) and [health checks](../operations/health-checks). ## The three servers diff --git a/docs/06-concepts/01-server-fundamentals/02-running-your-server.md b/docs/06-concepts/01-server-fundamentals/02-running-your-server.md index a4f29834..5e9d88c5 100644 --- a/docs/06-concepts/01-server-fundamentals/02-running-your-server.md +++ b/docs/06-concepts/01-server-fundamentals/02-running-your-server.md @@ -74,7 +74,7 @@ The server accepts arguments that control how it starts: `--mode` selects the ru In production, the `--role` argument controls which parts of the server run: -- **`monolith`** (default) runs everything: the API, Insights, and web servers, plus [future calls](../scheduling/setup) and [health checks](../operations/health-checks). +- **`monolith`** (default) runs everything: the API, Insights, and web servers, plus [future calls](../scheduling/overview) and [health checks](../operations/health-checks). - **`serverless`** serves requests only. Future calls and health checks are disabled, which fits platforms that start and stop instances on demand. - **`maintenance`** starts no servers. It performs one-shot work, applying migrations when passed `--apply-migrations` and running any due future calls, then exits. The exit code reports success or failure, which makes it fit for CI jobs and scheduled maintenance tasks. diff --git a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md index 93778f19..bdcec55a 100644 --- a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md +++ b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md @@ -22,7 +22,7 @@ Serverpod creates a session for every unit of work it runs, and the type reflect | **WebCallSession** | [Web server](../web-server/overview) routes | Single request | Web pages, form submissions | | **MethodStreamSession** | [Streaming methods](./streaming) | Stream duration | Real-time updates, chat | | **StreamingSession** | WebSocket connections of the [deprecated streaming endpoints API](./streaming#streaming-endpoints-deprecated) | Connection duration | Legacy real-time code | -| **FutureCallSession** | [Scheduled tasks](../scheduling/setup) | Task execution | Email sending, batch jobs | +| **FutureCallSession** | [Scheduled tasks](../scheduling/overview) | Task execution | Email sending, batch jobs | | **InternalSession** | [Manual creation](#create-a-session-for-background-work) | Until closed | Background work, migrations | You rarely choose a type yourself: your endpoint methods receive the right one, and manual creation always produces an `InternalSession`. @@ -49,7 +49,7 @@ class ExampleEndpoint extends Endpoint { | `authenticated` | The current user's authentication info. See [authentication](../authentication/basics). | | `log(...)` | Write a log entry tied to this session. See [logging](#logging). | | `addWillCloseListener(...)` | Register a cleanup callback that runs before the session closes. | -| `serverpod` | The running Serverpod instance, for example for [future calls](../scheduling/setup). | +| `serverpod` | The running Serverpod instance, for example for [future calls](../scheduling/overview). | ## Session lifecycle @@ -117,7 +117,7 @@ Future processUser(Session session, int userId) async { } ``` -Schedule the work as a [future call](../scheduling/setup) instead. Future calls survive server restarts and receive their own session when they run: +Schedule the work as a [future call](../scheduling/future-calls) instead. Future calls survive server restarts and receive their own session when they run: ```dart Future processUser(Session session, int userId) async { @@ -146,6 +146,6 @@ The test tools provide a `sessionBuilder` for calling endpoints in tests and sim ## Related - [Working with endpoints](../endpoints-and-apis): the methods that receive sessions. -- [Scheduling](../scheduling/setup): future calls, the managed way to run delayed work. +- [Scheduling](../scheduling/overview): future calls, the managed way to run delayed work. - [Logging](../operations/logging): where session log entries go. - [Authentication](../authentication/basics): what `session.authenticated` holds. diff --git a/docs/06-concepts/06-scheduling/01-overview.md b/docs/06-concepts/06-scheduling/01-overview.md new file mode 100644 index 00000000..acbe4aa7 --- /dev/null +++ b/docs/06-concepts/06-scheduling/01-overview.md @@ -0,0 +1,41 @@ +--- +description: Future calls schedule work to run later, on a delay, at a set time, or on a repeating schedule, persisted in the database so they survive restarts. +--- + +# Overview + +A future call is a piece of server work you schedule to run later instead of right now. You pick a method, say when it should run, and Serverpod invokes it at that time, even if the request that scheduled it has long since finished. Common uses are a welcome email an hour after sign-up, a reminder a day before an appointment, or a cleanup job that runs every night. + +Future calls are stored in the database, so they survive a server restart and are shared across every server instance you run. This makes them different from an in-memory `Timer`: a scheduled call is not lost if the process that created it stops. + +## What you can schedule + +You write a future call as a method on a class, generate the type-safe code, and then schedule it in one of three ways: + +- **After a delay**, for example one hour from now. +- **At a specific time**, for example a fixed date and time in the future. +- **On a repeating schedule**, either a fixed interval (every 20 minutes) or a [cron](https://en.wikipedia.org/wiki/Cron) expression (every day at 02:00 UTC). Cron is a standard text format for describing repeating schedules. + +## Execution guarantees + +A scheduled call runs **at least once**. Across all your running server instances, one instance claims each call and normally runs it a single time. If that instance crashes partway through, the call is picked up and run again by another instance, so a call can occasionally run more than once. Write work that is safe to repeat (for example, check whether the email was already sent before sending it). + +If the method itself throws an exception, Serverpod logs the error and does not retry the call. You are responsible for retrying failed work: schedule a new call if the work needs to happen. Recurring calls are the exception, since the next run is always scheduled regardless of whether the current one succeeds. + +## Where future calls run + +Which servers run future calls depends on their role, the mode a server is started in. Servers in the default `monolith` role run them, and future calls need a database. A server started in the `serverless` role does not run them. If you host in a serverless environment, run a separate scheduled process in the `maintenance` role to execute due calls. See [server roles](../server-fundamentals/running-your-server#choose-a-server-role) and [hosting elsewhere](../../deployments/custom-hosting/hosting-elsewhere). + +## What each page covers + +- **[Future calls](future-calls)**: define a future call, schedule it after a delay or at a time, and cancel scheduled calls. +- **[Recurring tasks](recurring-tasks)**: run a call on a repeating cron or interval schedule. +- **[Inheritance](inheritance)**: extend future calls from other classes and modules. +- **[Configuration](configuration)**: set concurrency, the scan interval, and broken-call handling. +- **[Legacy](legacy)**: the older string-based API, kept for existing code. + +## Related + +- [Sessions](../endpoints-and-apis/sessions): the session a future call receives when it runs. +- [Server roles](../server-fundamentals/running-your-server#choose-a-server-role): which roles run scheduled work. +- [Configuration reference](../lookups/configuration-reference): every future-call key with its environment variable. diff --git a/docs/06-concepts/06-scheduling/01-setup.md b/docs/06-concepts/06-scheduling/01-setup.md deleted file mode 100644 index 5c3a5b97..00000000 --- a/docs/06-concepts/06-scheduling/01-setup.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -description: Future calls in Serverpod schedule work to run after a delay, at a specific time, or on a recurring interval, persisted across restarts. ---- - -# Setup - -Serverpod supports scheduling future work with the `future call` feature. Future calls are calls that will be invoked at a later time. An example is if you want to send a drip-email campaign after a user signs up. You can schedule a future call for a day, a week, a month, or a [recurring interval](recurring-task). The calls are stored in the database, so they will persist even if the server is restarted. - -A future call is guaranteed to only execute once across all your instances that are running, but execution failures are not handled automatically. It is your responsibility to schedule a new future call if the work was not able to complete. - -:::info -The future call feature is not enabled when running Serverpod in serverless mode. -::: - -To create future calls, extend the `FutureCall` class and define the methods you wish to invoke at a later time. - -```dart -import 'package:serverpod/serverpod.dart'; - -class ExampleFutureCall extends FutureCall { - Future doWork(Session session) async { - // Do something interesting in the future here. - } - - Future doOtherWork(Session session, String data) async { - // Do something interesting in the future here. - } -} -``` - -:::info -For a method to be recognized by Serverpod as a future call, it must return a `Future` and have at least one positional parameter which must be a [`Session`](../endpoints-and-apis/sessions) object. You can pass any serializable types as other parameters, and even use `List`, `Map`, `Set` or Dart records as long as they are typed. `Streaming` parameters are not supported. -::: - -:::warning -It is not valid to override the `invoke` method of the `FutureCall` class. This method is reserved for the execution of the future call. -::: - -Next, you need to generate the code for your future calls: - -```bash -$ serverpod generate -``` - -Calling `serverpod generate` will create a type-safe interface for invoking the future calls in the server's `generated/future_calls.dart` file. This interface can be accessed from the Serverpod object. - -The future calls you create are registered by `Serverpod` after the server starts. - -```dart -import 'package:serverpod/serverpod.dart'; - -import 'src/generated/protocol.dart'; -import 'src/generated/endpoints.dart'; - -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); - - await pod.start(); -} -``` - -You are now able to schedule future calls to be invoked in the future by calling either `callWithDelay` or `callAtTime` depending on your needs. - -:::note -The `futureCalls` getter is only available if at least one future call has been defined. -::: - -:::warning -Scheduling a future call before the server starts will lead to exceptions. -::: - -Invoke a future call 1 hour from now by calling `callWithDelay`. - -```dart -await pod.futureCalls - .callWithDelay(const Duration(hours: 1)) - .example - .doWork(); -``` - -Invoke a future call at a specific time and/or date in the future by calling `callAtTime`. - -```dart -await pod.futureCalls - .callAtTime(DateTime(2026, 1, 1)) - .example - .doOtherWork('1'); -``` - -:::info -Scheduling a future call at a specific time/date will always resolve the `DateTime` to UTC. -::: - -When scheduling a future call, it is also possible to give it an `identifier` so that it can be referenced later. The same identifier can be applied to multiple future calls. - -```dart -await pod.futureCalls - .callWithDelay( - const Duration(hours: 1), - identifier: 'an-identifying-string', - ) - .example - .doWork(); -``` - -This identifier can then be used to cancel all future calls scheduled with said identifier. - -```dart -await pod.futureCalls.cancel('an-identifying-string'); -``` - -Calls that share an identifier still run independently; the identifier only groups them so you can cancel them together, and cancelling affects only calls that have not run yet. - -## Schedule from an endpoint - -The examples above use the `Serverpod` object directly. Inside an endpoint you reach it through `session.serverpod`, so you can schedule a future call in response to a request: - -```dart -class OrderEndpoint extends Endpoint { - Future placeOrder(Session session, Order order) async { - // ... store the order ... - - // Send a follow-up an hour from now. - await session.serverpod.futureCalls - .callWithDelay(const Duration(hours: 1)) - .example - .doWork(); - } -} -``` - -## Inspecting scheduled calls and timeouts - -Scheduled future calls are stored in the `serverpod_future_call` database table. You can query it to see what is pending, which helps when debugging why a call did or did not run. - -Future calls do not have an execution timeout: a call runs until its method returns. If a call needs a time limit, enforce it inside the method yourself, for example with `Future.timeout` around the work. diff --git a/docs/06-concepts/06-scheduling/02-future-calls.md b/docs/06-concepts/06-scheduling/02-future-calls.md new file mode 100644 index 00000000..9211e523 --- /dev/null +++ b/docs/06-concepts/06-scheduling/02-future-calls.md @@ -0,0 +1,138 @@ +--- +description: A future call is a method Serverpod runs later, scheduled after a delay or at a specific time, and cancellable by identifier. +--- + +# Future calls + +A future call is a method that Serverpod runs later. You define it on a class, generate the type-safe code, and schedule it from anywhere you have access to the server. This page covers the one-off cases: run once after a delay, or once at a specific time. For repeating schedules, see [Recurring tasks](recurring-tasks). + +## Define a future call + +Extend the `FutureCall` class and add the methods you want to invoke later. + +```dart +import 'package:serverpod/serverpod.dart'; + +class ExampleFutureCall extends FutureCall { + Future doWork(Session session) async { + // Do something interesting in the future here. + } + + Future doOtherWork(Session session, String data) async { + // Do something interesting in the future here. + } +} +``` + +Each method must: + +- Return `Future`. +- Take a [`Session`](../endpoints-and-apis/sessions) as its first parameter. +- Use only [serializable types](../data-and-the-database/models) for any other parameters. Typed `List`, `Map`, `Set`, and Dart records are allowed, but streams are not. + +Generate the code for your future calls. With `serverpod start` running, saving the file regenerates the code. Outside a session, run `serverpod generate`. + +This creates a type-safe interface in your server's `lib/src/generated/future_calls.dart` file, reachable from the `Serverpod` object as `pod.futureCalls`. You do not register these calls yourself. Serverpod does it for you when the server starts, so your `server.dart` needs no future-call setup: + +```dart +import 'package:serverpod/serverpod.dart'; + +import 'src/generated/protocol.dart'; +import 'src/generated/endpoints.dart'; + +void run(List args) async { + final pod = Serverpod( + args, + Protocol(), + Endpoints(), + ); + + await pod.start(); +} +``` + +:::note +The `pod.futureCalls` accessor exists only once you have defined at least one future call and generated the code. +::: + +:::warning +Scheduling a call before the server has started throws an exception, so schedule from an endpoint or after `pod.start()` rather than while building the server. +::: + +## Schedule a call + +Schedule a call by chaining the timing method, the accessor for your class, and the method to invoke. The accessor is your class name with the first letter lowercased and a trailing `FutureCall` removed, so `ExampleFutureCall` becomes `example`. + +Run a call one hour from now with `callWithDelay`: + +```dart +await pod.futureCalls + .callWithDelay(const Duration(hours: 1)) + .example + .doWork(); +``` + +Run a call at a specific time with `callAtTime`: + +```dart +await pod.futureCalls + .callAtTime(DateTime(2030, 1, 1)) + .example + .doOtherWork('1'); +``` + +The call runs at the exact instant you pass. A `DateTime` denotes an absolute moment, so `DateTime(2030, 1, 1)` schedules the call for midnight in the server's local time. Pass a UTC value, such as `DateTime.utc(2030, 1, 1)`, if you want the numbers to be read as UTC. + +### Schedule from an endpoint + +Inside an endpoint you reach the server through `session.serverpod`, so you can schedule a call in response to a request: + +```dart +class OrderEndpoint extends Endpoint { + Future placeOrder(Session session, Order order) async { + // ... store the order ... + + // Send a follow-up an hour from now. + await session.serverpod.futureCalls + .callWithDelay(const Duration(hours: 1)) + .example + .doWork(); + } +} +``` + +## Cancel scheduled calls + +Give a call an `identifier` when you schedule it so you can cancel it later. The same identifier can be applied to several calls. + +```dart +await pod.futureCalls + .callWithDelay( + const Duration(hours: 1), + identifier: 'an-identifying-string', + ) + .example + .doWork(); +``` + +Cancel every not-yet-run call that shares an identifier: + +```dart +await pod.futureCalls.cancel('an-identifying-string'); +``` + +Calls that share an identifier run independently. The identifier only groups them so you can cancel them together, and cancelling affects only calls that have not run yet. + +## Inspect scheduled calls + +Scheduled calls are stored in the `serverpod_future_call` database table. You can query it to see what is pending, which helps when debugging why a call did or did not run. + +## Execution timeout + +Future calls have no execution timeout: a call runs until its method returns. If a call needs a time limit, enforce it inside the method yourself, for example with `Future.timeout` around the work. + +## Related + +- [Recurring tasks](recurring-tasks): run a call on a repeating schedule. +- [Configuration](configuration): concurrency, scan interval, and broken-call handling. +- [Sessions](../endpoints-and-apis/sessions): the session a future call receives when it runs. diff --git a/docs/06-concepts/06-scheduling/02-recurring-task.md b/docs/06-concepts/06-scheduling/02-recurring-task.md deleted file mode 100644 index 34289b9e..00000000 --- a/docs/06-concepts/06-scheduling/02-recurring-task.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -description: Recurring tasks in Serverpod re-run at a fixed interval by scheduling the next future call from within the current one, cron-style. ---- - -# Recurring Task - -The recommended way to achieve cron-like scheduling is by scheduling a future call inside another. -To set this up, extend the `FutureCall` class and define two methods. - -```dart -import 'package:serverpod/serverpod.dart'; - -class ExampleFutureCall extends FutureCall { - Future doWork(Session session, int input) async { - await _doWork(session, input); - } - - Future _doWork(Session session, int input) async { - session.log('Working with input $input'); - } -} -``` - -Next, generate the code for your future call: - -```bash -$ serverpod generate -``` - -:::info -Code is only generated for the public method while the private method contains the logic to be invoked as a recurring task. -::: - -Next, import the generated `endpoints.dart` file and schedule the recurring future call using the generated code: - -```dart -import 'package:serverpod/serverpod.dart'; -import 'generated/endpoints.dart'; - -class ExampleFutureCall extends FutureCall { - Future doWork(Session session, int input) async { - await session.serverpod.futureCalls - .callWithDelay(const Duration(minutes: 20)) - .example - .doWork(input + 1); - - await _doWork(session, input); - } - - Future _doWork(Session session, int input) async { - session.log('Working with input $input'); - } -} -``` - -Now when you schedule the `doWork` future call, it will continuously invoke `_doWork` at an interval of 20 minutes. - -```dart -import 'package:serverpod/serverpod.dart'; - -import 'src/generated/protocol.dart'; -import 'src/generated/endpoints.dart'; - -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); - - await pod.start(); - await pod.futureCalls.callWithDelay(Duration(minutes: 20)).example.doWork(2); -} -``` diff --git a/docs/06-concepts/06-scheduling/03-inheritance.md b/docs/06-concepts/06-scheduling/03-inheritance.md deleted file mode 100644 index 0ea12178..00000000 --- a/docs/06-concepts/06-scheduling/03-inheritance.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -description: FutureCall inheritance extends or overrides future call classes from other Serverpod modules, including abstract base classes exposed through subclasses. ---- - -# Inheritance - -Inheritance gives you the possibility to modify the behavior of `FutureCall` classes defined in other Serverpod modules. If the parent `FutureCall` class was marked as `abstract`, no code is generated for it. - -Currently, there are the following possibilities to extend another `FutureCall` class: - -## Inheriting from a `FutureCall` class - -Given an existing `FutureCall` class, it is possible to extend or modify its behavior while retaining the already exposed methods. - -```dart -import 'package:serverpod/serverpod.dart'; - -class Greeter extends FutureCall { - Future hello(Session session, String name) async { - session.log('Hello $name'); - } -} - -class MyGreeter extends Greeter { - Future bye(Session session, String name) async { - session.log('Bye $name'); - } -} -``` - -The generated server code will now be able to access both `Greeter` and `MyGreeter`. -Whereas the `Greeter` only exposes the original `hello` method, `MyGreeter` now exposes both the inherited `hello` and its own `bye` methods. - -## Inheriting from a `FutureCall` class marked `abstract` - -Future calls marked as `abstract` are not added to the server. But if they are subclassed, their methods will be exposed through the subclass. - -```dart -import 'package:serverpod/serverpod.dart'; - -abstract class Greeter extends FutureCall { - Future hello(Session session, String name) async { - session.log('Hello $name'); - } -} - -class MyGreeter extends Greeter {} -``` - -Since `Greeter` is `abstract`, it will not be added to the server. However, `MyGreeter` will expose a single `hello` method. - -:::info -Serverpod modules can expose future calls to users with `abstract` `FutureCall`. Code is only generated on the current project that extends the abstract future call. -::: - -### Extending an `abstract` `FutureCall` class - -In the above example, the `MyGreeter` only exposed the inherited `hello` method. It can be further extended with custom methods like this: - -```dart -import 'package:serverpod/serverpod.dart'; - -class MyGreeter extends Greeter { - Future bye(Session session, String name) async { - session.log('Bye $name'); - } -} -``` - -In this case, it will expose both a `hello` and a `bye` method. - -### Overriding future call methods - -It is possible to override methods of the superclass. This can be useful when you want to modify the behavior of specific methods but preserve the rest. - -```dart -import 'package:serverpod/serverpod.dart'; - -abstract class Greeter extends FutureCall { - Future hello(Session session, String name) async { - session.log('Hello $name'); - } -} - -class ExcitedGreeter extends Greeter { - @override - Future hello(Session session, String name) async { - session.log('Hello $name!!!'); - } -} -``` - -Since `Greeter` is `abstract`, it will not be exposed on the server. The `ExcitedGreeter` will expose a single `hello` method, overriding the superclass implementation by adding `!!!` to the output. - -This way, you can modify the behavior of future call methods while still sharing the implementation through calls to `super`. Be aware that the method signature has to be compatible with the base class per Dart's rules, meaning you can add optional parameters, but can not add required parameters or change the return type. diff --git a/docs/06-concepts/06-scheduling/03-recurring-tasks.md b/docs/06-concepts/06-scheduling/03-recurring-tasks.md new file mode 100644 index 00000000..63eba4b2 --- /dev/null +++ b/docs/06-concepts/06-scheduling/03-recurring-tasks.md @@ -0,0 +1,103 @@ +--- +description: A recurring task runs a future call on a repeating schedule, set with callRecurring using either a fixed interval or a cron expression. +--- + +# Recurring tasks + +A recurring task runs the same future call again and again on a schedule, such as a nightly cleanup or an hourly sync. Schedule one with `callRecurring`, choosing either a fixed interval or a [cron](https://en.wikipedia.org/wiki/Cron) expression. Serverpod schedules the next run automatically, so you set it up once. + +Define the future call the same way as any other, then schedule it with `callRecurring`. Give recurring calls an `identifier` so you can stop them later. + +```dart +import 'package:serverpod/serverpod.dart'; + +class ExampleFutureCall extends FutureCall { + Future cleanUp(Session session) async { + session.log('Running the recurring cleanup.'); + } +} +``` + +## Repeat on a fixed interval + +Use `every` to run the call once per interval. The first run happens one interval from now, unless you pass a `start` time. + +```dart +await pod.futureCalls + .callRecurring(identifier: 'nightly-cleanup') + .every(const Duration(hours: 24)) + .example + .cleanUp(); +``` + +Pass `start` to set when the first run happens. Later runs follow one interval apart from it: + +```dart +await pod.futureCalls + .callRecurring(identifier: 'nightly-cleanup') + .every(const Duration(hours: 24), start: DateTime.now().toUtc()) + .example + .cleanUp(); +``` + +A `start` in the past runs the call at the next scan, then continues on the interval from there. To pin a task to a wall-clock time such as 02:00 every day, use a cron expression instead. + +If the server is down when a run is due, that run happens once when the server is back, then the schedule jumps ahead to the next interval boundary. The intervals missed while the server was down do not pile up and fire all at once. + +## Repeat on a cron schedule + +Use `cron` for calendar-based schedules that a fixed interval cannot express, such as "every day at 02:00" or "every Monday". A cron expression is a short text format of five fields, `minute hour day-of-month month day-of-week`, where `*` means "every". Serverpod also accepts a sixth field, written first, for seconds. + +```dart +await pod.futureCalls + .callRecurring(identifier: 'daily-report') + .cron('0 2 * * *') // Every day at 02:00 UTC. + .example + .cleanUp(); +``` + +Cron times are interpreted in UTC, so `'0 2 * * *'` runs at 02:00 UTC, not in the server's local time. The `Cron` parser is available from `package:serverpod/serverpod.dart` if you want to work with expressions directly. An invalid expression throws a `CronFormatException` when you schedule the call. + +## Stop a recurring task + +A recurring task keeps rescheduling until you cancel it by its identifier: + +```dart +await pod.futureCalls.cancel('nightly-cleanup'); +``` + +This is why recurring calls should always be given an identifier when scheduled. + +## Failure behavior + +Serverpod schedules the next run before it invokes the current one, so a recurring task keeps going even if one run throws. A failed run is logged and skipped, and the schedule continues. This is the opposite of a one-off call, which is not retried after a failure. + +## Reschedule manually + +For an interval that changes from run to run, schedule the next call from inside the current one instead of using `callRecurring`. The call reschedules itself with whatever delay you compute: + +```dart +class ExampleFutureCall extends FutureCall { + Future poll(Session session, int backoffSeconds) async { + await _poll(session); + + // Schedule the next run with a delay computed from this run. + await session.serverpod.futureCalls + .callWithDelay(Duration(seconds: backoffSeconds * 2)) + .example + .poll(backoffSeconds * 2); + } + + Future _poll(Session session) async { + // ... the work ... + } +} +``` + +Because this pattern reschedules from inside the method, a run that throws before it schedules the next one stops the loop. Prefer `callRecurring` for fixed schedules. + +## Related + +- [Future calls](future-calls): defining a call and scheduling one-off runs. +- [Configuration](configuration): how often the server checks for due calls. +- [Server roles](../server-fundamentals/running-your-server#choose-a-server-role): which roles run scheduled work. diff --git a/docs/06-concepts/06-scheduling/04-configuration.md b/docs/06-concepts/06-scheduling/04-configuration.md deleted file mode 100644 index 8fc9c021..00000000 --- a/docs/06-concepts/06-scheduling/04-configuration.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -description: Future call configuration in Serverpod sets concurrency limits, the scan interval, and broken-call handling through YAML or environment variables. ---- - -# Configuration - -Future calls can be configured using options defined in the configuration files or environment variables. For a detailed list of configuration options, refer to the [Configuration reference](../lookups/configuration-reference). - -```yaml -futureCallExecutionEnabled: true - -futureCall: - concurrencyLimit: 1 # default - scanInterval: 5000 # default, in milliseconds -``` - -### Enable or disable future call execution - -This option allows you to enable or disable the execution of future calls. By default, it is set to `true`. You might want to disable future call execution in environments where you don't want background tasks to run, such as during testing or in a staging environment where you want to focus on API behavior without triggering scheduled tasks. - -```yaml -futureCallExecutionEnabled: false -``` - -### Concurrency limit - -This option sets the maximum number of future calls that can run concurrently. By default, it is set to `1`. Configuring this is useful if you have resource-intensive tasks and want to avoid overloading your server. For example, in a production environment, you might want to tune this value to ensure that not all of the server's resources are allocated to future calls, leaving room for other critical tasks. - -Setting this value to a negative number or `null` removes the limitation, allowing an unlimited number of concurrent future calls. However, this should be used with caution as it can lead to resource exhaustion. - -```yaml -futureCall: - concurrencyLimit: 5 # Adjust this value based on your server's capacity -``` - -### Scan interval - -This option determines how often the system scans for future calls to execute, in milliseconds. The default value is `5000` (5 seconds). Adjusting this interval can help balance responsiveness and resource usage. For example, reducing the interval can make future calls execute closer to their scheduled time, while increasing it can reduce database load in environments with limited resources. - -```yaml -futureCall: - scanInterval: 2000 # Adjust this value based on your server's responsiveness needs -``` - -## Managing broken future calls - -Scheduled future calls can become broken if, before they run, the server is restarted and: - -- The method of a future call spec class is removed, leading to the removal of the previous generated future call execution class. -- The signature of a future call method is changed in a way that will lead to a generated model that fails to deserialize the stored JSON. -- The model that is used as a parameter to a future call method is changed in a way that will lead to failure in the stored JSON deserialization. - -The above cases will lead to runtime errors when trying to execute the future call. Although ensuring backwards compatibility is the responsibility of the developer, Serverpod exposes tools to prevent or remove broken future calls. - -### Check broken calls - -By default, the server will perform a check for broken future calls on startup if there are less than 1000 future calls scheduled in the database. - -This check can be controlled using the `checkBrokenCalls` configuration option. If it is set to `true`, the server will perform a check for broken future calls on startup regardless of the number of calls. If it is set to `false`, the server will not perform a check for broken future calls on startup. - -```yaml -futureCall: - checkBrokenCalls: false -``` - -:::tip -The future calls check can be used with the maintenance role to programmatically validate that the server can be restarted without breaking future calls. The process will exit normally, but logs can be inspected to verify that no broken future calls were found. - -```bash -$ dart run bin/main.dart --role maintenance -``` - -::: - -### Delete broken calls - -When detecting broken future calls, the server will log a warning, but will not delete them by default. This behavior can be changed by setting the `deleteBrokenCalls` configuration option to `true` (default is `false`). - -This configuration is only valid if the check is executed (either automatically or through explicitly enabling `checkBrokenCalls`). - -```yaml -futureCall: - deleteBrokenCalls: false -``` diff --git a/docs/06-concepts/06-scheduling/04-inheritance.md b/docs/06-concepts/06-scheduling/04-inheritance.md new file mode 100644 index 00000000..8a235c6a --- /dev/null +++ b/docs/06-concepts/06-scheduling/04-inheritance.md @@ -0,0 +1,91 @@ +--- +description: FutureCall inheritance shares methods between classes, exposes a module's future calls through an abstract base, and allows overrides. +--- + +# Inheritance + +A `FutureCall` class can extend another and inherit its future-call methods. You can use this to share common logic across your own classes, or to run future calls that a [module](../server-fundamentals/modules) exposes to the projects that depend on it. A subclass keeps every method it inherits and can add new methods or override inherited ones. + +## Extend a class + +When one `FutureCall` extends another, the generated code exposes both the inherited methods and the new ones. + +```dart +import 'package:serverpod/serverpod.dart'; + +class Greeter extends FutureCall { + Future hello(Session session, String name) async { + session.log('Hello $name'); + } +} + +class MyGreeter extends Greeter { + Future bye(Session session, String name) async { + session.log('Bye $name'); + } +} +``` + +The `Greeter` class exposes `hello`, and `MyGreeter` exposes both the inherited `hello` and its own `bye`. + +## Expose future calls from a module + +Mark the parent `abstract` when you want it to define methods without being scheduled on its own. Serverpod generates no accessor for an abstract `FutureCall`, so the methods become available only through a concrete subclass. + +```dart +import 'package:serverpod/serverpod.dart'; + +abstract class Greeter extends FutureCall { + Future hello(Session session, String name) async { + session.log('Hello $name'); + } +} + +class MyGreeter extends Greeter {} +``` + +Here `Greeter` is not scheduled directly, and `MyGreeter` exposes the inherited `hello`. + +This is the pattern a module uses to hand future calls to the projects that depend on it. The module ships an abstract `Greeter`, and the consuming project defines a concrete subclass such as `MyGreeter`. The code is generated in the consuming project, so `pod.futureCalls` there exposes the module's `hello` method through the local subclass. + +A concrete subclass can still add its own methods on top of the inherited ones: + +```dart +class MyGreeter extends Greeter { + Future bye(Session session, String name) async { + session.log('Bye $name'); + } +} +``` + +This `MyGreeter` exposes both `hello` and `bye`. + +## Override an inherited method + +A subclass can override an inherited method to change its behavior. An override replaces the parent's implementation: + +```dart +import 'package:serverpod/serverpod.dart'; + +abstract class Greeter extends FutureCall { + Future hello(Session session, String name) async { + session.log('Hello $name'); + } +} + +class ExcitedGreeter extends Greeter { + @override + Future hello(Session session, String name) async { + session.log('Hello $name!!!'); + } +} +``` + +Here `ExcitedGreeter` exposes a single `hello` that logs `Hello $name!!!`. To build on the parent's behavior instead of replacing it, call `super.hello(session, name)` from inside the override. + +The override must keep a signature compatible with the base method, following Dart's own rules: you can add optional parameters, but you cannot add required parameters or change the return type. + +## Related + +- [Future calls](future-calls): defining and scheduling a call. +- [Modules](../server-fundamentals/modules): sharing code between projects. diff --git a/docs/06-concepts/06-scheduling/05-configuration.md b/docs/06-concepts/06-scheduling/05-configuration.md new file mode 100644 index 00000000..2d346446 --- /dev/null +++ b/docs/06-concepts/06-scheduling/05-configuration.md @@ -0,0 +1,105 @@ +--- +description: Future call settings cover execution, concurrency, the scan interval, and broken-call handling, set in config files or environment variables. +--- + +# Configuration + +You configure future calls in your Serverpod config files or through environment variables. The options and their environment-variable names are listed in full in the [Configuration reference](../lookups/configuration-reference). + +| Option | Default | Controls | +| --- | --- | --- | +| `futureCallExecutionEnabled` | `true` | Whether this server runs future calls at all. | +| `futureCall.concurrencyLimit` | `1` | How many calls may run at once. | +| `futureCall.scanInterval` | `5000` | How often, in milliseconds, the server checks for due calls. | +| `futureCall.checkBrokenCalls` | unset | Whether to scan for broken calls on startup. | +| `futureCall.deleteBrokenCalls` | `false` | Whether to delete broken calls that are found. | + +```yaml +futureCallExecutionEnabled: true + +futureCall: + concurrencyLimit: 1 # default + scanInterval: 5000 # default, in milliseconds +``` + +## Execution options + +### Enable or disable execution + +The `futureCallExecutionEnabled` option turns future call execution on or off for a server. It is `true` by default. Set it to `false` in environments where background tasks should not run, such as a staging server where you want to test API behavior without triggering scheduled work. + +```yaml +futureCallExecutionEnabled: false +``` + +### Concurrency limit + +The `concurrencyLimit` option sets how many future calls may run at the same time. The default is `1`, meaning calls run one after another. Raise it to run more calls in parallel, or keep it small so future calls do not crowd out other work on a busy server. + +Set it to `0` or a negative number to remove the limit entirely, allowing unlimited concurrent calls. Use this with care, since a burst of due calls can then exhaust the server's resources. + +```yaml +futureCall: + concurrencyLimit: 5 +``` + +### Scan interval + +The `scanInterval` option sets how often, in milliseconds, the server checks the database for calls that are due. The default is `5000` (5 seconds). A shorter interval runs calls closer to their scheduled time. A longer one reduces database load. + +```yaml +futureCall: + scanInterval: 2000 +``` + +## Broken future calls + +A scheduled call becomes broken when the code needed to run it no longer matches what was stored. This happens if, before the call runs, you restart the server after: + +- Removing the method the call points to. +- Changing a future call method's signature so the stored parameters no longer deserialize. +- Changing a model used as a parameter so the stored data no longer deserializes. + +A broken call fails at runtime when the server tries to execute it. You are responsible for keeping calls backward compatible, but Serverpod can detect and remove broken calls for you. + +### Check for broken calls + +On startup the server can scan for broken calls. Whether it does depends on `checkBrokenCalls`: + +- **Unset (the default):** the check runs only when the table holds fewer than 1000 future calls, so a large backlog does not slow startup. +- **`true`:** the check always runs, regardless of how many calls are scheduled. +- **`false`:** the check never runs. + +```yaml +futureCall: + checkBrokenCalls: true +``` + +:::note +The `maintenance` role also runs this check. Started in that role, the server checks for broken calls, runs every currently-due future call once, and then exits. This is how future calls get processed in environments that do not run a persistent server, such as serverless hosting. See [server roles](../server-fundamentals/running-your-server#choose-a-server-role). + +```bash +dart run bin/main.dart --role maintenance +``` + +The process exits normally whether or not broken calls are found, so read the result from the logs. +::: + +:::warning +This role executes every due future call, so it is not a dry check. Do not run it against a production database to see what would happen. +::: + +### Delete broken calls + +When the check finds broken calls it logs a warning but does not delete them. Set `deleteBrokenCalls` to `true` to delete them instead. Deletion only happens when the check actually runs. + +```yaml +futureCall: + deleteBrokenCalls: true +``` + +## Related + +- [Future calls](future-calls): defining and scheduling a call. +- [Configuration reference](../lookups/configuration-reference): every key with its environment variable. +- [Server roles](../server-fundamentals/running-your-server#choose-a-server-role): the maintenance role used above. diff --git a/docs/06-concepts/06-scheduling/05-legacy.md b/docs/06-concepts/06-scheduling/05-legacy.md deleted file mode 100644 index 87c2549e..00000000 --- a/docs/06-concepts/06-scheduling/05-legacy.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -description: The legacy string-based API for registering and scheduling future calls. Prefer the type-safe API for new code. ---- - -# Legacy - -:::warning -This approach is error prone since it involves manually registering and scheduling future calls using string identifiers. The recommended way to interact with the future calls feature is through the [type-safe API](setup). -::: - -To create a future call, extend the `FutureCall` class and override the `invoke` method. The method takes two params: the first is a [`Session`](../endpoints-and-apis/sessions) object and the second is an optional serializable model ([See models](../data-and-the-database/models)). - -```dart -import 'package:serverpod/serverpod.dart'; - -class ExampleFutureCall extends FutureCall { - @override - Future invoke(Session session, MyModelEntity? object) async { - // Do something interesting in the future here. - } -} -``` - -Register the future call in the `run` function in your `server.dart` file by calling `registerFutureCall` with an instance of the class and a globally unique name string. The name is used to invoke the future call later. - -```dart -void run(List args) async { - final pod = Serverpod( - args, - Protocol(), - Endpoints(), - ); - - ... - - pod.registerFutureCall(ExampleFutureCall(), 'exampleFutureCall'); - - ... -} -``` - -With the future call registered, you can schedule it using either `futureCallWithDelay` or `futureCallAtTime` depending on your needs. - -Invoke the future call 1 hour from now by calling `futureCallWithDelay`. - -```dart -await session.serverpod.futureCallWithDelay( - 'exampleFutureCall', - data, - const Duration(hours: 1), -); -``` - -Invoke the future call at a specific time and/or date in the future by calling `futureCallAtTime`. - -```dart -await session.serverpod.futureCallAtTime( - 'exampleFutureCall', - data, - DateTime(2030, 1, 1), -); -``` - -:::note -`data` is an object created from a class defined in one of your yaml model files and must match the type expected by the future call. `data` may also be `null` if you don't need it. -::: - -When registering a future call, it is also possible to give it an `identifier` so that it can be referenced later. The same identifier can be applied to multiple future calls. - -```dart -await session.serverpod.futureCallWithDelay( - 'exampleFutureCall', - data, - const Duration(hours: 1), - identifier: 'an-identifying-string', -); -``` - -This identifier can then be used to cancel all future calls registered with said identifier. - -```dart -await session.serverpod.cancelFutureCall('an-identifying-string'); -``` diff --git a/docs/06-concepts/06-scheduling/06-legacy.md b/docs/06-concepts/06-scheduling/06-legacy.md new file mode 100644 index 00000000..91d2e127 --- /dev/null +++ b/docs/06-concepts/06-scheduling/06-legacy.md @@ -0,0 +1,84 @@ +--- +description: The older string-based API for registering and scheduling future calls. Prefer the type-safe API for new code. +--- + +# Legacy + +Before the type-safe API, future calls were registered and scheduled using string names. This older API still works, but the scheduling and cancellation methods (`futureCallWithDelay`, `futureCallAtTime`, and `cancelFutureCall`) are deprecated in Serverpod 4.0 and produce compiler warnings. Use the [type-safe API](future-calls) for new code. + +:::warning +This approach is error prone because it relies on string names that the compiler cannot check. The [type-safe API](future-calls) is the recommended way to work with future calls. +::: + +To create a future call, extend `FutureCall` and override the `invoke` method. The type parameter is the model passed to the call, so `FutureCall` receives a `MyModelEntity`. The first parameter is a [`Session`](../endpoints-and-apis/sessions). The second is that model, or `null` if the call needs no data. + +```dart +import 'package:serverpod/serverpod.dart'; + +class MyFutureCall extends FutureCall { + @override + Future invoke(Session session, MyModelEntity? object) async { + // Do something interesting in the future here. + } +} +``` + +Register the call in the `run` function in your `server.dart` file with `registerFutureCall`, passing an instance and a globally unique name. The name is used to schedule the call later. Registration is not deprecated. It remains the way to register a legacy future call. + +```dart +void run(List args) async { + final pod = Serverpod( + args, + Protocol(), + Endpoints(), + ); + + pod.registerFutureCall(MyFutureCall(), 'myFutureCall'); + + await pod.start(); +} +``` + +With the call registered, schedule it with `futureCallWithDelay` or `futureCallAtTime`. Both take the registered name and a data object, which is an instance of a class from one of your model files and must match the type the call expects. The data may be `null` if the call needs none. + +Run the call one hour from now: + +```dart +await session.serverpod.futureCallWithDelay( + 'myFutureCall', + data, + const Duration(hours: 1), +); +``` + +Run the call at a specific time: + +```dart +await session.serverpod.futureCallAtTime( + 'myFutureCall', + data, + DateTime(2030, 1, 1), +); +``` + +Pass an `identifier` when you schedule a call so you can cancel it later. The same identifier can be applied to several calls. + +```dart +await session.serverpod.futureCallWithDelay( + 'myFutureCall', + data, + const Duration(hours: 1), + identifier: 'an-identifying-string', +); +``` + +Cancel every not-yet-run call scheduled with that identifier: + +```dart +await session.serverpod.cancelFutureCall('an-identifying-string'); +``` + +## Related + +- [Future calls](future-calls): the type-safe API to use instead. +- [Models](../data-and-the-database/models): defining the data a legacy call receives.