From 98977f27030bea4ed2a2b6c671f9de60d4f48d1b Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Mon, 27 Jul 2026 10:47:39 +0100 Subject: [PATCH 1/6] docs: Rework the Scheduling section for 4.0 --- .../01-your-serverpod-project.md | 2 +- .../02-running-your-server.md | 2 +- .../02-endpoints-and-apis/02-sessions.md | 8 +- docs/06-concepts/06-scheduling/01-overview.md | 35 +++++ docs/06-concepts/06-scheduling/01-setup.md | 140 ------------------ .../06-scheduling/02-future-calls.md | 128 ++++++++++++++++ .../06-scheduling/02-recurring-task.md | 74 --------- .../06-scheduling/03-inheritance.md | 95 ------------ .../06-scheduling/03-recurring-tasks.md | 95 ++++++++++++ .../06-scheduling/04-configuration.md | 84 ----------- .../06-scheduling/04-inheritance.md | 86 +++++++++++ .../06-scheduling/05-configuration.md | 95 ++++++++++++ docs/06-concepts/06-scheduling/05-legacy.md | 83 ----------- docs/06-concepts/06-scheduling/06-legacy.md | 79 ++++++++++ 14 files changed, 524 insertions(+), 482 deletions(-) create mode 100644 docs/06-concepts/06-scheduling/01-overview.md delete mode 100644 docs/06-concepts/06-scheduling/01-setup.md create mode 100644 docs/06-concepts/06-scheduling/02-future-calls.md delete mode 100644 docs/06-concepts/06-scheduling/02-recurring-task.md delete mode 100644 docs/06-concepts/06-scheduling/03-inheritance.md create mode 100644 docs/06-concepts/06-scheduling/03-recurring-tasks.md delete mode 100644 docs/06-concepts/06-scheduling/04-configuration.md create mode 100644 docs/06-concepts/06-scheduling/04-inheritance.md create mode 100644 docs/06-concepts/06-scheduling/05-configuration.md delete mode 100644 docs/06-concepts/06-scheduling/05-legacy.md create mode 100644 docs/06-concepts/06-scheduling/06-legacy.md 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..765bc657 --- /dev/null +++ b/docs/06-concepts/06-scheduling/01-overview.md @@ -0,0 +1,35 @@ +--- +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). 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). + +## In this section + +- **[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. 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..1cb51865 --- /dev/null +++ b/docs/06-concepts/06-scheduling/02-future-calls.md @@ -0,0 +1,128 @@ +--- +description: Define a future call, schedule it after a delay or at a specific time with the type-safe API, and cancel scheduled calls 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. Scheduling a call before the server has started throws an exception. +::: + +## 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. 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..5f7416a5 --- /dev/null +++ b/docs/06-concepts/06-scheduling/03-recurring-tasks.md @@ -0,0 +1,95 @@ +--- +description: Run a future call on a repeating schedule 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. The `start` value is itself the first run time, and later runs follow one interval apart from it, so use a time at or near now rather than one far in the future: + +```dart +await pod.futureCalls + .callRecurring(identifier: 'nightly-cleanup') + .every(const Duration(hours: 24), start: DateTime.utc(2026, 1, 1, 2)) + .example + .cleanUp(); +``` + +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". + +```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. 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..296cb116 --- /dev/null +++ b/docs/06-concepts/06-scheduling/04-inheritance.md @@ -0,0 +1,86 @@ +--- +description: Share future call methods by extending another FutureCall class, expose them from modules with an abstract base, and override inherited methods. +--- + +# 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'); + } +} +``` + +`Greeter` exposes `hello`. `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!!!'); + } +} +``` + +`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. 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..695b3070 --- /dev/null +++ b/docs/06-concepts/06-scheduling/05-configuration.md @@ -0,0 +1,95 @@ +--- +description: Configure future call execution, concurrency, the scan interval, and broken-call handling through 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 + +`futureCallExecutionEnabled` 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 + +`concurrencyLimit` 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 + +`scanInterval` 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 +``` + +Because it executes due calls, do not run this as a dry check against a production database. The process exits normally whether or not broken calls are found, so read the result from the logs. +::: + +### 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 +``` 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..bd5444a8 --- /dev/null +++ b/docs/06-concepts/06-scheduling/06-legacy.md @@ -0,0 +1,79 @@ +--- +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'); +``` From 7ed76305147bcaa3692c93f8d437129da1a4364b Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Mon, 27 Jul 2026 15:43:00 +0100 Subject: [PATCH 2/6] docs: Rework the Operations section for 4.0 --- cloud_docs/concepts/logs.md | 2 + cloud_docs/guides/redis.md | 4 +- .../02-running-your-server.md | 26 ++- .../03-configuration.md | 2 +- .../02-endpoints-and-apis/02-sessions.md | 2 +- .../02-endpoints-and-apis/06-caching.md | 153 ++++++++++++++ ...{06-file-uploads.md => 07-file-uploads.md} | 0 ...eritance.md => 08-endpoint-inheritance.md} | 0 ...bility.md => 09-backward-compatibility.md} | 0 ...iddleware.md => 10-endpoint-middleware.md} | 0 ...tp-calls.md => 11-configure-http-calls.md} | 0 docs/06-concepts/07-operations/01-caching.md | 123 ----------- docs/06-concepts/07-operations/01-overview.md | 24 +++ docs/06-concepts/07-operations/02-logging.md | 123 ++++++----- .../07-operations/03-health-checks.md | 179 +++++++--------- .../07-operations/04-security-and-tls.md | 38 ++-- .../07-operations/05-exception-monitoring.md | 72 +++++++ .../07-operations/05-experimental-features.md | 198 ------------------ .../08-testing/03-advanced-examples.md | 68 ++++++ .../lookups/configuration-reference.md | 2 +- 20 files changed, 506 insertions(+), 510 deletions(-) create mode 100644 docs/06-concepts/02-endpoints-and-apis/06-caching.md rename docs/06-concepts/02-endpoints-and-apis/{06-file-uploads.md => 07-file-uploads.md} (100%) rename docs/06-concepts/02-endpoints-and-apis/{07-endpoint-inheritance.md => 08-endpoint-inheritance.md} (100%) rename docs/06-concepts/02-endpoints-and-apis/{08-backward-compatibility.md => 09-backward-compatibility.md} (100%) rename docs/06-concepts/02-endpoints-and-apis/{09-endpoint-middleware.md => 10-endpoint-middleware.md} (100%) rename docs/06-concepts/02-endpoints-and-apis/{10-configure-http-calls.md => 11-configure-http-calls.md} (100%) delete mode 100644 docs/06-concepts/07-operations/01-caching.md create mode 100644 docs/06-concepts/07-operations/01-overview.md create mode 100644 docs/06-concepts/07-operations/05-exception-monitoring.md delete mode 100644 docs/06-concepts/07-operations/05-experimental-features.md diff --git a/cloud_docs/concepts/logs.md b/cloud_docs/concepts/logs.md index 7595185e..b3f40cce 100644 --- a/cloud_docs/concepts/logs.md +++ b/cloud_docs/concepts/logs.md @@ -132,6 +132,8 @@ Two environment variables control session logging. With a database enabled (the To change either default, set the value with `scloud variable set`. See [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for variable management. +For what the server records, which tables it writes to, and how retention works, see [Logging](https://docs.serverpod.dev/concepts/operations/logging) in the framework documentation. + ## Troubleshooting **Invalid timestamp format.** Use ISO 8601 form (`YYYY-MM-DDTHH:MM:SSZ`) or a supported duration string (`5m`, `2h`, `1d`). diff --git a/cloud_docs/guides/redis.md b/cloud_docs/guides/redis.md index c655baa9..ae9105a0 100644 --- a/cloud_docs/guides/redis.md +++ b/cloud_docs/guides/redis.md @@ -73,7 +73,7 @@ After a successful deploy, your server will use Upstash for Redis-backed caching ## Related -- [Serverpod configuration](https://docs.serverpod.dev/concepts/configuration) for Redis options and environment variables. -- [Serverpod caching](https://docs.serverpod.dev/concepts/caching) for local and Redis-backed caches. +- [Serverpod configuration](https://docs.serverpod.dev/concepts/server-fundamentals/configuration) for Redis options and environment variables. +- [Serverpod caching](https://docs.serverpod.dev/concepts/endpoints-and-apis/caching) for local and Redis-backed caches. - [Upstash: Connect your client](https://upstash.com/docs/redis/howto/connectclient) for connection details and TLS. - [Passwords, secrets, and environment variables](/cloud/concepts/passwords-secrets-env-vars) for how Serverpod Cloud injects passwords and variables. 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..3897dd9c 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 @@ -75,9 +75,33 @@ 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). -- **`serverless`** serves requests only. Future calls and health checks are disabled, which fits platforms that start and stop instances on demand. +- **`serverless`** serves requests only. Future calls and [health metric collection](../operations/health-checks#health-metrics) are disabled, which fits platforms that start and stop instances on demand. The health probes still answer. - **`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. +## Run code on shutdown + +Register a shutdown task to do cleanup work when the server stops, such as flushing state or releasing an external resource. Tasks run after the server stops accepting requests but before the database and Redis connections close, so they can still use them. + +:::warning +Shutdown tasks are an experimental API and can change in a breaking way in any minor release. +::: + +Register them on the `Serverpod` instance in `lib/server.dart`, where you create it: + +```dart +pod.experimental.shutdownTasks.addTask(#taskIdentifier, () async { + // Your shutdown logic here. +}); +``` + +Each task is registered under an identifier you choose, used in log messages and to remove the task again. Any object works; `#taskIdentifier` above is a Dart symbol, which is a convenient way to write a constant name. + +```dart +pod.experimental.shutdownTasks.removeTask(#taskIdentifier); +``` + +Registering two tasks under the same identifier throws a `StateError`. All tasks run concurrently, and the server waits for them all before shutting down. A task that throws does not stop the shutdown, but the error is logged and the process exits with a non-zero code, which a host reading exit status will treat as a failed shutdown. + ## Related - [`serverpod start` reference](../cli/commands/start): every command-line option. diff --git a/docs/06-concepts/01-server-fundamentals/03-configuration.md b/docs/06-concepts/01-server-fundamentals/03-configuration.md index c9f78df2..1f853051 100644 --- a/docs/06-concepts/01-server-fundamentals/03-configuration.md +++ b/docs/06-concepts/01-server-fundamentals/03-configuration.md @@ -338,7 +338,7 @@ experimental_features: all: true ``` -See the [experimental features documentation](../operations/experimental-features) for details. +See [Exception monitoring](../operations/exception-monitoring) for the experimental features Serverpod currently exposes. :::warning Experimental features may change or be removed in future versions. 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..7552fdf2 100644 --- a/docs/06-concepts/02-endpoints-and-apis/02-sessions.md +++ b/docs/06-concepts/02-endpoints-and-apis/02-sessions.md @@ -42,7 +42,7 @@ class ExampleEndpoint extends Endpoint { | Member | What it is | | --------------------------- | ----------------------------------------------------------------------------------------------- | | `db` | Database access. See the [CRUD docs](../data-and-the-database/database/crud). | -| `caches` | Local and distributed caching. See [caching](../operations/caching). | +| `caches` | Local and distributed caching. See [caching](./caching). | | `storage` | File storage. See [file uploads](./file-uploads). | | `messages` | Server events for real-time communication. See [server events](./server-events). | | `passwords` | Secrets from config and environment. See [configuration](../server-fundamentals/configuration). | diff --git a/docs/06-concepts/02-endpoints-and-apis/06-caching.md b/docs/06-concepts/02-endpoints-and-apis/06-caching.md new file mode 100644 index 00000000..1f673b23 --- /dev/null +++ b/docs/06-concepts/02-endpoints-and-apis/06-caching.md @@ -0,0 +1,153 @@ +--- +description: Cache objects in server memory or in Redis through session.caches to avoid repeating expensive database work, with lifetimes, groups, and explicit invalidation. +--- + +# Caching + +Some work is expensive to repeat: a query that joins several tables, a result assembled from many rows, or a value fetched from another service. Caching stores the result under a key so the next request can read it back instead of doing the work again. + +Serverpod's caches are reached from the [`Session`](./sessions) object, alongside the database and messaging. Anything you cache must be serializable: the primitives (`int`, `String`, `bool`, `double`, `DateTime`, `Duration`, `ByteData`, `UuidValue`, `Uri`, `BigInt`), collections of them, and your own [models](../data-and-the-database/models). Use a model rather than a hand-written class: one with a `toJson` stores fine and throws on read, one without fails immediately with `JsonUnsupportedObjectError`. + +## The caches + +| Cache | Where it lives | Use it for | +| --- | --- | --- | +| `session.caches.local` | Memory of the server handling this call | The default choice for cached values. | +| `session.caches.localPrio` | Memory of the same server, kept separate | Values you want to keep hot, held apart from ordinary entries. | +| `session.caches.global` | Redis, shared by every server instance | Values that must be the same on every instance. | + +There is a fourth cache, `session.caches.query`, which Serverpod manages for cacheable database queries. It is not a place to put your own objects. + +Each local cache holds at most 10,000 entries. When a cache is full, the oldest entry is dropped to make room, based on when it was inserted rather than when it was last read, so a frequently read key can still be evicted. + +## Cache an object + +Read a value with `get`, and write one with `put`: + +```dart +Future getUserData(Session session, int userId) async { + var cacheKey = 'UserData-$userId'; + + // Try the cache first. + var userData = await session.caches.local.get(cacheKey); + if (userData != null) return userData; + + // Not cached, so load it and store it for five minutes. + userData = await UserData.db.findById(session, userId); + if (userData != null) { + await session.caches.local.put( + cacheKey, + userData, + lifetime: const Duration(minutes: 5), + ); + } + + return userData; +} +``` + +The `lifetime` sets how long the entry stays valid. After it elapses, the next read misses and returns `null`. Omit `lifetime` and the entry never expires on its own, so it stays until you invalidate it or it is evicted to make room. + +Cache `DateTime` values as UTC, since that is how they come back: + +```dart +await session.caches.local.put('lastUpdate', DateTime.now().toUtc()); + +var cached = await session.caches.local.get('lastUpdate'); +``` + +Collections work the same way. Give `get` the full type, including what the collection holds: + +```dart +await session.caches.local.put('users', [alice, bob]); + +var users = await session.caches.local.get>('users'); +``` + +Serverpod generates the code to read a collection type back only when your project uses it, such as in an endpoint signature or a model field. Caching a shape that appears nowhere else in your project throws on read. + +### Load on a miss + +The `CacheMissHandler` class folds "read it, and compute it if it is missing" into one call. Serverpod stores whatever the handler returns: + +```dart +Future getUserData(Session session, int userId) async { + return session.caches.local.get( + 'UserData-$userId', + CacheMissHandler( + () async => UserData.db.findById(session, userId), + lifetime: const Duration(minutes: 5), + ), + ); +} +``` + +The call returns `null` when the handler does, and nothing is stored in that case. If several requests miss the same key at once, the local caches run the handler once and let the others wait for that result. The Redis-backed global cache runs it per request. + +## Invalidate entries + +A cached value goes stale as soon as the underlying data changes, so remove it when you write: + +```dart +await UserData.db.updateRow(session, userData); +await session.caches.local.invalidateKey('UserData-${userData.id}'); +``` + +To drop several related entries at once, put them in a group and invalidate the group: + +```dart +await session.caches.local.put( + 'UserData-$userId', + userData, + group: 'user-$userId', +); + +// Later, remove every entry in the group. +await session.caches.local.invalidateGroup('user-$userId'); +``` + +Use `containsKey` to check for an entry without reading it, and `clear` to empty the cache. + +:::warning +Groups are a local-cache feature. On `session.caches.global`, passing `group:` to `put` or calling `invalidateGroup` throws `UnimplementedError`, so code that groups entries breaks when you move it to the global cache. +::: + +## The global cache and Redis + +The global cache uses Redis, which is what makes it shared between server instances. When Redis is not configured, it falls back to an in-memory cache that belongs to a single server process. + +:::warning +The fallback applies in every run mode, production included. It exists so you can run code that uses the global cache without a Redis server during development, not as a substitute for Redis in production. In that state each instance keeps its own copy, nothing is shared between instances, and everything is lost on restart. + +The server does not warn you about this at normal logging levels, and the [readiness probe](../operations/health-checks) still passes, so confirm your Redis configuration before running more than one instance. If Redis is configured but unreachable, the readiness probe fails, so that case does get caught. +::: + +Redis counts as configured when your config has a `redis` section that is enabled. A `redis` section that omits `enabled` is treated as enabled. If the section is present but its password is missing, the server fails to start rather than falling back. + +Serverpod does not manage the size of the global cache or decide what it evicts. Redis does, according to how you have configured it, so set a memory limit and an eviction policy there if you cache a large number of objects globally. + +Serverpod Cloud does not provide a managed Redis instance, so a deployed app uses the in-memory fallback unless you connect your own. See the [Redis guide](/cloud/guides/redis) for how to set one up. + +## Send Redis commands directly + +Redis supports operations the cache API does not expose, such as atomic counters and sorted sets. You can borrow the connection Serverpod manages: + +```dart +Future incrementCounter(Session session, String counterName) async { + var command = await session.serverpod.redisController?.getConnection(); + if (command == null) return null; + + var result = await command.send_object(['INCR', 'my_app:$counterName']); + return result is int ? result : null; +} +``` + +The connection is `null` when Redis is disabled or unreachable, including when the global cache is on its in-memory fallback. Responses come back in the shape Redis defines for each command, so check the result before casting it. + +The returned `RedisCommand` is Serverpod's own connection: do not close it, and do not replace `Serverpod.redisController`. Namespace your keys so they cannot collide with Serverpod's own cache entries. + +## Related + +- [Sessions](./sessions): the object the caches hang off. +- [Server events](./server-events): the other Redis-backed feature, for messaging between instances. +- [Configuration](../server-fundamentals/configuration): where the `redis` section lives. diff --git a/docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md b/docs/06-concepts/02-endpoints-and-apis/07-file-uploads.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/06-file-uploads.md rename to docs/06-concepts/02-endpoints-and-apis/07-file-uploads.md diff --git a/docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md b/docs/06-concepts/02-endpoints-and-apis/08-endpoint-inheritance.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/07-endpoint-inheritance.md rename to docs/06-concepts/02-endpoints-and-apis/08-endpoint-inheritance.md diff --git a/docs/06-concepts/02-endpoints-and-apis/08-backward-compatibility.md b/docs/06-concepts/02-endpoints-and-apis/09-backward-compatibility.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/08-backward-compatibility.md rename to docs/06-concepts/02-endpoints-and-apis/09-backward-compatibility.md diff --git a/docs/06-concepts/02-endpoints-and-apis/09-endpoint-middleware.md b/docs/06-concepts/02-endpoints-and-apis/10-endpoint-middleware.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/09-endpoint-middleware.md rename to docs/06-concepts/02-endpoints-and-apis/10-endpoint-middleware.md diff --git a/docs/06-concepts/02-endpoints-and-apis/10-configure-http-calls.md b/docs/06-concepts/02-endpoints-and-apis/11-configure-http-calls.md similarity index 100% rename from docs/06-concepts/02-endpoints-and-apis/10-configure-http-calls.md rename to docs/06-concepts/02-endpoints-and-apis/11-configure-http-calls.md diff --git a/docs/06-concepts/07-operations/01-caching.md b/docs/06-concepts/07-operations/01-caching.md deleted file mode 100644 index c8065633..00000000 --- a/docs/06-concepts/07-operations/01-caching.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -description: Caching in Serverpod stores frequently requested objects in local server memory or a distributed Redis cache to reduce expensive database queries. ---- - -# Caching - -Accessing the database can be expensive for complex queries or if you need to run many different queries for a specific task. Serverpod makes it easy to cache frequently requested objects in the memory of your server. Any object can be cached, including primitive types (`int`, `String`, `DateTime`, `Duration`, `ByteData`, `UuidValue`), lists, maps, and serializable models. Objects can be stored in the Redis cache if your Serverpod is hosted across multiple servers in a cluster. - -:::info -Objects must be serializable to be cached. Non-serializable objects will throw an error when attempting to cache them. Most Dart types are serializable, including primitives, collections, and custom objects with `toJson`/`fromJson` methods. All objects that can be used with endpoints or the database are supported. -::: - -## Caching objects - -Caches can be accessed through the `Session` object. This is an example of an endpoint method for requesting data about a user: - -```dart -Future getUserData(Session session, int userId) async { - // Define a unique key for the UserData object - var cacheKey = 'UserData-$userId'; - - // Try to retrieve the object from the cache - var userData = await session.caches.local.get(cacheKey); - - // If the object wasn't found in the cache, load it from the database and - // save it in the cache. Make it valid for 5 minutes. - if (userData == null) { - userData = await UserData.db.findById(session, userId); - await session.caches.local.put(cacheKey, userData!, lifetime: Duration(minutes: 5)); - } - - // Return the user data to the client - return userData; -} -``` - -There are three caches where you can store your objects, all reached through the `Session` object. Two are local to the server handling the current session: a regular cache (`session.caches.local`) and a priority cache (`session.caches.localPrio`) for frequently accessed objects. The third, `session.caches.global`, is distributed across the server cluster through Redis. - -Depending on the type and number of objects that are cached in the global cache, you may want to specify custom caching rules in Redis. This is currently not handled automatically by Serverpod. - -:::info -During development, you can run code that uses the global cache without a running Redis server. If Redis is unreachable (or not configured) in a non-production run mode, the global cache falls back to an isolated in-memory cache. -::: - -### Caching primitive objects - -To cache primitive objects, call the `put` method with the object. For the `get`, specify the object type as the generic parameter, as for `SerializableModel` objects: - -```dart -await session.caches.local.put('userCount', 17, lifetime: Duration(minutes: 5)); - -var count = await session.caches.local.get('userCount'); -``` - -For `DateTime` objects, it is recommended to always cache them as UTC. Otherwise, you may get unexpected results when retrieving the object from the cache. - -```dart -var lastUpdate = DateTime.now().toUtc(); - -await session.caches.local.put('lastUpdate', lastUpdate); - -// Retrieved `DateTime` object will always be in UTC. -var cached = await session.caches.local.get('lastUpdate'); -``` - -### Caching lists and collections - -Lists and collections can also be cached directly: - -```dart -var users = [UserData(name: 'Alice'), UserData(name: 'Bob')]; - -await session.caches.local.put('users', users); - -var cachedUsers = await session.caches.local.get>('users'); -``` - -### Cache miss handler - -If you want to handle cache misses in a specific way, you can pass in a `CacheMissHandler` to the `get` method. The `CacheMissHandler` makes it possible to store an object in the cache when a cache miss occurs. - -The above example rewritten using the `CacheMissHandler`: - -```dart -Future getUserData(Session session, int userId) async { - // Define a unique key for the UserData object - var cacheKey = 'UserData-$userId'; - - // Try to retrieve the object from the cache - var userData = await session.caches.local.get( - cacheKey, - // If the object wasn't found in the cache, load it from the database and - // save it in the cache. Make it valid for 5 minutes. - CacheMissHandler( - () async => UserData.db.findById(session, userId), - lifetime: Duration(minutes: 5), - ), - ); - - // Return the user data to the client - return userData; -} -``` - -If the `CacheMissHandler` returns `null`, no object will be stored in the cache. - -## Sending custom Redis commands - -Redis supports operations that the global cache does not expose, such as atomic counters and sorted sets. If you want to use these operations, you can borrow the connection Serverpod already manages and send the command using `getConnection`: - -```dart -Future incrementCounter(Session session, String counterName) async { - var command = await session.serverpod.redisController?.getConnection(); - if (command == null) return null; - - var result = await command.send_object(['INCR', 'my_app:$counterName']); - return result is int ? result : null; -} -``` - -The connection is `null` when Redis is disabled or unreachable, including when the global cache is running on its in-memory development fallback. Responses come back in the shape Redis defines for the command, so check the result before casting it. - -The returned `RedisCommand` is Serverpod's own connection. Do not close it or replace `Serverpod.redisController`. It is also a good practice to namespace your keys so they cannot collide with Serverpod's cache entries. diff --git a/docs/06-concepts/07-operations/01-overview.md b/docs/06-concepts/07-operations/01-overview.md new file mode 100644 index 00000000..1ebe8ee8 --- /dev/null +++ b/docs/06-concepts/07-operations/01-overview.md @@ -0,0 +1,24 @@ +--- +description: Operations covers running a Serverpod server in production, seeing what it is doing through logs, proving it is healthy, securing its traffic, and catching exceptions. +--- + +# Overview + +Once your server is written, the work shifts from building features to running them. This section covers what you need after deployment: seeing what the server is doing, proving to a host that it is healthy, securing the traffic it accepts, and finding out when something breaks. + +Everything here works the same whether you deploy to [Serverpod Cloud](../../deployments/deploy-to-serverpod-cloud) or [host it yourself](../../deployments/custom-hosting/choosing-a-strategy), though the two differ in how much is set up for you. + +## What each page covers + +- **[Logging](logging)**: what the server records for every call, where those records go, and how to keep the log tables from growing without bound. +- **[Health checks](health-checks)**: the HTTP endpoints a host calls to decide whether your server is alive and ready for traffic, plus the metrics Serverpod collects about itself. +- **[Security and TLS](security-and-tls)**: how traffic to your server is encrypted, and when you need to configure that yourself. +- **[Exception monitoring](exception-monitoring)**: reporting exceptions to a monitoring service as they happen. This one is an experimental API. + +## Related + +- [Configuration](../server-fundamentals/configuration): the config files and environment variables every setting on these pages is read from. +- [Sessions](../endpoints-and-apis/sessions): the object that produces most of what ends up in your logs. +- [Caching](../endpoints-and-apis/caching): storing values in server memory or Redis. +- [Run code on shutdown](../server-fundamentals/running-your-server#run-code-on-shutdown): cleanup work when the server stops. +- [Insights](../../tools/insights): the companion app for reading logs and metrics. diff --git a/docs/06-concepts/07-operations/02-logging.md b/docs/06-concepts/07-operations/02-logging.md index 8273b5a0..8b7ebc9d 100644 --- a/docs/06-concepts/07-operations/02-logging.md +++ b/docs/06-concepts/07-operations/02-logging.md @@ -1,16 +1,27 @@ --- -description: Serverpod logging records custom messages, exceptions, and queries via the session log method and stores them in the database, with configurable retention and console output. +description: Serverpod records a log entry for calls, queries, and your own messages, stores them in the database or prints them to the console, and can purge old entries. --- # Logging -Serverpod uses the database for storing logs; this makes it easy to search for errors, slow queries, or debug messages. To log custom messages during the execution of a session, use the `log` method of the `session` object. When the session is closed, either from successful execution or by failing from throwing an exception, the messages are written to the log. By default, session log entries are written for every completed session. +Logging is how you find out what your server did after it did it: which calls ran, which queries were slow, and what failed. Serverpod records this for you, and you add your own messages on top. + +There are four kinds of record, and the difference matters for everything below: + +- **Session records** describe one unit of work, such as an endpoint call: how long it took, whether it failed, and which endpoint it hit. +- **Query records** describe individual database queries run during that session. +- **Log messages** are the entries you write yourself with `session.log`. +- **Stream messages** are the messages passed by [streaming](../endpoints-and-apis/streaming) sessions. + +## Write your own messages + +Call `log` on the [session](../endpoints-and-apis/sessions) you were given: ```dart session.log('This is working well'); ``` -You can also pass exceptions and stack traces to the `log` method or set the logging level. +Pass a level, an exception, and a stack trace when something goes wrong: ```dart session.log( @@ -21,86 +32,86 @@ session.log( ); ``` -Log entries are stored in the following tables of the database: `serverpod_log` for text messages, `serverpod_query_log` for queries, and `serverpod_session_log` for completed sessions. +Messages are collected while the session runs and written when it closes, whether it finished normally or threw. -## Controlling session logs with environment variables or configuration files +## Where logs go -You can control whether session logs are written to the database, the console, both, or neither, using environment variables or configuration files. **Environment variables take priority** over configuration file settings if both are provided. +Records are written to the database, to the console, to both, or to neither. -For the default values when environment variables are not set, see the [default behavior for session logs](#default-behavior-for-session-logs). +In the database they land in four tables: -### Environment variables +| Table | Holds | +| --- | --- | +| `serverpod_session_log` | One row per completed session. | +| `serverpod_log` | Your `session.log` messages. | +| `serverpod_query_log` | Database queries. | +| `serverpod_message_log` | Stream messages from streaming sessions. | -- `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED`: Controls whether session logs are written to the database. -- `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL`: How often to run the log cleanup job (duration string, e.g. `6h`, `24h`). Set to empty to disable automated purging. -- `SERVERPOD_SESSION_LOG_RETENTION_PERIOD`: How long to keep session log entries (duration string, e.g. `30d`, `6h`). Set to empty or omit to use the default (90 days). -- `SERVERPOD_SESSION_LOG_RETENTION_COUNT`: Maximum number of session log entries to keep. Set to empty or omit to use the default (100,000). -- `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED`: Controls whether session logs are output to the console. -- `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT`: The format for console logging (`text` or `json`). See the [Configuration reference](../lookups/configuration-reference). +The last three reference the session row, so deleting a session row removes its queries and messages with it. -### Configuration file example +:::info +The companion app [Serverpod Insights](../../tools/insights) reads and searches these tables, and can change the runtime settings described below. +::: -You can also configure logging behavior directly in the configuration file: +## Which sessions get recorded -```yaml -sessionLogs: - persistentEnabled: true # Logs are stored in the database - cleanupInterval: 6h # Run cleanup every 6 hours - retentionPeriod: 30d # Keep entries for 30 days - retentionCount: 5000 # Keep at most 5,000 entries - consoleEnabled: true # Logs are output to the console -``` +Not every session produces a row, and the default depends on the run mode. -Duration strings for the cleanup interval and retention period use the same format as in [models](../data-and-the-database/models#supported-default-values): e.g. `30d`, `6h`, `1d 2h 30min`. +In `development`, every completed session is recorded. In `staging`, `production`, and `test`, a session is recorded only when it ran longer than one second, it failed, or it produced a log, query, or message entry. Ordinary fast calls leave no row, which keeps the table to the sessions worth looking at. -## Default behavior for session logs +These thresholds are runtime settings stored in the `serverpod_runtime_settings` table, so you can change them on a running server through Insights without redeploying. They control whether all sessions are logged, whether all queries are logged, what counts as slow, and the minimum level a message must have to be kept. You can also override them per endpoint and per method. -By default, session logging behavior depends on whether the project has database support: +## Configure logging -- **When a database is present** +Session logging is configured under `sessionLogs:` in your config file for the run mode, or through environment variables. Environment variables win over the config file, key by key. - - `persistentEnabled` is set to `true`, meaning logs are stored in the database. - - `consoleEnabled` defaults to `true` in the `development` run mode and `false` in the other run modes, so local runs print to the console while deployed runs rely on the database logs unless console logging is explicitly enabled. +| Setting | Environment variable | Default | +| --- | --- | --- | +| `persistentEnabled` | `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED` | `true` when a database is configured | +| `consoleEnabled` | `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED` | `true` in `development` or when there is no database, otherwise `false` | +| `consoleLogFormat` | `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT` | `text` in `development`, otherwise `json` | +| `cleanupInterval` | `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL` | `24h`, but see [Purge old records](#purge-old-records) | +| `retentionPeriod` | `SERVERPOD_SESSION_LOG_RETENTION_PERIOD` | `90d`, but see [Purge old records](#purge-old-records) | +| `retentionCount` | `SERVERPOD_SESSION_LOG_RETENTION_COUNT` | `100000`, but see [Purge old records](#purge-old-records) | -- **When no database is present** +```yaml +sessionLogs: + persistentEnabled: true # Store records in the database + consoleEnabled: true # Also print them + cleanupInterval: 6h # Purge every 6 hours + retentionPeriod: 30d # Keep 30 days + retentionCount: 5000 # Keep at most 5,000 sessions +``` - - `persistentEnabled` is set to `false` since persistent logging requires a database. - - `consoleEnabled` is set to `true`, meaning logs are printed to the console by default. +Durations use the same format as [model default values](../data-and-the-database/models#supported-default-values), such as `30d`, `6h`, or `1d 2h 30min`. :::warning -If `persistentEnabled` is set to `true` but **no database is configured**, a `StateError` will be thrown. Persistent logging requires database support, and Serverpod ensures that misconfigurations are caught early by raising this error. +Setting `persistentEnabled` to `true` without a configured database throws a `StateError` on startup. Persistent logging needs somewhere to persist to. + +Persistent logging is also unavailable on SQLite, which cannot handle the concurrent writes it needs. The server warns and skips it. Nothing takes its place automatically, so enable `consoleEnabled` if you want records in the run modes where it defaults to off. ::: :::info -You can use the companion app **[Serverpod Insights](../../tools/insights)** to read, search, and configure the logs. +Every environment variable in the table takes a real value. Setting one to an empty string is not a way to unset it: the server fails to start. To turn a policy off, set the key to `null` in the config file instead. ::: -### Log retention and automated purging +## Purge old records -Since log entries are stored in the database when persistent logging is enabled, the logs table can grow without bound if not purged. Serverpod can automatically purge logs based on configurable retention policies to prevent unchecked storage growth. +Log tables grow with every call your server handles, so Serverpod can delete old records for you. Cleanup runs on the `cleanupInterval`, and removes session rows that are either older than `retentionPeriod` or beyond the newest `retentionCount`, whichever applies first. Deleting a session row takes its query, message, and log rows with it. -#### Default values +The `cleanupInterval` setting is the switch for the whole job. With no interval set, nothing is purged whatever the two retention values say. Purging also requires `persistentEnabled`, since there is nothing to delete otherwise. -- **Cleanup interval**: 24 hours; the cleanup job runs once per day. -- **Retention period**: 90 days; removes entries older than this. -- **Retention count**: 100,000 entries; removes entries that exceed this count. +Cleanup is triggered by log writes rather than by a timer, so a server that is not logging anything does not purge. A single pass gives up after an hour, and the next interval starts a fresh one. -If both time-based (retention period) and count-based (retention count) limits are set, entries are removed if they are either too old or beyond the maximum count, whichever is reached first. +:::warning +The three cleanup settings only fall back to their defaults when `sessionLogs` is absent from your config entirely. If you set any one session-log key, in the config file or through an environment variable, the settings you did not set resolve to null rather than to the defaults in the table above, and that policy is switched off with no warning. -:::note -Automatic cleanup is only available when persistent logging is enabled and the cleanup interval is configured. +This affects new projects: the generated `development`, `test`, and `production` configs each contain a `sessionLogs` block, so purging is off in those run modes until you set the three values explicitly. Set `cleanupInterval`, `retentionPeriod`, and `retentionCount` together whenever you configure any part of `sessionLogs`. ::: -#### Customizing retention policies - -All three settings are optional and can be set to `null` to disable the respective policy. If the `cleanupInterval` is set to `null`, no purging will run regardless of the other settings, and log tables can grow without bound until you run cleanup manually or re-enable the interval. +## Related -Configure retention and cleanup via [environment variables](#environment-variables) or the [configuration file](#configuration-file-example). For example, to keep 30 days and at most 5,000 entries, with cleanup every 6 hours: - -```yaml -sessionLogs: - persistentEnabled: true - cleanupInterval: 6h - retentionPeriod: 30d - retentionCount: 5000 -``` +- [Configuration](../server-fundamentals/configuration): the config files these settings live in. +- [Configuration reference](../lookups/configuration-reference): every session-log key with its environment variable. +- [Sessions](../endpoints-and-apis/sessions): what a session is, and the `log` method. +- [Insights](../../tools/insights): reading logs and editing runtime settings. diff --git a/docs/06-concepts/07-operations/03-health-checks.md b/docs/06-concepts/07-operations/03-health-checks.md index 918e43d9..68e2f491 100644 --- a/docs/06-concepts/07-operations/03-health-checks.md +++ b/docs/06-concepts/07-operations/03-health-checks.md @@ -1,96 +1,62 @@ --- -description: Health checks in Serverpod expose Kubernetes-style HTTP endpoints (/livez, /readyz, /startupz) for liveness, readiness, and startup probing, with support for custom health indicators and metrics. +description: Serverpod answers liveness, readiness, and startup probes at /livez, /readyz, and /startupz, supports custom health indicators, and collects health metrics about itself. --- # Health checks -Serverpod provides a complete health check system that allows you to monitor the health of your server and your dependencies through Kubernetes-style HTTP endpoints (`/livez`, `/readyz`, `/startupz`) - each with a specific purpose that helps orchestrators (like Kubernetes) make informed decisions about container lifecycle and traffic routing. +When your server runs behind a host that can restart it or route traffic away from it, that host needs a way to ask how the server is doing. Serverpod answers three such questions over HTTP, at URLs that match what container platforms like Kubernetes expect. -## Endpoints +This page covers two separate things that share the word "health": -### Liveness Probe `/livez` +- **Health probes**, the HTTP endpoints something else calls to decide whether to send you traffic or restart you. +- **Health metrics**, numbers Serverpod records about itself into your database for you to look at later. -The liveness probe answers: "Should this container be killed and restarted?". +## Health probes -- Returns `200 OK` if the server process can respond. -- Only fails if the process is fundamentally broken. -- A failed liveness check triggers a pod restart. -- Does not check dependencies (database, Redis, etc.). +The three endpoints are always available, with no setup. Every server your Serverpod instance runs answers them, so in a default development configuration they respond on the API server's port `8080`, the Insights server's `8081`, and the web server's `8082` when your project has one. Point your host at whichever port it can reach. -This endpoint is intentionally permissive. It should only fail when the process is truly unrecoverable (deadlocks, memory corruption, infinite loops). Transient issues like slow database queries or temporary network blips should not trigger restarts. +These three paths are reserved. A [web server route](../web-server/routing) registered on them would never be reached. -**Example:** - -```bash -curl http://localhost:8080/livez -# Returns: 200 OK -``` - -### Readiness Probe `/readyz` - -The readiness probe answers: "Should traffic be routed to this container?". - -- Returns `200 OK` if all dependencies are healthy. -- Returns `503 Service Unavailable` if any critical dependency is unavailable. -- Checks database connectivity, Redis connectivity (if configured), and custom health indicators. - -A failed readiness check stops traffic routing without restarting the pod. This allows the pod to recover from temporary issues without receiving extra pressure from new traffic. - -**Example:** +| Endpoint | Question it answers | Returns | +| --- | --- | --- | +| `/livez` | Is the process still working, or should it be restarted? | `200` if the server can respond at all. | +| `/readyz` | Should traffic be sent here right now? | `200` when dependencies are healthy, `503` when they are not. | +| `/startupz` | Has the server finished starting? | `200` once startup is complete. | ```bash curl http://localhost:8080/readyz -# Returns: 200 OK or 503 Service Unavailable ``` -### Startup Probe `/startupz` +**Liveness** is deliberately permissive. It only reports failure when the process is broken beyond recovery, because failing it means a restart. A slow database or a brief network problem should not restart your server, so `/livez` does not check dependencies at all. -The startup probe answers: "Has this container finished initializing?". +**Readiness** is the one that controls traffic. It checks the database, Redis when it is enabled, and any custom indicators you add. Failing readiness stops new traffic without restarting the process, which gives a struggling server room to recover. -- Returns `200 OK` once server initialization (including migrations) is complete. -- Prevents premature liveness/readiness checks during boot. -- Kubernetes waits for this to pass before starting liveness/readiness probes. +**Startup** exists so the other two are not consulted too early. While it is failing, a platform holds off its liveness and readiness probes. In practice Serverpod opens its HTTP listeners as the last step of starting, so a probe sent during startup gets a refused connection rather than a `503`. -This endpoint will determine when the pod is ready to receive traffic. While this endpoint is failing, the orchestrator will not route any traffic to the pod. +### Response format -**Example:** +The probes follow the [draft standard for health check responses](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06). -```bash -curl http://localhost:8080/startupz -# Returns: 200 OK once startup is complete -``` - -## Response format - -Health endpoints return JSON responses following the [RFC draft for Health Check Response Format](https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06). - -- **Unauthenticated requests** receive only HTTP status codes (no body) for security. -- **Authenticated requests** receive detailed JSON responses. - -The format of the response is as follows: +Requests without valid authentication get the status code and an empty body, so nothing about your dependencies is exposed publicly. An authenticated request gets the same status code plus a body. Any credential your server's authentication handler accepts unlocks the body, with no particular scope required. ```json { - "status": "pass", // or "fail" + "status": "pass", + "time": "2026-01-14T10:30:00Z", "checks": { - "database:connection": [ // The name of the check. + "database:connection": [ { - "componentId": "primary-db", // The ID of the component. - "componentType": "datastore", // The type of the component. - "status": "pass", // or "fail" - "observedValue": 12, // Optional value of the check. - "observedUnit": "ms", // Optional unit of the check. - "output": "Connection normal", // Optional output of the check. - "time": "2026-01-14T10:30:00Z" // The time of the check. + "componentType": "datastore", + "status": "pass", + "observedValue": 12, + "observedUnit": "ms", + "time": "2026-01-14T10:30:00Z" } ], - "redis:latency": [ + "redis:connection": [ { - "componentId": "cache-cluster", "componentType": "datastore", "status": "pass", - "observedValue": 3, - "observedUnit": "ms", "time": "2026-01-14T10:30:00Z" } ] @@ -98,21 +64,19 @@ The format of the response is as follows: } ``` -## Built-in health indicators - -Serverpod automatically registers health indicators based on your configuration: +When a check fails, the response also carries `notes` listing which ones. The `checks` object is left out when there is nothing to report, which is the normal case for `/livez`. -- **ServerpodStartupIndicator** - Tracks server initialization completion. -- **DatabaseHealthIndicator** - Checks database connectivity (if database is configured). -- **RedisHealthIndicator** - Checks Redis connectivity (if Redis is enabled). +### Built-in indicators -## Custom health indicators +Serverpod registers these based on your configuration: -You can add custom health indicators to check external services, microservices, or other dependencies. +- `serverpod:startup` records that the server has begun starting. +- `database:connection` checks the database, when one is configured. +- `redis:connection` checks Redis, when it is enabled. -### Creating a custom indicator +### Add your own indicator -Create a class that extends `HealthIndicator`: +Extend `HealthIndicator` to check something your server depends on, such as an external API: ```dart import 'package:serverpod/serverpod.dart'; @@ -134,13 +98,9 @@ class StripeApiIndicator extends HealthIndicator { Future check() async { final stopwatch = Stopwatch()..start(); try { - // Perform your health check await stripeClient.ping(); stopwatch.stop(); - - return pass( - observedValue: stopwatch.elapsedMilliseconds.toDouble(), - ); + return pass(observedValue: stopwatch.elapsedMilliseconds.toDouble()); } catch (e) { return fail(output: 'Stripe API unavailable: $e'); } @@ -148,9 +108,9 @@ class StripeApiIndicator extends HealthIndicator { } ``` -### Registering custom indicators +The type parameter is the type of `observedValue`, which is what the check reports alongside pass or fail. Use `output` to attach a message, as the failure branch above does. Override `componentId` when several instances of the same component exist, such as `primary-db` and `replica-db`, and the response should say which one answered. The built-in indicators leave it unset. -Register your indicators when creating the Serverpod instance: +Register it through `healthConfig` when you create the server, choosing the list by which probe should run it: ```dart final pod = Serverpod( @@ -159,42 +119,34 @@ final pod = Serverpod( Endpoints(), healthConfig: HealthConfig( cacheTtl: Duration(seconds: 2), - additionalReadinessIndicators: [ - StripeApiIndicator(), - InventoryServiceIndicator(), - ], - additionalStartupIndicators: [ - CacheWarmupIndicator(), - ], + additionalReadinessIndicators: [StripeApiIndicator()], + additionalStartupIndicators: [CacheWarmupIndicator()], ), ); ``` -### Configuration options +- `additionalReadinessIndicators` are checked by `/readyz`, so use them for dependencies that must be available to serve traffic. +- `additionalStartupIndicators` are checked by `/startupz`, so use them for work that has to finish before the server is ready at all, such as warming a cache. -The `HealthConfig` class provides the following options: +The `cacheTtl` option sets how long a result is reused before the check runs again, which keeps frequent probing from hammering your dependencies. It defaults to one second. Each indicator can set its own `timeout`, five seconds by default, so one slow check cannot hold up the whole response. -- **`cacheTtl`** - How long to cache health check results (default: 1 second). Prevents "thundering herd" during high-frequency probing. -- **`additionalReadinessIndicators`** - Custom indicators checked by `/readyz`. -- **`additionalStartupIndicators`** - Custom indicators checked by `/startupz`. +## Health metrics -Each indicator can specify its own timeout via the `timeout` getter (default: 5 seconds). This prevents slow checks from blocking the entire health endpoint. +Separately from the probes, Serverpod records numbers about itself: CPU, memory, and how long the database takes to respond. These go into the `serverpod_health_metric` and `serverpod_health_connection_info` tables, and Insights charts them. -## Health metrics collection +Collection runs once per `healthCheckInterval`, one minute by default. Setting the interval to zero turns collection off. A few conditions apply: a cycle writes nothing when the database has not been used since the last one, collection only runs in the `monolith` and `maintenance` [server roles](../server-fundamentals/running-your-server#choose-a-server-role), and it does not run on Windows. -Independently from the health check endpoints, Serverpod also collects health metrics about the server and its dependencies while running. Metrics like CPU, memory usage and response time to the database are stored in the database every minute in the `serverpod_health_metric` and `serverpod_health_connection_info` tables. Such metrics can be graphically visualized through Serverpod Insights. +Older rows are folded up rather than kept forever: minute rows become hourly after two days, and hourly rows become daily after about a month. -### Adding custom metrics +### Record your own metric -Add custom health metrics to monitor external services or internal processes. To set up your custom metrics, you must create a `HealthCheckHandler` and register it with your Serverpod. +A `HealthCheckHandler` runs on the same schedule as the built-in metrics and returns whatever you want recorded: ```dart -// Create your custom health metric handler. Future> myHealthCheckHandler( - Serverpod pod, DateTime timestamp) async { - // Actually perform some checks. - - // Return a list of health metrics for the given timestamp. + Serverpod pod, + DateTime timestamp, +) async { return [ ServerHealthMetric( name: 'MyMetric', @@ -202,20 +154,31 @@ Future> myHealthCheckHandler( timestamp: timestamp, isHealthy: true, value: 1.0, + granularity: 1, ), ]; } ``` -Register your handler when you create your Serverpod object. +The `granularity` field is the period the value covers, in minutes. Use `1` for values recorded on the normal cycle, since Serverpod produces the hourly and daily rows itself. + +Register the handler on the server: ```dart final pod = Serverpod( - args, - Protocol(), - Endpoints(), - healthCheckHandler: myHealthCheckHandler, - ); + args, + Protocol(), + Endpoints(), + healthCheckHandler: myHealthCheckHandler, +); ``` -Once registered, your health check handler will be called once a minute to perform any health checks that you have configured. You can view the status of your checks in Serverpod Insights or in the database. +:::note +The two APIs read alike but are not related. Use `healthConfig` with `HealthIndicator` for the HTTP probes, and `healthCheckHandler` with `ServerHealthMetric` for the recorded metrics. +::: + +## Related + +- [Logging](logging): the other half of knowing what your server is doing. +- [Insights](../../tools/insights): charts for the collected metrics. +- [Custom hosting](../../deployments/custom-hosting/choosing-a-strategy): wiring the probes up to your host. diff --git a/docs/06-concepts/07-operations/04-security-and-tls.md b/docs/06-concepts/07-operations/04-security-and-tls.md index edf787d1..89db776a 100644 --- a/docs/06-concepts/07-operations/04-security-and-tls.md +++ b/docs/06-concepts/07-operations/04-security-and-tls.md @@ -1,23 +1,18 @@ --- -description: Security configuration in Serverpod lets you enable TLS/SSL directly on the server or configure the client to trust a certificate, using SecurityContextConfig. +description: How traffic to a Serverpod server is encrypted, when something else handles that for you, and how to serve HTTPS directly from the server instead. --- # Security and TLS -Serverpod can terminate TLS/SSL directly on the server and configure the client to trust your certificate. +Traffic between your app and your server should be encrypted, which on the web means HTTPS. HTTPS is HTTP wrapped in TLS, and somewhere in the chain a piece of software has to hold your certificate and do the encrypting. That job is called terminating TLS. -:::info +Most of the time it is not your server doing it. On Serverpod Cloud, TLS is handled for you and there is nothing to configure. On your own infrastructure it is normally handled in front of the server, by a load balancer or reverse proxy such as Nginx, a cloud load balancer, or Cloudflare, which forwards plain HTTP to Serverpod on an internal network. -In a production environment, TLS termination is normally handled by a load balancer or reverse proxy (e.g., Nginx, AWS ALB, or Cloudflare). -However, Serverpod also supports setting up TLS/SSL directly on the server, allowing you to provide your own certificates if needed. +Serverpod can also terminate TLS itself, which is useful when there is nothing in front of it to do the job. -::: +## Serve HTTPS from the server -## Server security configuration - -To enable TLS/SSL, pass a `SecurityContextConfig` to the `Serverpod` constructor. - -### Dart configuration example +Pass a `SecurityContextConfig` when you create the server, with a certificate chain and private key for each server you want to secure: ```dart final securityContext = SecurityContext() @@ -36,13 +31,13 @@ Serverpod( ); ``` -## Client security configuration +A Serverpod instance runs [three servers](../server-fundamentals/your-serverpod-project#the-three-servers), and each takes its own context, so you can secure them independently. -When connecting to a Serverpod server over HTTPS, the client must be configured to trust the server's certificate. +## Trust the server's certificate from your app -### Dart configuration example +Your app only needs configuring when it cannot verify your certificate on its own. Certificates from a public authority, including the ones Serverpod Cloud provisions, are trusted automatically and need nothing here. -To enable SSL/TLS, pass a `SecurityContext` to the `Client` constructor. +Self-signed certificates and private certificate authorities are the exception. There, name the certificate you want trusted by passing a `SecurityContext` to the generated `Client`: ```dart final securityContext = SecurityContext() @@ -51,15 +46,14 @@ final securityContext = SecurityContext() final client = Client( 'https://yourserver.com', securityContext: securityContext, - ... ); ``` -#### Using `SecurityContext` with `httpClientOverride` +### With an HTTP client override -If you use the [`httpClientOverride` parameter](../endpoints-and-apis/configure-http-calls), provide the security context through the HTTP client you pass in. You cannot set `securityContext` and `httpClientOverride` on the same `Client` instance. +The `securityContext` and [`httpClientOverride`](../endpoints-and-apis/configure-http-calls) parameters cannot both be set on the same `Client`, since the override replaces the HTTP client the security context would have configured. Supply the certificates through the client you pass in instead. -For example, on `dart:io` platforms you can create an `HttpClient` with your trusted certificates and wrap it in an `IOClient`: +On `dart:io` platforms, build an `HttpClient` with your trusted certificates and wrap it: ```dart import 'dart:io'; @@ -76,3 +70,9 @@ final client = Client( ), ); ``` + +## Related + +- [Configure HTTP calls](../endpoints-and-apis/configure-http-calls): certificates and HTTP client overrides on the app side. +- [Configuration](../server-fundamentals/configuration): request size limits and header settings. +- [Custom hosting](../../deployments/custom-hosting/choosing-a-strategy): where a proxy fits when you host it yourself. diff --git a/docs/06-concepts/07-operations/05-exception-monitoring.md b/docs/06-concepts/07-operations/05-exception-monitoring.md new file mode 100644 index 00000000..709f85f5 --- /dev/null +++ b/docs/06-concepts/07-operations/05-exception-monitoring.md @@ -0,0 +1,72 @@ +--- +description: Report exceptions from your code and from the framework to a monitoring service as they happen, using diagnostic event handlers. +--- + +# Exception monitoring + +When something throws in production you want to hear about it without reading log tables. Serverpod can hand every exception to code you provide, so you can forward it to a monitoring service such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), or [Datadog](https://www.datadoghq.com/). + +This works for exceptions thrown in your own code and for exceptions the framework raises itself, including failures during startup and shutdown. + +:::warning +This is an experimental feature. Its API can change in a breaking way in any minor release, so pin your Serverpod version if you depend on it and re-check this page when you upgrade. +::: + +## Add a handler + +Handlers are registered through the `experimentalFeatures` argument on the `Serverpod` constructor. A handler receives the event, the space it came from, and its context: + +```dart +var pod = Serverpod( + args, + Protocol(), + Endpoints(), + experimentalFeatures: ExperimentalFeatures( + diagnosticEventHandlers: [ + AsEventHandler((event, {required space, required context}) { + print('$event Origin is $space\n Context is ${context.toJson()}'); + }), + ], + ), +); +``` + +The `AsEventHandler` class wraps a plain function as a handler, which is the shortest way to get started. For anything longer, implement `DiagnosticEventHandler` yourself. The `space` tells you whether the event came from your application code or from the framework, and the `context` carries details such as the server ID and run mode. + +Add as many handlers as you like. Serverpod runs them concurrently and does not wait for them, so a slow handler does not slow down the request that triggered it. Each invocation is abandoned after `experimentalDiagnosticHandlerTimeout`, which defaults to 30 seconds. See the [Configuration reference](../lookups/configuration-reference). + +Handlers are for observation only. They cannot suppress an exception, change it, or alter the response. If a handler throws, the error is logged and otherwise ignored. + +## Report an exception yourself + +Call `submitDiagnosticEvent` on the `experimental` member of the server to report something you caught: + +```dart +class OrderEndpoint extends Endpoint { + Future placeOrder(Session session) async { + try { + throw Exception('An exception is thrown'); + } catch (e, stackTrace) { + session.serverpod.experimental.submitDiagnosticEvent( + ExceptionEvent(e, stackTrace), + session: session, + ); + } + return 'success'; + } +} +``` + +The `ExceptionEvent` class is the built-in event for a thrown exception and a stack trace. Passing the session attaches its context to the event, so the handler knows which call the exception came from. This works anywhere you have a session, including endpoint methods, web calls, and future calls. + +## Experimental APIs in general + +Experimental features are opt-in additions whose APIs are not yet stable. Runtime ones live under the `experimental` member of the `Serverpod` class, and graduate to `Serverpod` proper once they settle. Where possible the experimental version stays available as deprecated for a while before it is removed. + +Serverpod currently exposes two: the diagnostic event handlers on this page, and [shutdown tasks](../server-fundamentals/running-your-server#run-code-on-shutdown). Both are reached through `serverpod.experimental`. Nothing in this version is gated behind the `--experimental-features` command-line flag or the `experimental_features` config key, though both exist for when a feature needs them. See [Configuration](../server-fundamentals/configuration#experimental-features). + +## Related + +- [Logging](logging): what the server records without any handler. +- [Error handling and exceptions](../endpoints-and-apis/error-handling-and-exceptions): how exceptions reach your app. +- [Testing](../testing/advanced-examples): testing that your handlers receive the events you expect. diff --git a/docs/06-concepts/07-operations/05-experimental-features.md b/docs/06-concepts/07-operations/05-experimental-features.md deleted file mode 100644 index 64fb35d0..00000000 --- a/docs/06-concepts/07-operations/05-experimental-features.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -description: Experimental features in Serverpod are opt-in additions with unstable APIs, including diagnostic event handlers for exception monitoring and shutdown task registration. ---- - -# Experimental features - -:::warning -Be cautious when using experimental features in production environments, as their stability is uncertain and they may receive breaking changes in upcoming releases. -::: - -Experimental features are opt-in additions to Serverpod. They are disabled by default; enable them through the `experimentalFeatures` argument on the `Serverpod` constructor or via `config/generator.yaml`. - -:::note -To make the LSP server understand the usage of experimental flags and avoid complaints about unknown syntax on model files, configure experimental features in the `config/generator.yaml` file. See the [configuration documentation](../server-fundamentals/configuration#experimental-features) for more details. -::: - -## Experimental internal APIs - -Experimental internal APIs are placed under the `experimental` sub-API of the `Serverpod` class. -When an experimental feature matures it is moved from `experimental` to `Serverpod` proper. -If possible, the experimental API will remain for some time as `@deprecated`, and then removed. - -## Command-line enabled features - -Some of the experimental features are enabled by including the `--experimental-features` flag when running the serverpod command: - -```bash -$ serverpod generate --experimental-features=all -``` - -The current options you can pass are: - -| **Feature** | Description | -| :----------------- | :------------------------------------------------------------- | -| **all** | Enables all available experimental features. | - -## Exception monitoring - -Serverpod allows you to monitor exceptions in a central and flexible way by using diagnostic event handlers. -These work both for exceptions thrown in application code and from the framework (e.g. server startup or shutdown errors). - -This can be used to get all exceptions reported in realtime to services for monitoring and diagnostics, -such as [Sentry](https://sentry.io/), [Highlight](https://www.highlight.io/), and [Datadog](https://www.datadoghq.com/). - -It is easy to implement handlers and define custom filters within them. -Any number of handlers can be added. -They are run asynchronously and should not affect the behavior or response times of the server. - -These event handlers are for diagnostics only, -they do not allow any behavior-changing action such as suppressing exceptions or converting them to another exception type. - -### Setup - -This feature is enabled by providing one or more `DiagnosticEventHandler` implementations -to the Serverpod constructor's `experimentalFeatures` specification. - -Example: - -```dart - var serverpod = Serverpod( - ... - experimentalFeatures: ExperimentalFeatures( - diagnosticEventHandlers: [ - AsEventHandler((event, {required space, required context}) { - print('$event Origin is $space\n Context is ${context.toJson()}'); - }), - ], - ), - ); -``` - -### Submitting diagnostic events - -The API for submitting diagnostic events from user code, e.g. from endpoint methods, web calls, and future calls, -is the `submitDiagnosticEvent` method under the `experimental` member of the Serverpod class. - -```dart - void submitDiagnosticEvent( - DiagnosticEvent event, { - required Session session, - }) -``` - -Usage example: - -```dart -class DiagnosticEventTestEndpoint extends Endpoint { - Future submitExceptionEvent(Session session) async { - try { - throw Exception('An exception is thrown'); - } catch (e, stackTrace) { - session.serverpod.experimental.submitDiagnosticEvent( - ExceptionEvent(e, stackTrace), - session: session, - ); - } - return 'success'; - } -} -``` - -### Guidelines for handlers - -A `DiagnosticEvent` represents an event that occurs in the server. -Implementations of `DiagnosticEventHandler` can react to these events -in order to gain insights into the behavior of the server. - -As the name suggests the handlers should perform diagnostics only, -and not have any responsibilities that the regular functioning -of the server depends on. - -The registered handlers are typically run concurrently, -can not depend on each other, and asynchronously - -they are not awaited by the operation they are triggered from. - -If a handler throws an exception it will be logged to stderr -and otherwise ignored. - -### Test support - -This feature also includes support via the Serverpod test framework. -This means that the `withServerpod` construct can be used together with diagnostic event handlers to test that the events are submitted and propagated as intended. - -Example: - -```dart -void main() { - var exceptionHandler = TestExceptionHandler(); - - withServerpod('Given withServerpod with a diagnostic event handler', - experimentalFeatures: ExperimentalFeatures( - diagnosticEventHandlers: [exceptionHandler], - ), (sessionBuilder, endpoints) { - test( - 'when calling an endpoint method that submits an exception event ' - 'then the diagnostic event handler gets called', () async { - final result = await endpoints.diagnosticEventTest - .submitExceptionEvent(sessionBuilder); - expect(result, 'success'); - - final record = await exceptionHandler.events.first.timeout(Duration(seconds: 1)); - expect(record.event.exception, isA()); - expect(record.space, equals(OriginSpace.application)); - expect(record.context, isA()); - expect( - record.context.toJson(), - allOf([ - containsPair('serverId', 'default'), - containsPair('serverRunMode', 'test'), - containsPair('serverName', 'Server default'), - ])); - }); - }); -} -``` - -## Shutdown tasks - -Serverpod provides support for registering **shutdown tasks**: asynchronous operations that run when the server is shutting down. This is useful for performing cleanup operations such as saving application state or releasing external resources. - -Shutdown tasks are executed _after_ the server has stopped accepting new requests, but _before_ the Redis and database connections are closed. - -All registered shutdown tasks are executed concurrently, and the server waits for all tasks to complete before fully shutting down. If any task fails, the error is logged, but it does not prevent the server from shutting down. - -### Managing shutdown tasks - -To manage shutdown tasks, use the `experimental.shutdownTasks` API on your `Serverpod` instance. This API offers methods for adding and removing tasks. - -#### Add a shutdown task - -Each shutdown task is identified by a unique `Object` identifier and executes a function that returns a `Future`. - -To add a task, use the `addTask` method: - -```dart -var serverpod = Serverpod( - ... -); -serverpod.experimental.shutdownTasks.addTask( - #taskIdentifier, - () async { - // Your shutdown logic here - }, -); - -``` - -In the example above, a task is added with the identifier `#taskIdentifier`. This identifier is used for logging any errors that occur during task execution. - -#### Remove a shutdown task - -To remove a shutdown task, use the `removeTask` method: - -```dart -serverpod.experimental.shutdownTasks.removeTask(#taskIdentifier); -``` - -This will remove the previously registered task associated with `#taskIdentifier`. diff --git a/docs/06-concepts/08-testing/03-advanced-examples.md b/docs/06-concepts/08-testing/03-advanced-examples.md index 0ad02d23..6ed74d7b 100644 --- a/docs/06-concepts/08-testing/03-advanced-examples.md +++ b/docs/06-concepts/08-testing/03-advanced-examples.md @@ -153,3 +153,71 @@ withServerpod('Given example test', (sessionBuilder, endpoints) { This case should be rare and the above example is not a recommended best practice unless this problem is anticipated, or it has started happening. ::: + +## Testing exception monitoring + +`withServerpod` accepts the same `experimentalFeatures` argument as the server, so you can register a [diagnostic event handler](../operations/exception-monitoring) in a test and assert that your code reports the exceptions you expect. + +Write a handler that records what it receives, so the test can wait for an event: + +```dart +import 'dart:async'; + +import 'package:serverpod/serverpod.dart'; + +class TestExceptionHandler extends ExceptionHandler { + final eventsStreamController = + StreamController>(); + + Stream> get events => + eventsStreamController.stream; + + @override + Future handleTypedEvent( + ExceptionEvent event, { + required OriginSpace space, + required DiagnosticEventContext context, + }) async { + eventsStreamController.add(DiagnosticEventRecord(event, space, context)); + } +} +``` + +Then register it for the test run: + +```dart +void main() { + var exceptionHandler = TestExceptionHandler(); + + withServerpod( + 'Given withServerpod with a diagnostic event handler', + experimentalFeatures: ExperimentalFeatures( + diagnosticEventHandlers: [exceptionHandler], + ), + (sessionBuilder, endpoints) { + test( + 'when calling an endpoint method that submits an exception event ' + 'then the diagnostic event handler gets called', () async { + final result = await endpoints.order.placeOrder(sessionBuilder); + expect(result, 'success'); + + final record = + await exceptionHandler.events.first.timeout(Duration(seconds: 1)); + expect(record.event.exception, isA()); + expect(record.space, equals(OriginSpace.application)); + expect(record.context, isA()); + expect( + record.context.toJson(), + allOf([ + containsPair('serverId', 'default'), + containsPair('serverRunMode', 'test'), + containsPair('serverName', 'Server default'), + ]), + ); + }); + }, + ); +} +``` + +Handlers run asynchronously and are not awaited by the code that triggers them, so wait for the event rather than asserting immediately after the call. diff --git a/docs/06-concepts/lookups/configuration-reference.md b/docs/06-concepts/lookups/configuration-reference.md index b7007360..2cd47f0b 100644 --- a/docs/06-concepts/lookups/configuration-reference.md +++ b/docs/06-concepts/lookups/configuration-reference.md @@ -75,7 +75,7 @@ These options have no environment variable or config-file key. Set them on the ` | ServerpodConfig field | Default | Description | | ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------ | | healthCheckInterval | 1m | How often the server collects health metrics. Set to zero to disable health checks. | -| experimentalDiagnosticHandlerTimeout | 30s | The timeout for [diagnostic event handlers](../operations/experimental-features#exception-monitoring). | +| experimentalDiagnosticHandlerTimeout | 30s | The timeout for [diagnostic event handlers](../operations/exception-monitoring). | ### Password environment variables From 388ff952dbb804dfe7484485ba6e8a766747f987 Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Mon, 27 Jul 2026 16:10:51 +0100 Subject: [PATCH 3/6] docs: Address self-review feedback on the scheduling section --- docs/06-concepts/06-scheduling/01-overview.md | 2 +- docs/06-concepts/06-scheduling/03-recurring-tasks.md | 6 ++++-- docs/06-concepts/06-scheduling/04-inheritance.md | 4 ++-- docs/06-concepts/06-scheduling/05-configuration.md | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/06-concepts/06-scheduling/01-overview.md b/docs/06-concepts/06-scheduling/01-overview.md index 765bc657..d6f9f054 100644 --- a/docs/06-concepts/06-scheduling/01-overview.md +++ b/docs/06-concepts/06-scheduling/01-overview.md @@ -14,7 +14,7 @@ You write a future call as a method on a class, generate the type-safe code, and - **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). Cron is a standard text format for describing repeating schedules. +- **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 diff --git a/docs/06-concepts/06-scheduling/03-recurring-tasks.md b/docs/06-concepts/06-scheduling/03-recurring-tasks.md index 5f7416a5..606361fb 100644 --- a/docs/06-concepts/06-scheduling/03-recurring-tasks.md +++ b/docs/06-concepts/06-scheduling/03-recurring-tasks.md @@ -30,16 +30,18 @@ await pod.futureCalls .cleanUp(); ``` -Pass `start` to set when the first run happens. The `start` value is itself the first run time, and later runs follow one interval apart from it, so use a time at or near now rather than one far in the future: +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.utc(2026, 1, 1, 2)) + .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 diff --git a/docs/06-concepts/06-scheduling/04-inheritance.md b/docs/06-concepts/06-scheduling/04-inheritance.md index 296cb116..eedbbcf1 100644 --- a/docs/06-concepts/06-scheduling/04-inheritance.md +++ b/docs/06-concepts/06-scheduling/04-inheritance.md @@ -26,7 +26,7 @@ class MyGreeter extends Greeter { } ``` -`Greeter` exposes `hello`. `MyGreeter` exposes both the inherited `hello` and its own `bye`. +The `Greeter` class exposes `hello`, and `MyGreeter` exposes both the inherited `hello` and its own `bye`. ## Expose future calls from a module @@ -81,6 +81,6 @@ class ExcitedGreeter extends Greeter { } ``` -`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. +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. diff --git a/docs/06-concepts/06-scheduling/05-configuration.md b/docs/06-concepts/06-scheduling/05-configuration.md index 695b3070..d4ac7728 100644 --- a/docs/06-concepts/06-scheduling/05-configuration.md +++ b/docs/06-concepts/06-scheduling/05-configuration.md @@ -26,7 +26,7 @@ futureCall: ### Enable or disable execution -`futureCallExecutionEnabled` 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. +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 @@ -34,7 +34,7 @@ futureCallExecutionEnabled: false ### Concurrency limit -`concurrencyLimit` 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. +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. @@ -45,7 +45,7 @@ futureCall: ### Scan interval -`scanInterval` 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. +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: From a62ff1790a8ddedf3bea55a385996ceebe2c7615 Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Tue, 28 Jul 2026 09:56:23 +0100 Subject: [PATCH 4/6] docs: Address review feedback on the scheduling section --- docs/06-concepts/06-scheduling/01-overview.md | 8 +++++++- .../06-concepts/06-scheduling/02-future-calls.md | 14 ++++++++++++-- .../06-scheduling/03-recurring-tasks.md | 10 ++++++++-- docs/06-concepts/06-scheduling/04-inheritance.md | 7 ++++++- .../06-scheduling/05-configuration.md | 16 +++++++++++++--- docs/06-concepts/06-scheduling/06-legacy.md | 5 +++++ 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/docs/06-concepts/06-scheduling/01-overview.md b/docs/06-concepts/06-scheduling/01-overview.md index d6f9f054..acbe4aa7 100644 --- a/docs/06-concepts/06-scheduling/01-overview.md +++ b/docs/06-concepts/06-scheduling/01-overview.md @@ -26,10 +26,16 @@ If the method itself throws an exception, Serverpod logs the error and does not 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). -## In this section +## 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/02-future-calls.md b/docs/06-concepts/06-scheduling/02-future-calls.md index 1cb51865..9211e523 100644 --- a/docs/06-concepts/06-scheduling/02-future-calls.md +++ b/docs/06-concepts/06-scheduling/02-future-calls.md @@ -1,5 +1,5 @@ --- -description: Define a future call, schedule it after a delay or at a specific time with the type-safe API, and cancel scheduled calls by identifier. +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 @@ -52,7 +52,11 @@ void run(List args) async { ``` :::note -The `pod.futureCalls` accessor exists only once you have defined at least one future call and generated the code. Scheduling a call before the server has started throws an exception. +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 @@ -126,3 +130,9 @@ Scheduled calls are stored in the `serverpod_future_call` database table. You ca ## 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/03-recurring-tasks.md b/docs/06-concepts/06-scheduling/03-recurring-tasks.md index 606361fb..63eba4b2 100644 --- a/docs/06-concepts/06-scheduling/03-recurring-tasks.md +++ b/docs/06-concepts/06-scheduling/03-recurring-tasks.md @@ -1,5 +1,5 @@ --- -description: Run a future call on a repeating schedule with callRecurring, using either a fixed interval or a cron expression. +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 @@ -46,7 +46,7 @@ If the server is down when a run is due, that run happens once when the server i ## 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". +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 @@ -95,3 +95,9 @@ class ExampleFutureCall extends FutureCall { ``` 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-inheritance.md b/docs/06-concepts/06-scheduling/04-inheritance.md index eedbbcf1..365331e4 100644 --- a/docs/06-concepts/06-scheduling/04-inheritance.md +++ b/docs/06-concepts/06-scheduling/04-inheritance.md @@ -1,5 +1,5 @@ --- -description: Share future call methods by extending another FutureCall class, expose them from modules with an abstract base, and override inherited methods. +description: FutureCall inheritance shares methods between classes, exposes a module’s future calls through an abstract base, and allows overrides. --- # Inheritance @@ -84,3 +84,8 @@ class ExcitedGreeter extends Greeter { 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 index d4ac7728..2d346446 100644 --- a/docs/06-concepts/06-scheduling/05-configuration.md +++ b/docs/06-concepts/06-scheduling/05-configuration.md @@ -1,5 +1,5 @@ --- -description: Configure future call execution, concurrency, the scan interval, and broken-call handling through config files or environment variables. +description: Future call settings cover execution, concurrency, the scan interval, and broken-call handling, set in config files or environment variables. --- # Configuration @@ -79,10 +79,14 @@ futureCall: 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 +dart run bin/main.dart --role maintenance ``` -Because it executes due calls, do not run this as a dry check against a production database. The process exits normally whether or not broken calls are found, so read the result from the logs. +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 @@ -93,3 +97,9 @@ When the check finds broken calls it logs a warning but does not delete them. Se 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/06-legacy.md b/docs/06-concepts/06-scheduling/06-legacy.md index bd5444a8..91d2e127 100644 --- a/docs/06-concepts/06-scheduling/06-legacy.md +++ b/docs/06-concepts/06-scheduling/06-legacy.md @@ -77,3 +77,8 @@ 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. From 0a88146a15d62be61a06409f0c6de0a78be6a3cf Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Tue, 28 Jul 2026 10:21:27 +0100 Subject: [PATCH 5/6] docs: Use a straight apostrophe in the inheritance description --- docs/06-concepts/06-scheduling/04-inheritance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/06-concepts/06-scheduling/04-inheritance.md b/docs/06-concepts/06-scheduling/04-inheritance.md index 365331e4..8a235c6a 100644 --- a/docs/06-concepts/06-scheduling/04-inheritance.md +++ b/docs/06-concepts/06-scheduling/04-inheritance.md @@ -1,5 +1,5 @@ --- -description: FutureCall inheritance shares methods between classes, exposes a module’s future calls through an abstract base, and allows overrides. +description: FutureCall inheritance shares methods between classes, exposes a module's future calls through an abstract base, and allows overrides. --- # Inheritance From 7038004d2d0e00d1f5943fb9d3fc773ffab20ff5 Mon Sep 17 00:00:00 2001 From: developerjamiu Date: Tue, 28 Jul 2026 10:55:57 +0100 Subject: [PATCH 6/6] docs: Address review feedback on the operations section --- .../02-endpoints-and-apis/06-caching.md | 2 +- docs/06-concepts/07-operations/02-logging.md | 25 ++++++++++++------- .../07-operations/05-exception-monitoring.md | 12 +++------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/docs/06-concepts/02-endpoints-and-apis/06-caching.md b/docs/06-concepts/02-endpoints-and-apis/06-caching.md index 1f673b23..6b7684c5 100644 --- a/docs/06-concepts/02-endpoints-and-apis/06-caching.md +++ b/docs/06-concepts/02-endpoints-and-apis/06-caching.md @@ -1,5 +1,5 @@ --- -description: Cache objects in server memory or in Redis through session.caches to avoid repeating expensive database work, with lifetimes, groups, and explicit invalidation. +description: A cache stores a result under a key so the next request can read it back, held in server memory or in Redis and reached through session.caches. --- # Caching diff --git a/docs/06-concepts/07-operations/02-logging.md b/docs/06-concepts/07-operations/02-logging.md index 8b7ebc9d..15c577a1 100644 --- a/docs/06-concepts/07-operations/02-logging.md +++ b/docs/06-concepts/07-operations/02-logging.md @@ -70,9 +70,8 @@ Session logging is configured under `sessionLogs:` in your config file for the r | `persistentEnabled` | `SERVERPOD_SESSION_PERSISTENT_LOG_ENABLED` | `true` when a database is configured | | `consoleEnabled` | `SERVERPOD_SESSION_CONSOLE_LOG_ENABLED` | `true` in `development` or when there is no database, otherwise `false` | | `consoleLogFormat` | `SERVERPOD_SESSION_CONSOLE_LOG_FORMAT` | `text` in `development`, otherwise `json` | -| `cleanupInterval` | `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL` | `24h`, but see [Purge old records](#purge-old-records) | -| `retentionPeriod` | `SERVERPOD_SESSION_LOG_RETENTION_PERIOD` | `90d`, but see [Purge old records](#purge-old-records) | -| `retentionCount` | `SERVERPOD_SESSION_LOG_RETENTION_COUNT` | `100000`, but see [Purge old records](#purge-old-records) | + +Three more settings control [purging](#purge-old-records), which behaves differently enough to be worth reading before you rely on it. ```yaml sessionLogs: @@ -87,8 +86,10 @@ Durations use the same format as [model default values](../data-and-the-database :::warning Setting `persistentEnabled` to `true` without a configured database throws a `StateError` on startup. Persistent logging needs somewhere to persist to. +::: -Persistent logging is also unavailable on SQLite, which cannot handle the concurrent writes it needs. The server warns and skips it. Nothing takes its place automatically, so enable `consoleEnabled` if you want records in the run modes where it defaults to off. +:::warning +Persistent logging is unavailable on SQLite, which cannot handle the concurrent writes it needs. The server warns and skips it, and nothing takes its place, so enable `consoleEnabled` if you want records in the run modes where it defaults to off. ::: :::info @@ -99,16 +100,22 @@ Every environment variable in the table takes a real value. Setting one to an em Log tables grow with every call your server handles, so Serverpod can delete old records for you. Cleanup runs on the `cleanupInterval`, and removes session rows that are either older than `retentionPeriod` or beyond the newest `retentionCount`, whichever applies first. Deleting a session row takes its query, message, and log rows with it. -The `cleanupInterval` setting is the switch for the whole job. With no interval set, nothing is purged whatever the two retention values say. Purging also requires `persistentEnabled`, since there is nothing to delete otherwise. - -Cleanup is triggered by log writes rather than by a timer, so a server that is not logging anything does not purge. A single pass gives up after an hour, and the next interval starts a fresh one. +| Setting | Environment variable | Default | +| --- | --- | --- | +| `cleanupInterval` | `SERVERPOD_SESSION_LOG_CLEANUP_INTERVAL` | Unset, so no purging | +| `retentionPeriod` | `SERVERPOD_SESSION_LOG_RETENTION_PERIOD` | Unset, so no age limit | +| `retentionCount` | `SERVERPOD_SESSION_LOG_RETENTION_COUNT` | Unset, so no count limit | :::warning -The three cleanup settings only fall back to their defaults when `sessionLogs` is absent from your config entirely. If you set any one session-log key, in the config file or through an environment variable, the settings you did not set resolve to null rather than to the defaults in the table above, and that policy is switched off with no warning. +These three fall back to `24h`, `90d`, and `100000` only when `sessionLogs` is absent from your config entirely. Set any one session-log key, in the file or through an environment variable, and the ones you did not set resolve to unset rather than to those values, switching that policy off with no warning. -This affects new projects: the generated `development`, `test`, and `production` configs each contain a `sessionLogs` block, so purging is off in those run modes until you set the three values explicitly. Set `cleanupInterval`, `retentionPeriod`, and `retentionCount` together whenever you configure any part of `sessionLogs`. +Generated projects ship a `sessionLogs` block in the `development`, `test`, and `production` configs, so purging is off in those run modes until you set all three explicitly. Set `cleanupInterval`, `retentionPeriod`, and `retentionCount` together whenever you configure any part of `sessionLogs`. ::: +The `cleanupInterval` setting is the switch for the whole job. With no interval set, nothing is purged whatever the two retention values say. Purging also requires `persistentEnabled`, since there is nothing to delete otherwise. + +Cleanup is triggered by log writes rather than by a timer, so a server that is not logging anything does not purge. A single pass gives up after an hour, and the next interval starts a fresh one. + ## Related - [Configuration](../server-fundamentals/configuration): the config files these settings live in. diff --git a/docs/06-concepts/07-operations/05-exception-monitoring.md b/docs/06-concepts/07-operations/05-exception-monitoring.md index 709f85f5..f4e68947 100644 --- a/docs/06-concepts/07-operations/05-exception-monitoring.md +++ b/docs/06-concepts/07-operations/05-exception-monitoring.md @@ -1,5 +1,5 @@ --- -description: Report exceptions from your code and from the framework to a monitoring service as they happen, using diagnostic event handlers. +description: Diagnostic event handlers report exceptions from your code and from the framework to a monitoring service as they happen. --- # Exception monitoring @@ -9,7 +9,7 @@ When something throws in production you want to hear about it without reading lo This works for exceptions thrown in your own code and for exceptions the framework raises itself, including failures during startup and shutdown. :::warning -This is an experimental feature. Its API can change in a breaking way in any minor release, so pin your Serverpod version if you depend on it and re-check this page when you upgrade. +This is an experimental feature, reached through `serverpod.experimental`. Its API can change in a breaking way in any minor release, so pin your Serverpod version if you depend on it and re-check this page when you upgrade. Experimental APIs move to `Serverpod` proper once they settle. ::: ## Add a handler @@ -59,14 +59,10 @@ class OrderEndpoint extends Endpoint { The `ExceptionEvent` class is the built-in event for a thrown exception and a stack trace. Passing the session attaches its context to the event, so the handler knows which call the exception came from. This works anywhere you have a session, including endpoint methods, web calls, and future calls. -## Experimental APIs in general - -Experimental features are opt-in additions whose APIs are not yet stable. Runtime ones live under the `experimental` member of the `Serverpod` class, and graduate to `Serverpod` proper once they settle. Where possible the experimental version stays available as deprecated for a while before it is removed. - -Serverpod currently exposes two: the diagnostic event handlers on this page, and [shutdown tasks](../server-fundamentals/running-your-server#run-code-on-shutdown). Both are reached through `serverpod.experimental`. Nothing in this version is gated behind the `--experimental-features` command-line flag or the `experimental_features` config key, though both exist for when a feature needs them. See [Configuration](../server-fundamentals/configuration#experimental-features). - ## Related - [Logging](logging): what the server records without any handler. - [Error handling and exceptions](../endpoints-and-apis/error-handling-and-exceptions): how exceptions reach your app. +- [Run code on shutdown](../server-fundamentals/running-your-server#run-code-on-shutdown): the other experimental API Serverpod exposes. +- [Configuration](../server-fundamentals/configuration#experimental-features): opting in to experimental features. - [Testing](../testing/advanced-examples): testing that your handlers receive the events you expect.