diff --git a/versioned_docs/version-2.9.0/06-concepts/05-sessions.md b/versioned_docs/version-2.9.0/06-concepts/05-sessions.md index 1d07327..8555f89 100644 --- a/versioned_docs/version-2.9.0/06-concepts/05-sessions.md +++ b/versioned_docs/version-2.9.0/06-concepts/05-sessions.md @@ -1,26 +1,303 @@ # Sessions -The `Session` object provides information about the current context in a method call in Serverpod. It provides access to the database, caching, authentication, data storage, and messaging within the server. It will also contain information about the HTTP request object. +A Session in Serverpod is a request-scoped context object that exists for the duration of a single client request or connection. It provides access to server resources and maintains state during request processing. -If you need additional information about a call, you may need to cast the Session to one of its subclasses, e.g., `MethodCallSession` or `StreamingSession`. The `MethodCallSession` object provides additional properties, such as the name of the endpoint and method and the underlying `HttpRequest` object. +Sessions are the gateway to Serverpod's functionality - every interaction with the database, cache, file storage, or messaging system happens through a session. The framework automatically creates the appropriate session type when a client makes a request, manages its lifecycle, and ensures proper cleanup when the request completes. For special cases like background tasks or system operations, you can also create and manage sessions manually. -:::tip +:::note + +A Serverpod Session should not be confused with the concept of "web sessions" or "user sessions" which persist over multiple API calls. See the [Authentication documentation](./11-authentication/01-setup.md) for managing persistent authentication. + +::: + +## Quick reference + +### Essential properties + +- **`db`** - Database access. [See database docs](./06-database/01-connection.md) +- **`caches`** - Local and distributed caching. [See caching docs](./08-caching.md) +- **`storage`** - File storage operations. [See file uploads](./12-file-uploads.md) +- **`messages`** - Server events for real-time communication within and across servers. [See server events docs](./16-server-events.md) +- **`passwords`** - Credentials from config and environment. [See configuration](./07-configuration.md) +- **`authenticated`** - Current user authentication info. [See authentication docs](./11-authentication/02-basics.md) + +### Key methods + +- **`log(message, level)`** - Add log entry +- **`addWillCloseListener(callback)`** - Register cleanup callback + +## Session types + +Serverpod creates different session types based on the context: + +| Type | Created For | Lifetime | Common Use Cases | +| ----------------------- | ---------------------------- | ------------------- | ---------------------------- | +| **MethodCallSession** | `Future` endpoint methods | Single request | API calls, CRUD operations | +| **WebCallSession** | Web server routes | Single request | Web pages, form submissions | +| **MethodStreamSession** | `Stream` endpoint methods | Stream duration | Real-time updates, chat | +| **StreamingSession** | WebSocket connections | Connection duration | Live dashboards, multiplayer | +| **FutureCallSession** | Scheduled tasks | Task execution | Email sending, batch jobs | +| **InternalSession** | Manual creation | Until closed | Background work, migrations | -You can use the Session object to access the IP address of the client calling a method. Serverpod includes an extension on `HttpRequest` that allows you to access the IP address even if your server is running behind a load balancer. +### Example: Automatic session (MethodCallSession) ```dart -session as MethodCallSession; -var ipAddress = session.httpRequest.remoteIpAddress; +// lib/src/endpoints/example_endpoint.dart +class ExampleEndpoint extends Endpoint { + Future hello(Session session, String name) async { + // MethodCallSession is created automatically + return 'Hello $name'; + // Session closes automatically when method returns + } +} ``` -::: +### Example: Manual session (InternalSession) + +InternalSession is the only session type that requires manual management: + +```dart +Future performMaintenance() async { + var session = await Serverpod.instance.createSession(); + try { + // Perform operations + await cleanupOldRecords(session); + await updateStatistics(session); + } finally { + await session.close(); // Must close manually! + } +} +``` -## Creating sessions +**Important**: Always use try-finally with InternalSession to prevent memory leaks. -In most cases, Serverpod manages the life cycle of the Session objects for you. A session is created for a call or a streaming connection and is disposed of when the call has been completed. In rare cases, you may want to create a session manually. For instance, if you are making a database call outside the scope of a method or a future call. In these cases, you can create a new session with the `createSession` method of the `Serverpod` singleton. You can access the singleton by the static `Serverpod.instance` field. If you create a new session, you are also responsible for closing it using the `session.close()` method. +## Session lifecycle -:::note +```mermaid +flowchart TB + Request([Request/Trigger]) --> Create[Session Created] + Create --> Init[Initialize] + Init --> Active[Execute Method] + Active --> Close[Close Session] + Close --> End([Request Complete]) +``` + +Sessions follow a predictable lifecycle from creation to cleanup. When a client makes a request, Serverpod automatically creates the appropriate session type (see table above), initializes it with a unique ID, and sets up access to resources like the database, cache, and file storage. + +During the active phase, your operation executes with full access to Serverpod resources through the session. You can query the database, write logs, send messages, and access storage - all operations are tracked and tied to this specific session. When the operation completes, most sessions close automatically, writing any accumulated logs to the database and releasing all resources. + +### Internal Sessions + +The only exception is `InternalSession`, which you create manually for background tasks. Manual sessions require explicit closure with `session.close()`. Forgetting to close these sessions causes memory leaks as logs accumulate indefinitely. Always use try-finally blocks to ensure proper cleanup. After any session closes, attempting to use it throws a `StateError`. -It's not recommended to keep a session open indefinitely as it can lead to memory leaks, as the session stores logs until it is closed. It's inexpensive to create a new session, so keep them short. +### Session cleanup callbacks + +You can register callbacks that execute just before a session closes using `addWillCloseListener`. This is useful for cleanup operations, releasing resources, or performing final operations: + +```dart +Future processData(Session session) async { + var tempFile = File('/tmp/processing_data.tmp'); + + // Register cleanup callback + session.addWillCloseListener((session) async { + if (await tempFile.exists()) { + await tempFile.delete(); + session.log('Cleaned up temporary file'); + } + }); + + // Process data using temp file + await tempFile.writeAsString('processing...'); + // Session closes automatically, cleanup callback runs +} +``` + +Cleanup callbacks run in the order they were registered and are called for all session types, including manual sessions when you call `session.close()`. + +## Logging + +Serverpod batches log entries for performance. During normal operations, logs accumulate in memory and are written to the database in a single batch when the session closes. This includes all your `session.log()` calls, database query timings, and session metadata. The exception is streaming sessions (`MethodStreamSession` and `StreamingSession`), which write logs continuously by default to avoid memory buildup during long connections. + +:::warning + +If you forget to close a manual session (`InternalSession`), logs remain in memory indefinitely and are never persisted - this is a common cause of both memory leaks and missing debug information. ::: + +## Common pitfalls and solutions + +### Pitfall 1: Using session after method returns + +**Problem:** Using a session after it's closed throws a `StateError` + +```dart +Future processUser(Session session, int userId) async { + var user = await User.db.findById(session, userId); + + // Schedule async work + Timer(Duration(seconds: 5), () async { + // ❌ Session is already closed! + // This will throw: StateError: Session is closed + await user.updateLastSeen(session); + }); + + return; // Session closes here +} +``` + +**Solution 1 - Use FutureCalls:** + +```dart +Future processUser(Session session, int userId) async { + var user = await User.db.findById(session, userId); + + // Schedule through Serverpod + await session.serverpod.futureCallWithDelay( + 'updateLastSeen', + UserIdData(userId: userId), + Duration(seconds: 5), + ); + + return; +} +``` + +**Solution 2 - Create manual session:** + +```dart +Future processUser(Session session, int userId) async { + var user = await User.db.findById(session, userId); + + Timer(Duration(seconds: 5), () async { + // Create new session for async work + var newSession = await Serverpod.instance.createSession(); + try { + await user.updateLastSeen(newSession); + } finally { + await newSession.close(); + } + }); + + return; +} +``` + +### Pitfall 2: Forgetting to close manual sessions + +**Problem:** + +```dart +// ❌ Memory leak! +var session = await Serverpod.instance.createSession(); +var users = await User.db.find(session); +// Forgot to close - session leaks memory +``` + +**Solution - Always use try-finally:** + +```dart +var session = await Serverpod.instance.createSession(); +try { + var users = await User.db.find(session); + // Process users +} finally { + await session.close(); // Always runs +} +``` + +## Best practices + +### 1. Let Serverpod manage sessions when possible + +Prefer using the session provided to your endpoint rather than creating new ones: + +```dart +// ✅ Good - Use provided session +Future> getActiveUsers(Session session) async { + return await User.db.find( + session, + where: (t) => t.isActive.equals(true), + ); +} + +// ❌ Avoid - Creating unnecessary session +Future> getActiveUsers(Session session) async { + var newSession = await Serverpod.instance.createSession(); + try { + return await User.db.find(newSession, ...); + } finally { + await newSession.close(); + } +} +``` + +### 2. Use FutureCalls for delayed operations + +Instead of managing sessions for async work, use Serverpod's future call system: + +```dart +// ✅ Good - Let Serverpod manage the session +await serverpod.futureCallWithDelay( + 'processPayment', + PaymentData(orderId: order.id), + Duration(hours: 1), +); + +// ❌ Complex - Manual session management +Future.delayed(Duration(hours: 1), () async { + var session = await Serverpod.instance.createSession(); + try { + await processPayment(session, order.id); + } finally { + await session.close(); + } +}); +``` + +### 3. Handle errors properly + +Always handle exceptions to prevent unclosed sessions: + +```dart +// ✅ Good - Errors won't prevent session cleanup +Future safeOperation() async { + var session = await Serverpod.instance.createSession(); + try { + await riskyOperation(session); + } catch (e) { + session.log('Operation failed: $e', level: LogLevel.error); + // Handle error appropriately + } finally { + await session.close(); + } +} +``` + +## Testing + +When testing endpoints, the `TestSessionBuilder` can be used to simulate and configure session properties for controlled test scenarios: + +```dart +withServerpod('test group', (sessionBuilder, endpoints) { + test('endpoint test', () async { + var result = await endpoints.users.getUser(sessionBuilder, 123); + expect(result.name, 'John'); + }); + + test('authenticated endpoint test', () async { + const int userId = 1234; + + var authenticatedSessionBuilder = sessionBuilder.copyWith( + authentication: AuthenticationOverride.authenticationInfo(userId, {Scope('user')}), + ); + + var result = await endpoints.users.updateProfile( + authenticatedSessionBuilder, + ProfileData(name: 'Jane') + ); + expect(result.success, isTrue); + }); +}); +``` + +For detailed testing strategies, see the [testing documentation](./19-testing/01-get-started.md).