--- url: 'https://docs.kurrent.io/clients/index.md' --- # Clients Building with KurrentDB begins with choosing the right client library for your programming language. KurrentDB client libraries provide the essential tools and APIs needed to integrate your applications with KurrentDB's event streaming platform, whether you're using [Kurrent Cloud](/cloud/introduction.md) or deploying [on-premises](https://www.kurrent.io/kurrent-platform-editions). KurrentDB maintains and actively develops client libraries for multiple programming languages. Our engineering team continuously enhances these libraries with new features, performance optimizations, security updates, and compatibility improvements to support your needs. --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/appending-events.md' --- # Appending events When you start working with EventStoreDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to EventStoreDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to EventStoreDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, EventStoreDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of EventStoreDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. EventStoreDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform EventStoreDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from EventStoreDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = EventStoreClientSettings.Create( $"esdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new EventStoreClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/delete-stream.md' --- # Deleting Events In EventStoreDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, EventStoreDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in EventStoreDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in EventStoreDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/getting-started.md' --- # Getting started Get started by connecting your application to EventStoreDB. ## Connecting to EventStoreDB To connect your application to EventStoreDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the .NET `EventStore.Client.Grpc` and `EventStore.Client.Grpc.Streams` package to your project: ```bash dotnet add package EventStoreDB.Client dotnet add package EventStore.Client.Grpc.Streams ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The EventStoreDB connection string supports two schemas: `esdb://` for connecting to a single-node server, and `esdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `esdb://`, the client will connect directly to the node; with `esdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `esdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` esdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` esdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `esdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new EventStoreClient(EventStoreClientSettings.Create("esdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to EventStoreDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites Install the required OpenTelemetry package from NuGet: ```bash dotnet add package EventStore.Client.Extensions.OpenTelemetry ``` You'll also need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddEventStoreClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using EventStore.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddEventStoreClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddEventStoreClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | EventStoreDB server address | `localhost` | | `server.port` | EventStoreDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: eventstoredb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.eventstoredb.stream: example-stream server.address: localhost server.port: 2113 db.system: eventstoredb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: >- https://docs.kurrent.io/clients/dotnet/legacy/v23.3/persistent-subscriptions.md --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Required packages Add the `EventStore.Client.Grpc` and `EventStore.Client.Grpc.PersistentSubscriptions` package to your project: ```bash dotnet add package EventStoreDB.Client dotnet add package EventStore.Client.Grpc.PersistentSubscriptions ``` ## Creating a client To work with persistent subscriptions, you need to create an instance of the `EventStorePersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new EventStorePersistentSubscriptionsClient( EventStoreClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/projections.md' --- # Projection management The client provides a way to manage projections in EventStoreDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Required packages Add the .NET `EventStore.Client.Grpc` and `EventStore.Client.Grpc.ProjectionManagement` package to your project: ```bash dotnet add package EventStoreDB.Client dotnet add package EventStore.Client.Grpc.ProjectionManagement ``` ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new EventStoreProjectionManagementClient( EventStoreClientSettings.Create("esdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/reading-events.md' --- # Reading Events There are two options for reading events from EventStoreDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in EventStoreDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells EventStoreDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against EventStoreDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events EventStoreDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/legacy/v23.3/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in EventStoreDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering EventStoreDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = KurentDBClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new KurentDBClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/getting-started.md' --- # Getting started Get started by connecting your application to KurrentDB. ## Connecting to KurrentDB To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the `KurrentDB.Client` package to your project: ```bash dotnet add package KurrentDB.Client --version "1.0.*" ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new KurentDBClient(KurentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddKurentDBClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: kurrentdb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.kurrentdb.stream: example-stream server.address: localhost server.port: 2113 db.system: kurrentdb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client To work with persistent subscriptions, you need to create an instance of the `KurentDBPersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new KurrentDBPersistentSubscriptionsClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new KurrentDBProjectionManagementClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/reading-events.md' --- # Reading Events There are two options for reading events from KurrentDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ```cs using System.Text.Json; AppendStreamRequest[] requests = [ new( "order-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "OrderCreated", Encoding.UTF8.GetBytes("{\"orderId\": \"21345\", \"amount\": 99.99}")) ] ), new( "inventory-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "ItemReserved", Encoding.UTF8.GetBytes("{\"itemId\": \"abc123\", \"quantity\": 2}")) ] ) ]; await client.MultiStreamAppendAsync(requests); ``` The result returns the position of the last appended record in the transaction and a collection of responses for each stream appended in the transaction. ::: warning The metadata for an event must be a valid JSON object where both keys and values are strings. It is essential that the JSON is well-formed and not missing, as any malformed or absent metadata will result in an `ArgumentException` being thrown. You can use the provided `Encode` and `Decode` extension methods when writing and reading metadata. For example: ```cs var metadata = new Dictionary { { "userId", "user-456" } }; // encode to bytes before appending var metadataBytes = metadata.Encode(); ``` And when reading metadata back: ```cs var metadata = metadataBytes.Decode(); ``` ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = KurentDBClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new KurentDBClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/getting-started.md' --- # Getting started Get started by connecting your application to KurrentDB. ## Connecting to KurrentDB To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the `KurrentDB.Client` package to your project: ```bash dotnet add package KurrentDB.Client --version "1.1.*" ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new KurentDBClient(KurentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddKurentDBClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: kurrentdb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.kurrentdb.stream: example-stream server.address: localhost server.port: 2113 db.system: kurrentdb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client To work with persistent subscriptions, you need to create an instance of the `KurentDBPersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new KurrentDBPersistentSubscriptionsClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new KurrentDBProjectionManagementClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/reading-events.md' --- # Reading Events There are two options for reading events from KurrentDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ```cs using System.Text.Json; AppendStreamRequest[] requests = [ new( "order-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "OrderCreated", Encoding.UTF8.GetBytes("{\"orderId\": \"21345\", \"amount\": 99.99}")) ] ), new( "inventory-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "ItemReserved", Encoding.UTF8.GetBytes("{\"itemId\": \"abc123\", \"quantity\": 2}")) ] ) ]; await client.MultiStreamAppendAsync(requests); ``` The result returns the position of the last appended record in the transaction and a collection of responses for each stream appended in the transaction. ::: warning The metadata for an event must be a valid JSON object where both keys and values are strings. It is essential that the JSON is well-formed and not missing, as any malformed or absent metadata will result in an `ArgumentException` being thrown. You can use the provided `Encode` and `Decode` extension methods when writing and reading metadata. For example: ```cs var metadata = new Dictionary { { "userId", "user-456" } }; // encode to bytes before appending var metadataBytes = metadata.Encode(); ``` And when reading metadata back: ```cs var metadata = metadataBytes.Decode(); ``` ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = KurentDBClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new KurentDBClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/getting-started.md' --- # Getting started Get started by connecting your application to KurrentDB. ## Connecting to KurrentDB To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the `KurrentDB.Client` package to your project: ```bash dotnet add package KurrentDB.Client --version "1.1.*" ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new KurentDBClient(KurentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddKurentDBClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: kurrentdb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.kurrentdb.stream: example-stream server.address: localhost server.port: 2113 db.system: kurrentdb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client To work with persistent subscriptions, you need to create an instance of the `KurentDBPersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new KurrentDBPersistentSubscriptionsClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new KurrentDBProjectionManagementClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/reading-events.md' --- # Reading Events There are two options for reading events from KurrentDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ```cs using System.Text.Json; AppendStreamRequest[] requests = [ new( "order-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "OrderCreated", Encoding.UTF8.GetBytes("{\"orderId\": \"21345\", \"amount\": 99.99}")) ] ), new( "inventory-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "ItemReserved", Encoding.UTF8.GetBytes("{\"itemId\": \"abc123\", \"quantity\": 2}")) ] ) ]; await client.MultiStreamAppendAsync(requests); ``` The result returns the position of the last appended record in the transaction and a collection of responses for each stream appended in the transaction. ::: warning The metadata for an event must be a valid JSON object where both keys and values are strings. It is essential that the JSON is well-formed and not missing, as any malformed or absent metadata will result in an `ArgumentException` being thrown. You can use the provided `Encode` and `Decode` extension methods when writing and reading metadata. For example: ```cs var metadata = new Dictionary { { "userId", "user-456" } }; // encode to bytes before appending var metadataBytes = metadata.Encode(); ``` And when reading metadata back: ```cs var metadata = metadataBytes.Decode(); ``` ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = KurentDBClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new KurentDBClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/getting-started.md' --- # Getting started Get started by connecting your application to KurrentDB. ## Connecting to KurrentDB To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the `KurrentDB.Client` package to your project: ```bash dotnet add package KurrentDB.Client --version "1.1.*" ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new KurentDBClient(KurentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddKurentDBClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: kurrentdb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.kurrentdb.stream: example-stream server.address: localhost server.port: 2113 db.system: kurrentdb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client To work with persistent subscriptions, you need to create an instance of the `KurentDBPersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new KurrentDBPersistentSubscriptionsClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new KurrentDBProjectionManagementClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/reading-events.md' --- # Reading Events There are two options for reading events from KurrentDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.3/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `AppendToStream` method. ```cs var eventData = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", StreamState.NoStream, new List { eventData } ); ``` `AppendToStream` takes a collection of `EventData`, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **type**, and **data**. ### EventId This takes the format of a `Uuid` and is used to uniquely identify the event you are trying to append. If two events with the same `Uuid` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); // attempt to append the same event again await client.AppendToStreamAsync("order-123", StreamState.Any, [orderPlaced]); ``` ### Type Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ## Handling concurrency When appending events to a stream, you can supply a *stream state* or *stream revision*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```cs var orderPlaced = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray()); var orderShipped = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderPlaced]); // attempt to append the second event expecting no stream await client.AppendToStreamAsync("order-123", StreamState.NoStream, [orderShipped]); ``` There are three available stream states: * `Any` * `NoStream` * `StreamExists` This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```cs{1-3,11,21} var lastEvent = client .ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start) .LastAsync(); var orderPaid = new EventData( Uuid.NewUuid(), "OrderPaid", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderPaid] ); var orderCancelled = new EventData( Uuid.NewUuid(), "OrderCancelled", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendToStreamAsync( "order-123", lastEvent.OriginalEventNumber.ToUInt64(), [orderCancelled] ); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```cs{5} await client.AppendToStreamAsync( "order-123", StreamState.Any, new[] { eventData }, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Atomic appends KurrentDB provides two operations for appending events to one or more streams in a single atomic transaction: `AppendRecords` and `MultiStreamAppend`. Both guarantee that either all writes succeed or the entire operation fails, but they differ in how records are organized, ordered, and validated. | | `AppendRecords` | `MultiStreamAppend` | |------------------------|-----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | **Available since** | KurrentDB 26.1 | KurrentDB 25.1 | | **Record ordering** | Interleaved. Records from different streams can be mixed, and their exact order is preserved in the global log. | Grouped. All records for a stream are sent together; ordering across streams is not guaranteed. | | **Consistency checks** | Decoupled. Can validate the state of any stream, including streams not being written to. | Coupled. Expected state is specified per stream being written to. | ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ### AppendRecords ::: note This feature is only available in KurrentDB 26.1 and later. ::: `AppendRecords` appends events to one or more streams atomically. Each record specifies which stream it targets, and the exact order of records is preserved in the global log across all streams. #### Single stream The simplest usage appends events to a single stream: ```cs var eventOne = new EventData( Uuid.NewUuid(), "OrderPlaced", "{\"orderId\": \"123\"}"u8.ToArray() ); var eventTwo = new EventData( Uuid.NewUuid(), "OrderShipped", "{\"orderId\": \"123\"}"u8.ToArray() ); await client.AppendRecordsAsync("order-123", [eventOne, eventTwo]); ``` When no expected state is provided, no consistency check is performed, which is equivalent to `StreamState.Any`. You can also pass an expected stream state for optimistic concurrency: ```cs await client.AppendRecordsAsync( "order-123", StreamState.NoStream, [eventOne, eventTwo] ); ``` #### Multiple streams Use `AppendRecord` to target different streams. Records can be interleaved freely, and the global log preserves the exact order you specify: ```cs var records = new[] { new AppendRecord("order-stream", new EventData( Uuid.NewUuid(), "OrderCreated", "{\"orderId\": \"123\"}"u8.ToArray() )), new AppendRecord("inventory-stream", new EventData( Uuid.NewUuid(), "ItemReserved", "{\"itemId\": \"abc\", \"quantity\": 2}"u8.ToArray() )), new AppendRecord("order-stream", new EventData( Uuid.NewUuid(), "OrderConfirmed", "{\"orderId\": \"123\"}"u8.ToArray() )), }; await client.AppendRecordsAsync(records); ``` #### Consistency checks Consistency checks let you validate the state of any stream, including streams you are not writing to, before the append is committed. All checks are evaluated atomically: if any check fails, the entire operation is rejected and an `AppendConsistencyViolationException` is thrown with details about every failing check and the actual state observed. ```cs var records = new[] { new AppendRecord("order-stream", new EventData( Uuid.NewUuid(), "OrderConfirmed", "{\"orderId\": \"123\"}"u8.ToArray() )), }; var checks = new[] { // ensure the inventory stream exists before confirming the order, // even though we are not writing to it new ConsistencyCheck.StreamStateCheck("inventory-stream", StreamState.StreamExists), }; await client.AppendRecordsAsync(records, checks); ``` Because checks are decoupled from writes, you can validate the state of streams you are not writing to, enabling patterns where a business decision depends on the state of multiple streams but the resulting event is written to only one of them. ### MultiStreamAppend ::: note This feature is only available in KurrentDB 25.1 and later. ::: `MultiStreamAppend` appends events to one or more streams atomically. Records are grouped per stream using `AppendStreamRequest`, where each request specifies a stream name, an expected state, and the events for that stream. ```cs AppendStreamRequest[] requests = [ new( "order-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "OrderCreated", Encoding.UTF8.GetBytes("{\"orderId\": \"21345\", \"amount\": 99.99}")) ] ), new( "inventory-stream", StreamState.Any, [ new EventData(Uuid.NewUuid(), "ItemReserved", Encoding.UTF8.GetBytes("{\"itemId\": \"abc123\", \"quantity\": 2}")) ] ) ]; await client.MultiStreamAppendAsync(requests); ``` Each stream can only appear once in the request. The expected state is validated per stream before the transaction is committed. The result returns the position of the last appended record in the transaction and a collection of responses for each stream. --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```cs const string userCertFile = "/path/to/user.crt"; const string userKeyFile = "/path/to/user.key"; var settings = KurentDBClientSettings.Create( $"kurrentdb://localhost:2113/?tls=true&tlsVerifyCert=true&userCertFile={userCertFile}&userKeyFile={userKeyFile}" ); await using var client = new KurentDBClient(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```cs await client.DeleteAsync(streamName, StreamState.Any); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```cs await client.TombstoneAsync(streamName, StreamState.Any); ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/getting-started.md' --- # Getting started Get started by connecting your application to KurrentDB. ## Connecting to KurrentDB To connect your application to KurrentDB, instantiate and configure the client. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: ### Required packages Add the `KurrentDB.Client` package to your project: ```bash dotnet add package KurrentDB.Client --version "1.1.*" ``` ### Connection string Each SDK has its own way of configuring the client, but the connection string can always be used. The KurrentDB connection string supports two schemas: `kurrentdb://` for connecting to a single-node server, and `kurrentdb+discover://` for connecting to a multi-node cluster. The difference between the two schemas is that when using `kurrentdb://`, the client will connect directly to the node; with `kurrentdb+discover://` schema the client will use the gossip protocol to retrieve the cluster information and choose the right node to connect to. Since version 22.10, ESDB supports gossip on single-node deployments, so `kurrentdb+discover://` schema can be used for connecting to any topology. The connection string has the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` There, `cluster.dns.name` is the name of a DNS `A` record that points to all the cluster nodes. Alternatively, you can list cluster nodes separated by comma instead of the cluster DNS name: ``` kurrentdb+discover://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ### Creating a client First, create a client and get it connected to the database. ```cs var client = new KurentDBClient(KurentDBClientSettings.Create("kurrentdb://admin:changeit@localhost:2113?tls=false&tlsVerifyCert=false")); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ### Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. ::: tip Server-side projections User-defined server-side projections require events to be serialized in JSON format. We use JSON for serialization in the documentation examples. ::: The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```cs using System.Text.Json; public class OrderCreated { public string? OrderId { get; set; } } var evt = new OrderCreated { OrderId = Guid.NewGuid().ToString("N"), }; var orderCreated = new EventData( Uuid.NewUuid(), "OrderCreated", JsonSerializer.SerializeToUtf8Bytes(evt) ); ``` ### Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```cs await client.AppendToStreamAsync("order-123", StreamState.Any, [orderCreated]); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ### Reading events Finally, we can read events back from the `order-123` stream. ```cs var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); var events = await result.ToListAsync(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures. The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/observability.md' --- # Observability The .NET client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to install exporters for your chosen observability platform: ```bash # For console output dotnet add package OpenTelemetry.Exporter.Console # For Jaeger dotnet add package OpenTelemetry.Exporter.Jaeger # For OTLP (OpenTelemetry Protocol) dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol # For Seq dotnet add package Seq.Extensions.Logging ``` ## Basic Configuration Configure instrumentation using the `AddKurentDBClientInstrumentation()` extension method. Here's a minimal setup: ```cs {15} using KurrentDB.Client.Extensions.OpenTelemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry.Resources; using OpenTelemetry.Trace; const string serviceName = "my-eventstore-app"; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService(serviceName)) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() ); }) .Build(); await host.RunAsync(); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=dotnet). You can configure multiple exporters simultaneously: ```cs {10-18} using OpenTelemetry.Exporter; var host = Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddOpenTelemetry() .ConfigureResource(builder => builder.AddService("my-eventstore-app")) .WithTracing(tracerBuilder => tracerBuilder .AddKurrentDBClientInstrumentation() .AddConsoleExporter() .AddJaegerExporter(options => { options.Endpoint = new Uri("http://localhost:14268/api/traces"); }) .AddOtlpExporter(options => { options.Endpoint = new Uri("http://localhost:4318/v1/traces"); }) ); }) .Build(); ``` For detailed configuration options, refer to the [OpenTelemetry .NET documentation](https://opentelemetry.io/docs/languages/dotnet/). ## Understanding Traces ### What Gets Traced The .NET client currently creates traces for append, catch-up and persistent subscription operations. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `otel.status_code` | Status code for the operation | `UNSET`, `OK`, `ERROR` | | `otel.status_description` | Status of a span | | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | ### Sample Trace Output Here's an example trace from a stream append operation: ```bash Activity.TraceId: 8da04787239dbb85c1f9c6fba1b1f0d6 Activity.SpanId: 4352ec4a66a20b95 Activity.TraceFlags: Recorded Activity.ActivitySourceName: kurrentdb Activity.DisplayName: streams.append Activity.Kind: Client Activity.StartTime: 2024-05-29T06:50:41.2519016Z Activity.Duration: 00:00:00.1500707 Activity.Tags: db.kurrentdb.stream: example-stream server.address: localhost server.port: 2113 db.system: kurrentdb db.operation: streams.append event.count: 3 StatusCode: Ok Resource associated with Activity: service.name: my-eventstore-app service.instance.id: 7316ef20-c354-4e64-97da-c1b99c2c28b0 service.version: 1.0.0 deployment.environment: production telemetry.sdk.name: opentelemetry telemetry.sdk.language: dotnet telemetry.sdk.version: 1.9.0 ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client To work with persistent subscriptions, you need to create an instance of the `KurentDBPersistentSubscriptionsClient`. This client is used to manage persistent subscriptions, including creating, updating, and deleting subscription groups. ```cs await using var client = new KurrentDBPersistentSubscriptionsClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToStreamAsync("order-123", "subscription-group", settings); ``` ### Subscribing to `$all` ```cs var settings = new PersistentSubscriptionSettings(); await client.CreateToAllAsync("subscription-group", StreamFilter.Prefix("user"), settings); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```cs await using var subscription = client.SubscribeToStream( "order-123", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); break; } } ``` | Parameter | Description | |:----------------------|:---------------------------------------------------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to subscribe to. | | `eventAppeared` | The action to call when an event arrives over the subscription. | | `subscriptionDropped` | The action to call if the subscription is dropped. | | `bufferSize` | The number of in-flight messages this client is allowed. **Default: 10** | ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```cs await using var subscription = client.SubscribeToAll("subscription-group", cancellationToken: ct); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): await HandleEvent(resolvedEvent); break; } } ``` The `SubscribeToAllAsync` method is identical to the `SubscribeToStreamAsync` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```cs await using var subscription = client.SubscribeToStream( "test-stream", "subscription-group", cancellationToken: ct ); await foreach (var message in subscription.Messages) { switch (message) { case PersistentSubscriptionMessage.SubscriptionConfirmation(var subscriptionId): Console.WriteLine($"Subscription {subscriptionId} to stream with manual acks started"); break; case PersistentSubscriptionMessage.Event(var resolvedEvent, _): try { await HandleEvent(resolvedEvent); await subscription.Ack(resolvedEvent); } catch (UnrecoverableException ex) { await subscription.Nack(PersistentSubscriptionNakEventAction.Park, ex.Message, resolvedEvent); } break; } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```cs var settings = new PersistentSubscriptionSettings( resolveLinkTos: true, checkPointLowerBound: 20 ); await client.UpdateToStreamAsync("order-123", "subscription-group", settings); ``` | Parameter | Description | |:--------------|:----------------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to update. | | `settings` | The settings to use when creating the subscription. | ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | |:------------------------|:----------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------| | `ResolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `StartFrom` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `ExtraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `MessageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `MaxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `LiveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `ReadBatchSize` | The number of events read at a time when paging through history. | `20` | | `HistoryBufferSize` | The number of events to cache when paging through history. | `500` | | `CheckPointAfter` | The amount of time to try to checkpoint after. | `2` seconds | | `MinCheckPointCount` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `MaxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | `1000` | | `MaxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `NamedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```cs try { await client.DeleteToStreamAsync("order-123", "subscription-group"); } catch (PersistentSubscriptionNotFoundException) { Console.WriteLine("Subscription group does not exist."); } catch (Exception ex) { Console.WriteLine($"Subscription to stream delete error: {ex.GetType()} {ex.Message}"); } ``` | Parameter | Description | |:--------------|:-----------------------------------------------| | `stream` | The stream the persistent subscription is on. | | `groupName` | The name of the subscription group to delete. | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client Projection management operations are exposed through the dedicated client. ```cs var client = new KurrentDBProjectionManagementClient( KurrentDBClientSettings.Create("kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false") ); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; await client.CreateContinuousAsync("count-events", js); ``` Trying to create projections with the same name will result in an error: ```cs var name = "count-events"; await client.CreateContinuousAsync(name, js); try { await client.CreateContinuousAsync(name, js); } catch (RpcException e) when (e.StatusCode is StatusCode.AlreadyExists) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("Conflict")) { // will be removed in a future release var format = $"{name} already exists"; Console.WriteLine(format); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```cs await client.RestartSubsystemAsync(); ``` ## Enable a projection Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.EnableAsync("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```cs try { await client.EnableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```cs await client.DisableAsync("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```cs try { await client.DisableAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Delete a projection This feature is currently not supported by the client. ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```cs await client.AbortAsync("countEvents_Abort"); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```cs try { await client.AbortAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```cs await client.ResetAsync("countEvents_Reset"); ``` Resetting a projection that does not exist will result in an error. ```cs try { await client.ResetAsync("projection that does not exists"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine(e.Message); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```cs const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, "fromAll().when()"); await client.UpdateAsync(name, js); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```cs try { await client.UpdateAsync("Update Not existing projection", "fromAll().when()"); } catch (RpcException e) when (e.StatusCode is StatusCode.NotFound) { Console.WriteLine(e.Message); } catch (RpcException e) when (e.Message.Contains("NotFound")) { // will be removed in a future release Console.WriteLine("'Update Not existing projection' does not exists and can not be updated"); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListAllAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs var details = client.ListContinuousAsync(); await foreach (var detail in details) Console.WriteLine( $@"{detail.Name}, {detail.Status}, {detail.CheckpointStatus}, {detail.Mode}, {detail.Progress}" ); ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```cs{18} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(state, event) { state.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); var status = await client.GetStatusAsync(name); Console.WriteLine( $@"{status?.Name}, {status?.Status}, {status?.CheckpointStatus}, {status?.Mode}, {status?.Progress}" ); ``` ## Get state Retrieves the state of a projection. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = $"count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); // give it some time to process and have a state. var document = await client.GetStateAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or you can retrieve the state as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetStateAsync(name); ``` ## Get result Retrieves the result of the named projection and partition. ```cs{20} const string js = """ fromAll() .when({ $init: function() { return { count: 0 }; }, $any: function(s, e) { s.count += 1; } }) .outputState(); """; var name = "count-events"; await client.CreateContinuousAsync(name, js); await Task.Delay(500); //give it some time to have a result. var document = await client.GetResultAsync(name); Console.WriteLine(document.RootElement.GetRawText()) ``` or it can be retrieved as a typed result: ```cs public class Result { public int count { get; set; } public override string ToString() => $"count= {count}"; }; var result = await client.GetResultAsync(name); ``` ## Projection Details The `ListAllAsync`, `ListContinuousAsync`, and `GetStatusAsync` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | |--------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `Status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/reading-events.md' --- # Reading Events There are two options for reading events from KurrentDB. You can either read from an individual stream, or read from the `$all` stream, which will return all events in the store. Each event in KurrentDB belongs to an individual stream. When reading events, pick the name of the stream from which you want to read the events and choose whether to read the stream forwards or backwards. All events have a `StreamPosition` and a `Position`. `StreamPosition` is a *big int* (unsigned 64-bit integer) and represents the place of the event in the stream. `Position` is the event's logical position, and is represented by `CommitPosition` and a `PreparePosition`. Note that when reading events you will supply a different "position" depending on whether you are reading from an individual stream or the `$all` stream. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadStreamAsync(Direction.Forwards, "order-123", StreamPosition.Start); ``` This will return an enumerable that can be iterated on: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading a stream, listed below. #### maxCount Passing in the max count will limit the number of events returned. #### resolveLinkTos When using projections to create new events, you can set whether the generated events are pointers to existing events. Setting this value to `true` tells KurrentDB to return the event as well as the event linking to it. #### configureOperationOptions You can use the `configureOperationOptions` argument to provide a function that will customise settings for each operation. #### userCredentials The `userCredentials` argument is optional. It is used to override the default credentials specified when creating the client instance. ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ### Reading from a revision Instead of providing the `StreamPosition` you can also provide a specific stream revision as a big int (unsigned 64-bit integer). You can use `FirstStreamPosition` and `LastStreamPosition` from a previous read result as the starting revision. ```cs{11} var orders = client.ReadStreamAsync( Direction.Forwards, "order-123", StreamPosition.Start ); if (orders.FirstStreamPosition is not null) { var customers = client.ReadStreamAsync( Direction.Forwards, "customer-456", orders.FirstStreamPosition ); } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```cs{2} var events = client.ReadStreamAsync( Direction.Backwards, "order-123", StreamPosition.End ); await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```cs{5} var result = client.ReadStreamAsync( Direction.Forwards, "order-123", revision: 10, maxCount: 20 ); if (await result.ReadState == ReadState.StreamNotFound) return; await foreach (var e in result) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log postion can either be a *stream position* `Start` or a *big int* (unsigned 64-bit integer): ```cs var events = client.ReadAllAsync(Direction.Forwards, Position.Start); ``` You can iterate asynchronously through the result: ```cs await foreach (var e in events) Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, resolveLinkTos: true ); ``` #### configureOperationOptions This argument is generic setting class for all operations that can be set on all operations executed against KurrentDB. #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```cs{4} var result = client.ReadAllAsync( Direction.Forwards, Position.Start, userCredentials: new UserCredentials("admin", "changeit"), cancellationToken: cancellationToken ); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *position* to the end: ```cs var events = client.ReadAllAsync(Direction.Backwards, Position.End); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```cs{4} var events = client.ReadAllAsync(Direction.Forwards, Position.Start); await foreach (var e in events) { if (e.Event.EventType.StartsWith("$")) continue; Console.WriteLine(Encoding.UTF8.GetString(e.OriginalEvent.Data.ToArray())); } ``` --- --- url: 'https://docs.kurrent.io/clients/dotnet/v1.4/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```cs await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` subscription:** ```cs await using var subscription = client.SubscribeToAll(FromAll.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start`, `End` or a 64-bit unsigned integer representing the stream revision): ```cs{3} await using var subscription = client.SubscribeToStream( "order-123", FromStream.After(StreamPosition.FromInt64(20)) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```cs{10} var result = await client.AppendToStreamAsync( "order-123", StreamState.NoStream, [ new EventData(Uuid.NewUuid(), "-", ReadOnlyMemory.Empty) ] ); await using var subscription = client.SubscribeToAll( FromAll.After(result.LogPosition) ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```cs // Stream await using var subscription = client.SubscribeToStream("order-123", FromStream.End); // $all await using var subscription = client.SubscribeToAll(FromAll.End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```cs{4} await using var subscription = client.SubscribeToStream( "$et-order", FromStream.Start, resolveLinkTos: true ); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; } } ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides a `subscriptionDropped` callback, which will get called when the subscription breaks. The `subscriptionDropped` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. The possible reasons for a subscription to drop are: | Reason | Why it might happen | |:------------------|:---------------------------------------------------------------------------------------------------------------------| | `Disposed` | The client canceled or disposed of the subscription. | | `SubscriberError` | An error occurred while handling an event in the subscription handler. | | `ServerError` | An error occurred on the server, and the server closed the subscription. Check the server logs for more information. | Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```cs{1,10} var checkpoint = FromStream.Start; // or read from a persistent store await using var subscription = client.SubscribeToStream("order-123", checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); checkpoint = FromStream.After(evnt.OriginalEventNumber); break; } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```cs{1,13} var checkpoint = FromAll.Start; // or read from a persistent store await using var subscription = client.SubscribeToAll(checkpoint); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); if (evnt.OriginalPosition is not null) checkpoint = FromAll.After(evnt.OriginalPosition.Value); break; } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `CaughtUp` message on the subscription. ```cs{8-10} await using var subscription = client.SubscribeToStream("order-123", FromStream.Start); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var evnt): Console.WriteLine($"Received event {evnt.OriginalEventNumber}@{evnt.OriginalStreamId}"); break; case StreamMessage.CaughtUp: Console.WriteLine("Caught up to live mode"); break; } } ``` ::: tip The `CaughtUp` message is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this message will be emitted right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```cs{3} await using var subscription = client.SubscribeToAll( FromAll.Start, userCredentials: new UserCredentials("admin", "changeit") ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(StreamFilter.Prefix("test-", "other-")) ); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```cs await using var subscription = client.SubscribeToAll( FromAll.Start, filterOptions: new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()) ); ``` ### Filtering by event type **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.Prefix("customer-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( EventTypeFilter.RegularExpression("^user|^company") ); ``` ### Filtering by stream name **By prefix:** ```cs var filterOptions = new SubscriptionFilterOptions(StreamFilter.Prefix("user-")); ``` **By regular expression:** ```cs var filterOptions = new SubscriptionFilterOptions( StreamFilter.RegularExpression("^account|^savings") ); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The client SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```cs{10-13} var filterOptions = new SubscriptionFilterOptions(EventTypeFilter.ExcludeSystemEvents()); await using var subscription = client.SubscribeToAll(FromAll.Start, filterOptions: filterOptions); await foreach (var message in subscription.Messages) { switch (message) { case StreamMessage.Event(var e): Console.WriteLine($"{e.Event.EventType} @ {e.Event.Position.CommitPosition}"); break; case StreamMessage.AllStreamCheckpointReached(var p): // Save commit position to a persistent store as a checkpoint Console.WriteLine($"checkpoint taken at {p.CommitPosition}"); break; } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```cs{3} var filterOptions = new SubscriptionFilterOptions( filter: EventTypeFilter.ExcludeSystemEvents(), checkpointInterval: 1000 ); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/appending-events.md' --- # Appending events When you start working with EventStoreDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to EventStoreDB is to create an `EventData` object and call `appendToStream` method. ```go{20-24} type OrderPlaced struct { OrderId string `json:"orderId"` Amount float64 `json:"amount"` } data := OrderPlaced{ OrderId: "ORD-123", Amount: 49.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := esdb.AppendToStreamOptions{ ExpectedRevision: esdb.NoStream{}, } result, err := db.AppendToStream(context.Background(), "orders-123", options, esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` `AppendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to EventStoreDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, EventStoreDB will only append one of the events to the stream. For example, the following code will only append a single event: ```go{7-12} _, err = db.AppendToStream(context.Background(), "orders-456", esdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "orders-456", esdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of EventStoreDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. EventStoreDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `esdb.ContentTypeJson` and `esdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform EventStoreDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```go{12,15-19,34-39} data := OrderPlaced{ OrderId: "ORD-001", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := esdb.AppendToStreamOptions{ ExpectedRevision: esdb.NoStream{}, } _, err = db.AppendToStream(context.Background(), "order-123", options, esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) if err != nil { panic(err) } bytes, err = json.Marshal(OrderPlaced{ OrderId: "ORD-002", Amount: 45.50, }) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "order-123", options, esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` There are several available expected revision options: * `esdb.Any` - No concurrency check * `esdb.NoStream{}` - Stream should not exist * `esdb.StreamExists{}` - Stream should exist * `esdb.StreamRevision{}` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from EventStoreDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```go{6,32,35-39,50-54} ropts := esdb.ReadStreamOptions{ Direction: esdb.Backwards, From: esdb.End{}, } stream, err := db.ReadStream(context.Background(), "orders-123", ropts, 1) if err != nil { panic(err) } defer stream.Close() lastEvent, err := stream.Recv() if err != nil { panic(err) } data := OrderPlaced{ OrderId: "ORD-123", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } aopts := esdb.AppendToStreamOptions{ ExpectedRevision: lastEvent.OriginalStreamRevision(), } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) data = OrderPlaced{ OrderId: "ORD-123", Amount: 39.99, } bytes, err = json.Marshal(data) if err != nil { panic(err) } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```go{5-8} result, err := db.AppendToStream( context.Background(), "orders", esdb.AppendToStreamOptions{ Authenticated: &esdb.Credentials{ Login: "admin", Password: "changeit" } }, event ) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```go settings, err := kurrentdb.ParseConnectionString("esdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}") if err != nil { panic(err) } db, err := kurrentdb.NewClient(settings) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/delete-stream.md' --- # Deleting Events In EventStoreDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, EventStoreDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in EventStoreDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```go options := esdb.DeleteStreamOptions{ ExpectedRevision: esdb.Any{}, } _, err = client.DeleteStream(context.Background(), streamName, options) ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in EventStoreDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```go options := esdb.TombstoneStreamOptions{ ExpectedRevision: esdb.Any{}, } _, err = client.TombstoneStream(context.Background(), streamName, options) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/getting-started.md' --- # Getting started This guide will help you get started with EventStoreDB in your Java application. It covers the basic steps to connect to EventStoreDB, create events, append them to streams, and read them back. ## Required packages Add the following dependencies to your `go.mod` file: ```bash go get http://github.com/EventStore/EventStore-Client-Go/v4/esdb ``` ## Connecting to EventStoreDB To connect your application to EventStoreDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to EventStoreDB is using secure mode (which is the default). However, if your EventStoreDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: EventStoreDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`esdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`esdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `esdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `esdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, esdb supports gossip on single-node deployments, so `esdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` esdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` esdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` esdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `esdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```go settings, err := esdb.ParseConnectionString("esdb://localhost:2113?tls=false") if err != nil { panic(err) } db, err := esdb.NewClient(settings) ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to EventStoreDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```go type OrderItem struct { ProductId string `json:"productId"` Quantity int `json:"quantity"` Price float64 `json:"price"` } type OrderCreated struct { OrderId string `json:"orderId"` CustomerId string `json:"customerId"` Items []OrderItem `json:"items"` TotalAmount float64 `json:"totalAmount"` Status string `json:"status"` } orderCreatedEvent := OrderCreated{ OrderId: uuid.NewString(), CustomerId: "customer-123", Items: []OrderItem{ {ProductId: "product-456", Quantity: 2, Price: 29.99}, {ProductId: "product-789", Quantity: 1, Price: 15.50}, }, TotalAmount: 75.48, Status: "pending", } data, err := json.Marshal(orderCreatedEvent) if err != nil { panic(err) } eventData := esdb.EventData{ ContentType: esdb.ContentTypeJson, EventType: "OrderCreated", Data: data, } ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```go _, err = db.AppendToStream(context.Background(), "orders", esdb.AppendToStreamOptions{}, eventData) ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ```go stream, err := db.ReadStream(context.Background(), "orders", esdb.ReadStreamOptions{}, 10) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } // Process the order event fmt.Printf("Order event: %s - %s\n", event.Event.EventType, string(event.Event.Data)) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```go err := client.CreatePersistentSubscription(context.Background(), "order-123", "subscription-group", esdb.PersistentStreamSubscriptionOptions{}) if err != nil { panic(err) } ``` ### Subscribing to `$all` ```go options := esdb.PersistentAllSubscriptionOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.StreamFilterType, Prefixes: []string{"test"}, }, } err := client.CreatePersistentSubscriptionToAll(context.Background(), "subscription-group", options) if err != nil { panic(err) } ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```go sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", esdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```go sub, err := client.SubscribeToPersistentSubscriptionToAll(context.Background(), "subscription-group", esdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```go{11} sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", esdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `NackActionPark` | Park the message and do not resend. Put it on poison queue. | | `NackActionRetry` | Explicitly retry the message. | | `NackActionSkip` | Skip this message do not resend and do not put in poison queue. | | `NackActionStop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```go await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```go await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/projections.md' --- # Projection management The client provides a way to manage projections in EventStoreDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a client ```go conf, err := esdb.ParseConnectionString(connectionString) if err != nil { panic(err) } client, err := esdb.NewProjectionClient(conf) ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```go script := ` fromAll() .when({ $init:function(){ return { count: 0 } }, myEventUpdatedType: function(state, event){ state.count += 1; } }) .transformBy(function(state){ state.count = 10; }) .outputState() ` name := fmt.Sprintf("countEvent_Create_%s", uuid.New()) err := client.Create(context.Background(), name, script, esdb.CreateProjectionOptions{}) if err != nil { panic(err) } ``` Trying to create projections with the same name will result in an error: ```go err := client.Create(context.Background(), name, script, esdb.CreateProjectionOptions{}) if esdbErr, ok := esdb.FromError(err); !ok { if esdbErr.IsErrorCode(esdb.ErrorCodeUnknown) && strings.Contains(esdbErr.Err().Error(), "Conflict") { log.Printf("projection %s already exists", name) return } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```go err := client.RestartSubsystem(context.Background(), esdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Enable(context.Background(), "$by_category", esdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```go err := client.Enable(context.Background(), "projection that doesn't exist", esdb.GenericProjectionOptions{}) if esdbError, ok := esdb.FromError(err); !ok { if esdbError.IsErrorCode(esdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Disable(context.Background(), "$by_category", esdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```go err := client.Disable(context.Background(), "projection that doesn't exist", esdb.GenericProjectionOptions{}) if esdbError, ok := esdb.FromError(err); !ok { if esdbError.IsErrorCode(esdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Delete a projection ```go err := client.Delete(context.Background(), "$by_category", esdb.DeleteProjectionOptions{}) if err != nil { panic(err) } ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```go err := client.Abort(context.Background(), "$by_category", esdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```go err := client.Abort(context.Background(), "projection that doesn't exist", esdb.GenericProjectionOptions{}) if esdbError, ok := esdb.FromError(err); !ok { if esdbError.IsErrorCode(esdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```go err := client.Reset(context.Background(), "$by_category", esdb.ResetProjectionOptions{}) if err != nil { panic(err) } ``` Resetting a projection that does not exist will result in an error. ```go err := client.Reset(context.Background(), "projection that doesn't exist", esdb.ResetProjectionOptions{}) if esdbError, ok := esdb.FromError(err); !ok { if esdbError.IsErrorCode(esdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```go err := client.Create(context.Background(), name, script, esdb.CreateProjectionOptions{}) if err != nil { panic(err) } err = client.Update(context.Background(), name, newScript, esdb.UpdateProjectionOptions{}) if err != nil { panic(err) } ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```go err := client.Update(context.Background(), "projection that doesn't exist", script, esdb.UpdateProjectionOptions{}) if esdbError, ok := esdb.FromError(err); !ok { if esdbError.IsErrorCode(esdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListAll(context.Background(), esdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListContinuous(context.Background(), esdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projection, err := client.GetStatus(context.Background(), "$by_category", esdb.GenericProjectionOptions{}) if err != nil { panic(err) } log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) ``` ## Get state Retrieves the state of a projection. ```go type Foobar struct { Count int64 } value, err := client.GetState(context.Background(), projectionName, esdb.GetStateProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var foobar Foobar if err = json.Unmarshal(jsonContent, &foobar); err != nil { panic(err) } log.Printf("count %d", foobar.Count) ``` ## Get result Retrieves the result of the named projection and partition. ```go type Baz struct { Result int64 } value, err := client.GetResult(context.Background(), projectionName, esdb.GetResultProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var baz Baz if err = json.Unmarshal(jsonContent, &baz); err != nil { panic(err) } log.Printf("result %d", baz.Result) ``` ## Projection Details The `ListAll`, `ListContinuous`, and `GetStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/reading-events.md' --- # Reading Events EventStoreDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in EventStoreDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in EventStoreDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `esdb.Start{}` to begin from the very beginning of the stream * Use `esdb.End{}` to begin from the current end of the stream * Use `esdb.StreamRevision{}` with a specific revision number (64-bit signed integer) ```go{3} options := esdb.ReadStreamOptions{ From: esdb.Start{}, Direction: esdb.Forwards, } stream, err := db.ReadStream(context.Background(), "some-stream", options, 100) if err != nil { panic(err) } defer stream.Close() ``` You can also start reading from a specific revision in the stream: ```go{2} options:= esdb.ReadStreamOptions{ From: esdb.Revision(10), } ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", esdb.ReadStreamOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```go options:= esdb.ReadAllOptions{ ResolveLinkTos: true, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := esdb.ReadStreamOptions{ From: esdb.Start{}, Authenticated: &esdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```go{2-3} options:= esdb.ReadStreamOptions{ Direction: esdb.Backwards, From: esdb.End{}, } stream, err := db.ReadStream(context.Background(), "some-stream", ropts, 10) ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `*ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `Recv()` method will return an error with the code `ErrorCodeResourceNotFound`. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```go{13-14} stream, err := db.ReadStream(context.Background(), "order-123", esdb.ReadStreamOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if err, ok := esdb.FromError(err); !ok { if err.Code() == esdb.ErrorCodeResourceNotFound { fmt.Print("Stream not found") } else if errors.Is(err, io.EOF) { break } else { panic(err) } } fmt.Printf("Event> %v", event) } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```go{1-4} options := esdb.ReadAllOptions{ From: esdb.Start{}, Direction: esdb.Forwards, } stream, err := db.ReadAll(context.Background(), options, 100) ``` You can also start reading from a specific position in the transaction log: ```go{3-6} options := esdb.ReadAllOptions{ ResolveLinkTos: false, From: &esdb.Position{ Commit: 10, Prepare: 10, }, } stream, err := db.ReadAll(context.Background(), ropts, 100) ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", esdb.ReadAllOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```go{3} options := esdb.ReadAllOptions{ From: esdb.Start{}, ResolveLinkTos: true, Direction: esdb.Forwards, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := esdb.ReadAllOptions{ From: esdb.Start{}, Authenticated: &esdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `esdb.Backwards`: ```go{2-3} options := esdb.ReadAllOptions{ Direction: esdb.Backwards, From: esdb.End{}, } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events EventStoreDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```go{22-24} stream, err := db.ReadAll(context.Background(), esdb.ReadAllOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) if strings.HasPrefix(event.OriginalEvent().EventType, "$") { continue } fmt.Printf("Event> %v", event) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/legacy/v4.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```go stream, err := db.SubscribeToStream(context.Background(), "order-123", esdb.SubscribeToStreamOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` **`$all` subscription:** ```go stream, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start{}`, `End{}` or a 64-bit signed integer representing the revision number): ```go{2} db.SubscribeToStream(context.Background(), "order-123", esdb.SubscribeToStreamOptions{ From: esdb.Revision(20), }) ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```go{2-5} db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ From: esdb.Position{ Commit: 1_056, Prepare: 1_056, }, }) ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```go // Stream options = esdb.SubscribeToStreamOptions{ From: esdb.End{}, } // $all db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ From: esdb.End{}, }) ``` ## Resolving link-to events Link-to events point to events in other streams in EventStoreDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```go{3} options = esdb.SubscribeToStreamOptions{ From: esdb.Start{}, ResolveLinkTos: true, } db.SubscribeToStream(context.Background(), "$et-orders", options) ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```go{22-25} options = esdb.SubscribeToStreamOptions{ From: esdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "some-stream", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = esdb.Revision(event.EventAppeared.OriginalEvent().EventNumber) } } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```go{21-24} options = esdb.SubscribeToAllOptions{ From: esdb.Start{}, } for { stream, err := db.SubscribeToAll(context.Background(), options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = event.EventAppeared.OriginalEvent().Position } } } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```go{22-25} options = esdb.SubscribeToStreamOptions{ From: esdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "order-123", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.CaughtUp != nil { // handles the caught up event fmt.Println("Caught up to live mode") } if event.EventAppeared != nil { // handles the event } } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```go{2-5} db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Authenticated: &esdb.Credentials{ Login: "admin", Password: "changeit", }, }) ``` ## Server-side Filtering EventStoreDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```go db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.StreamFilterType, Prefixes: []string{"test-", "other-"}, }, }) ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```go sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: esdb.ExcludeSystemEventsFilter(), }) ``` ### Filtering by event type **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.EventFilterType, Prefixes: []string{"customer-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.EventFilterType, Regex: "^user|^company", }, }) ``` ### Filtering by stream name **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.StreamFilterType, Prefixes: []string{"user-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.StreamFilterType, Regex: "^user|^company", }, }) ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```go{11-14} for { event := sub.Recv() if event.EventAppeared != nil { streamId := event.EventAppeared.OriginalEvent().StreamID revision := event.EventAppeared.OriginalEvent().EventNumber fmt.Printf("received event %v@%v", revision, streamId) } if event.CheckPointReached != nil { // Save commit position to a persistent store as a checkpoint fmt.Printf("checkpoint taken at %v", event.CheckPointReached.Commit) } if event.SubscriptionDropped != nil { break } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```go{6} sub, err := db.SubscribeToAll(context.Background(), esdb.SubscribeToAllOptions{ Filter: &esdb.SubscriptionFilter{ Type: esdb.EventFilterType, Regex: "/^[^\\$].*/", }, CheckpointInterval: 1, }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```go{20-24} type OrderPlaced struct { OrderId string `json:"orderId"` Amount float64 `json:"amount"` } data := OrderPlaced{ OrderId: "ORD-123", Amount: 49.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := kurrentdb.AppendToStreamOptions{ StreamState: kurrentdb.NoStream{}, } result, err := db.AppendToStream(context.Background(), "orders-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` `AppendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```go{7-12} _, err = db.AppendToStream(context.Background(), "orders-456", kurrentdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "orders-456", kurrentdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `kurrentdb.ContentTypeJson` and `kurrentdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```go{12,15-19,34-39} data := OrderPlaced{ OrderId: "ORD-001", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := kurrentdb.AppendToStreamOptions{ StreamState: kurrentdb.NoStream{}, } _, err = db.AppendToStream(context.Background(), "order-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) if err != nil { panic(err) } bytes, err = json.Marshal(OrderPlaced{ OrderId: "ORD-002", Amount: 45.50, }) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "order-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` There are several available expected revision options: * `kurrentdb.Any` - No concurrency check * `kurrentdb.NoStream{}` - Stream should not exist * `kurrentdb.StreamExists{}` - Stream should exist * `kurrentdb.StreamRevision{}` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```go{6,32,35-39,50-54} ropts := kurrentdb.ReadStreamOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } stream, err := db.ReadStream(context.Background(), "orders-123", ropts, 1) if err != nil { panic(err) } defer stream.Close() lastEvent, err := stream.Recv() if err != nil { panic(err) } data := OrderPlaced{ OrderId: "ORD-123", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } aopts := kurrentdb.AppendToStreamOptions{ StreamState: lastEvent.OriginalStreamRevision(), } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) data = OrderPlaced{ OrderId: "ORD-123", Amount: 39.99, } bytes, err = json.Marshal(data) if err != nil { panic(err) } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```go{5-8} result, err := db.AppendToStream( context.Background(), "orders", kurrentdb.AppendToStreamOptions{ Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit" } }, event ) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```go settings, err := kurrentdb.ParseConnectionString("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}") if err != nil { panic(err) } db, err := kurrentdb.NewClient(settings) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```go options := kurrentdb.DeleteStreamOptions{ StreamState: kurrentdb.Any{}, } _, err = client.DeleteStream(context.Background(), streamName, options) ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```go options := kurrentdb.TombstoneStreamOptions{ StreamState: kurrentdb.Any{}, } _, err = client.TombstoneStream(context.Background(), streamName, options) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Go application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following dependencies to your `go.mod` file: ```bash go get http://github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```go settings, err := kurrentdb.ParseConnectionString("kurrentdb://localhost:2113?tls=false") if err != nil { panic(err) } db, err := kurrentdb.NewClient(settings) ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```go type OrderItem struct { ProductId string `json:"productId"` Quantity int `json:"quantity"` Price float64 `json:"price"` } type OrderCreated struct { OrderId string `json:"orderId"` CustomerId string `json:"customerId"` Items []OrderItem `json:"items"` TotalAmount float64 `json:"totalAmount"` Status string `json:"status"` } orderCreatedEvent := OrderCreated{ OrderId: uuid.NewString(), CustomerId: "customer-123", Items: []OrderItem{ {ProductId: "product-456", Quantity: 2, Price: 29.99}, {ProductId: "product-789", Quantity: 1, Price: 15.50}, }, TotalAmount: 75.48, Status: "pending", } data, err := json.Marshal(orderCreatedEvent) if err != nil { panic(err) } eventData := kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderCreated", Data: data, } ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```go _, err = db.AppendToStream(context.Background(), "orders", kurrentdb.AppendToStreamOptions{}, eventData) ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ```go stream, err := db.ReadStream(context.Background(), "orders", kurrentdb.ReadStreamOptions{}, 10) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } // Process the order event fmt.Printf("Order event: %s - %s\n", event.Event.EventType, string(event.Event.Data)) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```go err := client.CreatePersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.PersistentStreamSubscriptionOptions{}) if err != nil { panic(err) } ``` ### Subscribing to `$all` ```go options := kurrentdb.PersistentAllSubscriptionOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"test"}, }, } err := client.CreatePersistentSubscriptionToAll(context.Background(), "subscription-group", options) if err != nil { panic(err) } ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```go sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```go sub, err := client.SubscribeToPersistentSubscriptionToAll(context.Background(), "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```go{11} sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `NackActionPark` | Park the message and do not resend. Put it on poison queue. | | `NackActionRetry` | Explicitly retry the message. | | `NackActionSkip` | Skip this message do not resend and do not put in poison queue. | | `NackActionStop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```go await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```go await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a client ```go conf, err := kurrentdb.ParseConnectionString(connectionString) if err != nil { panic(err) } client, err := kurrentdb.NewProjectionClient(conf) ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```go script := ` fromAll() .when({ $init:function(){ return { count: 0 } }, myEventUpdatedType: function(state, event){ state.count += 1; } }) .transformBy(function(state){ state.count = 10; }) .outputState() ` name := fmt.Sprintf("countEvent_Create_%s", uuid.New()) err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if err != nil { panic(err) } ``` Trying to create projections with the same name will result in an error: ```go err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if esdbErr, ok := kurrentdb.FromError(err); !ok { if esdbErr.IsErrorCode(kurrentdb.ErrorCodeUnknown) && strings.Contains(esdbErr.Err().Error(), "Conflict") { log.Printf("projection %s already exists", name) return } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```go err := client.RestartSubsystem(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Enable(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```go err := client.Enable(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Disable(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```go err := client.Disable(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Delete a projection ```go err := client.Delete(context.Background(), "$by_category", kurrentdb.DeleteProjectionOptions{}) if err != nil { panic(err) } ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```go err := client.Abort(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```go err := client.Abort(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```go err := client.Reset(context.Background(), "$by_category", kurrentdb.ResetProjectionOptions{}) if err != nil { panic(err) } ``` Resetting a projection that does not exist will result in an error. ```go err := client.Reset(context.Background(), "projection that doesn't exist", kurrentdb.ResetProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```go err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if err != nil { panic(err) } err = client.Update(context.Background(), name, newScript, kurrentdb.UpdateProjectionOptions{}) if err != nil { panic(err) } ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```go err := client.Update(context.Background(), "projection that doesn't exist", script, kurrentdb.UpdateProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListAll(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListContinuous(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projection, err := client.GetStatus(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) ``` ## Get state Retrieves the state of a projection. ```go type Foobar struct { Count int64 } value, err := client.GetState(context.Background(), projectionName, kurrentdb.GetStateProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var foobar Foobar if err = json.Unmarshal(jsonContent, &foobar); err != nil { panic(err) } log.Printf("count %d", foobar.Count) ``` ## Get result Retrieves the result of the named projection and partition. ```go type Baz struct { Result int64 } value, err := client.GetResult(context.Background(), projectionName, kurrentdb.GetResultProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var baz Baz if err = json.Unmarshal(jsonContent, &baz); err != nil { panic(err) } log.Printf("result %d", baz.Result) ``` ## Projection Details The `ListAll`, `ListContinuous`, and `GetStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `kurrentdb.Start{}` to begin from the very beginning of the stream * Use `kurrentdb.End{}` to begin from the current end of the stream * Use `kurrentdb.StreamRevision{}` with a specific revision number (64-bit signed integer) ```go{3} options := kurrentdb.ReadStreamOptions{ From: kurrentdb.Start{}, Direction: kurrentdb.Forwards, } stream, err := db.ReadStream(context.Background(), "some-stream", options, 100) if err != nil { panic(err) } defer stream.Close() ``` You can also start reading from a specific revision in the stream: ```go{2} options:= kurrentdb.ReadStreamOptions{ From: kurrentdb.Revision(10), } ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", kurrentdb.ReadStreamOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```go options:= kurrentdb.ReadAllOptions{ ResolveLinkTos: true, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := kurrentdb.ReadStreamOptions{ From: kurrentdb.Start{}, Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```go{2-3} options:= kurrentdb.ReadStreamOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } stream, err := db.ReadStream(context.Background(), "some-stream", ropts, 10) ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `*ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `Recv()` method will return an error with the code `ErrorCodeResourceNotFound`. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```go{13-14} stream, err := db.ReadStream(context.Background(), "order-123", kurrentdb.ReadStreamOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if err, ok := kurrentdb.FromError(err); !ok { if err.Code() == kurrentdb.ErrorCodeResourceNotFound { fmt.Print("Stream not found") } else if errors.Is(err, io.EOF) { break } else { panic(err) } } fmt.Printf("Event> %v", event) } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```go{1-4} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, Direction: kurrentdb.Forwards, } stream, err := db.ReadAll(context.Background(), options, 100) ``` You can also start reading from a specific position in the transaction log: ```go{3-6} options := kurrentdb.ReadAllOptions{ ResolveLinkTos: false, From: &kurrentdb.Position{ Commit: 10, Prepare: 10, }, } stream, err := db.ReadAll(context.Background(), ropts, 100) ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", kurrentdb.ReadAllOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```go{3} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, ResolveLinkTos: true, Direction: kurrentdb.Forwards, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `kurrentdb.Backwards`: ```go{2-3} options := kurrentdb.ReadAllOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```go{22-24} stream, err := db.ReadAll(context.Background(), kurrentdb.ReadAllOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) if strings.HasPrefix(event.OriginalEvent().EventType, "$") { continue } fmt.Printf("Event> %v", event) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```go stream, err := db.SubscribeToStream(context.Background(), "order-123", kurrentdb.SubscribeToStreamOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` **`$all` subscription:** ```go stream, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start{}`, `End{}` or a 64-bit signed integer representing the revision number): ```go{2} db.SubscribeToStream(context.Background(), "order-123", kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Revision(20), }) ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```go{2-5} db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ From: kurrentdb.Position{ Commit: 1_056, Prepare: 1_056, }, }) ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```go // Stream options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.End{}, } // $all db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ From: kurrentdb.End{}, }) ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```go{3} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, ResolveLinkTos: true, } db.SubscribeToStream(context.Background(), "$et-orders", options) ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```go{22-25} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "some-stream", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = kurrentdb.Revision(event.EventAppeared.OriginalEvent().EventNumber) } } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```go{21-24} options = kurrentdb.SubscribeToAllOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToAll(context.Background(), options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = event.EventAppeared.OriginalEvent().Position } } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```go{22-25} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "order-123", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.CaughtUp != nil { // handles the caught up event fmt.Println("Caught up to live mode") } if event.EventAppeared != nil { // handles the event } } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```go{2-5} db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, }) ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```go db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"test-", "other-"}, }, }) ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: kurrentdb.ExcludeSystemEventsFilter(), }) ``` ### Filtering by event type **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Prefixes: []string{"customer-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Regex: "^user|^company", }, }) ``` ### Filtering by stream name **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"user-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Regex: "^user|^company", }, }) ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```go{11-14} for { event := sub.Recv() if event.EventAppeared != nil { streamId := event.EventAppeared.OriginalEvent().StreamID revision := event.EventAppeared.OriginalEvent().EventNumber fmt.Printf("received event %v@%v", revision, streamId) } if event.CheckPointReached != nil { // Save commit position to a persistent store as a checkpoint fmt.Printf("checkpoint taken at %v", event.CheckPointReached.Commit) } if event.SubscriptionDropped != nil { break } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```go{6} sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Regex: "/^[^\\$].*/", }, CheckpointInterval: 1, }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```go{20-24} type OrderPlaced struct { OrderId string `json:"orderId"` Amount float64 `json:"amount"` } data := OrderPlaced{ OrderId: "ORD-123", Amount: 49.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := kurrentdb.AppendToStreamOptions{ StreamState: kurrentdb.NoStream{}, } result, err := db.AppendToStream(context.Background(), "orders-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` `AppendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```go{7-12} _, err = db.AppendToStream(context.Background(), "orders-456", kurrentdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "orders-456", kurrentdb.AppendToStreamOptions{}, event) if err != nil { panic(err) } ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `kurrentdb.ContentTypeJson` and `kurrentdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```go{12,15-19,34-39} data := OrderPlaced{ OrderId: "ORD-001", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } options := kurrentdb.AppendToStreamOptions{ StreamState: kurrentdb.NoStream{}, } _, err = db.AppendToStream(context.Background(), "order-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) if err != nil { panic(err) } bytes, err = json.Marshal(OrderPlaced{ OrderId: "ORD-002", Amount: 45.50, }) if err != nil { panic(err) } // attempt to append the same event again _, err = db.AppendToStream(context.Background(), "order-123", options, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` There are several available expected revision options: * `kurrentdb.Any` - No concurrency check * `kurrentdb.NoStream{}` - Stream should not exist * `kurrentdb.StreamExists{}` - Stream should exist * `kurrentdb.StreamRevision{}` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```go{6,32,35-39,50-54} ropts := kurrentdb.ReadStreamOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } stream, err := db.ReadStream(context.Background(), "orders-123", ropts, 1) if err != nil { panic(err) } defer stream.Close() lastEvent, err := stream.Recv() if err != nil { panic(err) } data := OrderPlaced{ OrderId: "ORD-123", Amount: 29.99, } bytes, err := json.Marshal(data) if err != nil { panic(err) } aopts := kurrentdb.AppendToStreamOptions{ StreamState: lastEvent.OriginalStreamRevision(), } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) data = OrderPlaced{ OrderId: "ORD-123", Amount: 39.99, } bytes, err = json.Marshal(data) if err != nil { panic(err) } _, err = db.AppendToStream(context.Background(), "orders-123", aopts, kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderPlaced", Data: bytes, }) ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```go{5-8} result, err := db.AppendToStream( context.Background(), "orders", kurrentdb.AppendToStreamOptions{ Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit" } }, event ) ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ```go type OrderCreated struct { OrderId string `json:"orderId"` Amount float64 `json:"amount"` } type PaymentProcessed struct { PaymentId string `json:"paymentId"` Amount float64 `json:"amount"` Method string `json:"method"` } metadata := map[string]string{ "source": "web-store", } metadataBytes, _ := json.Marshal(metadata) orderData, _ := json.Marshal(OrderCreated{OrderId: "12345", Amount: 99.99}) paymentData, _ := json.Marshal(PaymentProcessed{PaymentId: "PAY-789", Amount: 99.99, Method: "credit_card"}) requests := []kurrentdb.AppendStreamRequest{ { StreamName: "order-stream-1", Events: slices.Values([]kurrentdb.EventData{{ EventID: uuid.New(), EventType: "OrderCreated", ContentType: kurrentdb.ContentTypeJson, Data: orderData, Metadata: metadataBytes, }}), ExpectedStreamState: kurrentdb.Any{}, }, { StreamName: "payment-stream-1", Events: slices.Values([]kurrentdb.EventData{{ EventID: uuid.New(), EventType: "PaymentProcessed", ContentType: kurrentdb.ContentTypeJson, Data: paymentData, Metadata: metadataBytes, }}), ExpectedStreamState: kurrentdb.Any{}, }, } result, err := client.MultiStreamAppend(context.Background(), slices.Values(requests)) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```go settings, err := kurrentdb.ParseConnectionString("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}") if err != nil { panic(err) } db, err := kurrentdb.NewClient(settings) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```go options := kurrentdb.DeleteStreamOptions{ StreamState: kurrentdb.Any{}, } _, err = client.DeleteStream(context.Background(), streamName, options) ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```go options := kurrentdb.TombstoneStreamOptions{ StreamState: kurrentdb.Any{}, } _, err = client.TombstoneStream(context.Background(), streamName, options) ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Go application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following dependencies to your `go.mod` file: ```bash go get http://github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```go settings, err := kurrentdb.ParseConnectionString("kurrentdb://localhost:2113?tls=false") if err != nil { panic(err) } db, err := kurrentdb.NewClient(settings) ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```go type OrderItem struct { ProductId string `json:"productId"` Quantity int `json:"quantity"` Price float64 `json:"price"` } type OrderCreated struct { OrderId string `json:"orderId"` CustomerId string `json:"customerId"` Items []OrderItem `json:"items"` TotalAmount float64 `json:"totalAmount"` Status string `json:"status"` } orderCreatedEvent := OrderCreated{ OrderId: uuid.NewString(), CustomerId: "customer-123", Items: []OrderItem{ {ProductId: "product-456", Quantity: 2, Price: 29.99}, {ProductId: "product-789", Quantity: 1, Price: 15.50}, }, TotalAmount: 75.48, Status: "pending", } data, err := json.Marshal(orderCreatedEvent) if err != nil { panic(err) } eventData := kurrentdb.EventData{ ContentType: kurrentdb.ContentTypeJson, EventType: "OrderCreated", Data: data, } ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```go _, err = db.AppendToStream(context.Background(), "orders", kurrentdb.AppendToStreamOptions{}, eventData) ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ```go stream, err := db.ReadStream(context.Background(), "orders", kurrentdb.ReadStreamOptions{}, 10) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } // Process the order event fmt.Printf("Order event: %s - %s\n", event.Event.EventType, string(event.Event.Data)) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```go err := client.CreatePersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.PersistentStreamSubscriptionOptions{}) if err != nil { panic(err) } ``` ### Subscribing to `$all` ```go options := kurrentdb.PersistentAllSubscriptionOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"test"}, }, } err := client.CreatePersistentSubscriptionToAll(context.Background(), "subscription-group", options) if err != nil { panic(err) } ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```go sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```go sub, err := client.SubscribeToPersistentSubscriptionToAll(context.Background(), "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```go{11} sub, err := client.SubscribeToPersistentSubscription(context.Background(), "order-123", "subscription-group", kurrentdb.SubscribeToPersistentSubscriptionOptions{}) if err != nil { panic(err) } for { event := sub.Recv() if event.EventAppeared != nil { sub.Ack(event.EventAppeared.Event) } if event.SubscriptionDropped != nil { break } } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `NackActionPark` | Park the message and do not resend. Put it on poison queue. | | `NackActionRetry` | Explicitly retry the message. | | `NackActionSkip` | Skip this message do not resend and do not put in poison queue. | | `NackActionStop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```go await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```go await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a client ```go conf, err := kurrentdb.ParseConnectionString(connectionString) if err != nil { panic(err) } client, err := kurrentdb.NewProjectionClient(conf) ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```go script := ` fromAll() .when({ $init:function(){ return { count: 0 } }, myEventUpdatedType: function(state, event){ state.count += 1; } }) .transformBy(function(state){ state.count = 10; }) .outputState() ` name := fmt.Sprintf("countEvent_Create_%s", uuid.New()) err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if err != nil { panic(err) } ``` Trying to create projections with the same name will result in an error: ```go err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if esdbErr, ok := kurrentdb.FromError(err); !ok { if esdbErr.IsErrorCode(kurrentdb.ErrorCodeUnknown) && strings.Contains(esdbErr.Err().Error(), "Conflict") { log.Printf("projection %s already exists", name) return } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```go err := client.RestartSubsystem(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Enable(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```go err := client.Enable(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```go err := client.Disable(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```go err := client.Disable(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Delete a projection ```go err := client.Delete(context.Background(), "$by_category", kurrentdb.DeleteProjectionOptions{}) if err != nil { panic(err) } ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```go err := client.Abort(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```go err := client.Abort(context.Background(), "projection that doesn't exist", kurrentdb.GenericProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```go err := client.Reset(context.Background(), "$by_category", kurrentdb.ResetProjectionOptions{}) if err != nil { panic(err) } ``` Resetting a projection that does not exist will result in an error. ```go err := client.Reset(context.Background(), "projection that doesn't exist", kurrentdb.ResetProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```go err := client.Create(context.Background(), name, script, kurrentdb.CreateProjectionOptions{}) if err != nil { panic(err) } err = client.Update(context.Background(), name, newScript, kurrentdb.UpdateProjectionOptions{}) if err != nil { panic(err) } ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```go err := client.Update(context.Background(), "projection that doesn't exist", script, kurrentdb.UpdateProjectionOptions{}) if esdbError, ok := kurrentdb.FromError(err); !ok { if esdbError.IsErrorCode(kurrentdb.ErrorCodeResourceNotFound) { log.Printf("projection not found") return } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListAll(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projections, err := client.ListContinuous(context.Background(), kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } for i := range projections { projection := projections[i] log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```go projection, err := client.GetStatus(context.Background(), "$by_category", kurrentdb.GenericProjectionOptions{}) if err != nil { panic(err) } log.Printf( "%s, %s, %s, %s, %f", projection.Name, projection.Status, projection.CheckpointStatus, projection.Mode, projection.Progress, ) ``` ## Get state Retrieves the state of a projection. ```go type Foobar struct { Count int64 } value, err := client.GetState(context.Background(), projectionName, kurrentdb.GetStateProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var foobar Foobar if err = json.Unmarshal(jsonContent, &foobar); err != nil { panic(err) } log.Printf("count %d", foobar.Count) ``` ## Get result Retrieves the result of the named projection and partition. ```go type Baz struct { Result int64 } value, err := client.GetResult(context.Background(), projectionName, kurrentdb.GetResultProjectionOptions{}) if err != nil { panic(err) } jsonContent, err := value.MarshalJSON() if err != nil { panic(err) } var baz Baz if err = json.Unmarshal(jsonContent, &baz); err != nil { panic(err) } log.Printf("result %d", baz.Result) ``` ## Projection Details The `ListAll`, `ListContinuous`, and `GetStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Name`, `EffectiveName` | The name of the projection | | `Status` | A human readable string of the current statuses of the projection (see below) | | `StateReason` | A human readable string explaining the reason of the current projection state | | `CheckpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `Mode` | `Continuous`, `OneTime` , `Transient` | | `CoreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `Progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `WritesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `ReadsInProgress` | The number of read requests currently in progress | | `PartitionsCached` | The number of cached projection partitions | | `Position` | The Position of the last processed event | | `LastCheckpoint` | The Position of the last checkpoint of this projection | | `EventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `BufferedEvents` | The number of events in the projection read buffer | | `WritePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `WritePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `Version` | This is used internally, the version is increased when the projection is edited or reset | | `Epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `kurrentdb.Start{}` to begin from the very beginning of the stream * Use `kurrentdb.End{}` to begin from the current end of the stream * Use `kurrentdb.StreamRevision{}` with a specific revision number (64-bit signed integer) ```go{3} options := kurrentdb.ReadStreamOptions{ From: kurrentdb.Start{}, Direction: kurrentdb.Forwards, } stream, err := db.ReadStream(context.Background(), "some-stream", options, 100) if err != nil { panic(err) } defer stream.Close() ``` You can also start reading from a specific revision in the stream: ```go{2} options:= kurrentdb.ReadStreamOptions{ From: kurrentdb.Revision(10), } ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", kurrentdb.ReadStreamOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```go options:= kurrentdb.ReadAllOptions{ ResolveLinkTos: true, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := kurrentdb.ReadStreamOptions{ From: kurrentdb.Start{}, Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```go{2-3} options:= kurrentdb.ReadStreamOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } stream, err := db.ReadStream(context.Background(), "some-stream", ropts, 10) ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `*ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `Recv()` method will return an error with the code `ErrorCodeResourceNotFound`. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```go{13-14} stream, err := db.ReadStream(context.Background(), "order-123", kurrentdb.ReadStreamOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if err, ok := kurrentdb.FromError(err); !ok { if err.Code() == kurrentdb.ErrorCodeResourceNotFound { fmt.Print("Stream not found") } else if errors.Is(err, io.EOF) { break } else { panic(err) } } fmt.Printf("Event> %v", event) } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```go{1-4} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, Direction: kurrentdb.Forwards, } stream, err := db.ReadAll(context.Background(), options, 100) ``` You can also start reading from a specific position in the transaction log: ```go{3-6} options := kurrentdb.ReadAllOptions{ ResolveLinkTos: false, From: &kurrentdb.Position{ Commit: 10, Prepare: 10, }, } stream, err := db.ReadAll(context.Background(), ropts, 100) ``` You can then iterate synchronously through the result: ```go for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```go{5} stream, err := db.ReadStream( context.Background(), "order-123", kurrentdb.ReadAllOptions{}, 10 ) ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```go{3} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, ResolveLinkTos: true, Direction: kurrentdb.Forwards, } ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```go{3-6} options := kurrentdb.ReadAllOptions{ From: kurrentdb.Start{}, Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, } ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `kurrentdb.Backwards`: ```go{2-3} options := kurrentdb.ReadAllOptions{ Direction: kurrentdb.Backwards, From: kurrentdb.End{}, } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `EventType` property. ```go{22-24} stream, err := db.ReadAll(context.Background(), kurrentdb.ReadAllOptions{}, 100) if err != nil { panic(err) } defer stream.Close() for { event, err := stream.Recv() if errors.Is(err, io.EOF) { break } if err != nil { panic(err) } fmt.Printf("Event> %v", event) if strings.HasPrefix(event.OriginalEvent().EventType, "$") { continue } fmt.Printf("Event> %v", event) } ``` --- --- url: 'https://docs.kurrent.io/clients/golang/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```go stream, err := db.SubscribeToStream(context.Background(), "order-123", kurrentdb.SubscribeToStreamOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` **`$all` subscription:** ```go stream, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{}) if err != nil { panic(err) } defer stream.Close() for { event := stream.Recv() if event.EventAppeared != nil { // handles the event... } if event.CaughtUp != nil { } if event.SubscriptionDropped != nil { break } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`Start{}`, `End{}` or a 64-bit signed integer representing the revision number): ```go{2} db.SubscribeToStream(context.Background(), "order-123", kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Revision(20), }) ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```go{2-5} db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ From: kurrentdb.Position{ Commit: 1_056, Prepare: 1_056, }, }) ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```go // Stream options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.End{}, } // $all db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ From: kurrentdb.End{}, }) ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```go{3} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, ResolveLinkTos: true, } db.SubscribeToStream(context.Background(), "$et-orders", options) ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```go{22-25} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "some-stream", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = kurrentdb.Revision(event.EventAppeared.OriginalEvent().EventNumber) } } } ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```go{21-24} options = kurrentdb.SubscribeToAllOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToAll(context.Background(), options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.EventAppeared != nil { // handles the event... options.From = event.EventAppeared.OriginalEvent().Position } } } ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```go{22-25} options = kurrentdb.SubscribeToStreamOptions{ From: kurrentdb.Start{}, } for { stream, err := db.SubscribeToStream(context.Background(), "order-123", options) if err != nil { time.Sleep(1 * time.Second) continue } for { event := stream.Recv() if event.SubscriptionDropped != nil { stream.Close() break } if event.CaughtUp != nil { // handles the caught up event fmt.Println("Caught up to live mode") } if event.EventAppeared != nil { // handles the event } } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```go{2-5} db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Authenticated: &kurrentdb.Credentials{ Login: "admin", Password: "changeit", }, }) ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```go db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"test-", "other-"}, }, }) ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: kurrentdb.ExcludeSystemEventsFilter(), }) ``` ### Filtering by event type **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Prefixes: []string{"customer-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Regex: "^user|^company", }, }) ``` ### Filtering by stream name **By prefix:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Prefixes: []string{"user-"}, }, }) ``` **By regular expression:** ```go sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.StreamFilterType, Regex: "^user|^company", }, }) ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```go{11-14} for { event := sub.Recv() if event.EventAppeared != nil { streamId := event.EventAppeared.OriginalEvent().StreamID revision := event.EventAppeared.OriginalEvent().EventNumber fmt.Printf("received event %v@%v", revision, streamId) } if event.CheckPointReached != nil { // Save commit position to a persistent store as a checkpoint fmt.Printf("checkpoint taken at %v", event.CheckPointReached.Commit) } if event.SubscriptionDropped != nil { break } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```go{6} sub, err := db.SubscribeToAll(context.Background(), kurrentdb.SubscribeToAllOptions{ Filter: &kurrentdb.SubscriptionFilter{ Type: kurrentdb.EventFilterType, Regex: "/^[^\\$].*/", }, CheckpointInterval: 1, }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/appending-events.md' --- # Appending events When you start working with EventStoreDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to EventStoreDB is to create an `EventData` object and call `appendToStream` method. ```java {32-43} class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } public String getOrderId() { return orderId; } public String getCustomerId() { return customerId; } public double getTotalAmount() { return totalAmount; } public String getStatus() { return status; } } EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .expectedRevision(ExpectedRevision.noStream()); client.appendToStream("orders", options, eventData) .get(); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to EventStoreDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, EventStoreDB will only append one of the events to the stream. For example, the following code will only append a single event: ```java {3,15-16} EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .expectedRevision(ExpectedRevision.any()); client.appendToStream("orders", options, eventData) .get(); // attempt to append the same event again client.appendToStream("orders", options, eventData) .get(); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of EventStoreDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. EventStoreDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. This is automatically set when using the builder methods like `builderAsJson()` or `builderAsBinary()`. ## Handling concurrency When appending events to a stream, you can supply an *expected revision*. Your client uses this to inform EventStoreDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```java EventData eventDataOne = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); EventData eventDataTwo = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-457", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .expectedRevision(ExpectedRevision.noStream()); client.appendToStream("no-stream-stream", options, eventDataOne) .get(); // attempt to append the same event again client.appendToStream("no-stream-stream", options, eventDataTwo) .get(); ``` There are several available expected revision options: * `ExpectedRevision.any()` - No concurrency check * `ExpectedRevision.noStream()` - Stream should not exist * `ExpectedRevision.streamExists()` - Stream should exist * `ExpectedRevision.expectedRevision(long revision)` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from EventStoreDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. First, let's define the event classes for our ecommerce example: ```java public class PaymentProcessed { private String orderId; private String paymentId; private double amount; private String paymentMethod; public PaymentProcessed(String orderId, String paymentId, double amount, String paymentMethod) { this.orderId = orderId; this.paymentId = paymentId; this.amount = amount; this.paymentMethod = paymentMethod; } // getters omitted for brevity } public class OrderCancelled { private String orderId; private String reason; private String comment; public OrderCancelled(String orderId, String reason, String comment) { this.orderId = orderId; this.reason = reason; this.comment = comment; } // getters omitted for brevity } ``` Now, here's how to implement optimistic concurrency control: ```java ReadStreamOptions readOptions = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("order-12345", readOptions) .get(); // Get the current revision to use for optimistic concurrency long currentRevision = result.getLastStreamPosition(); // Two concurrent operations trying to update the same order EventData paymentProcessedEvent = EventData .builderAsJson( UUID.randomUUID(), "PaymentProcessed", new PaymentProcessed("order-12345", "payment-789", 149.99, "VISA")) .build(); EventData orderCancelledEvent = EventData .builderAsJson( UUID.randomUUID(), "OrderCancelled", new OrderCancelled("order-12345", "customer-request", "Customer changed mind")) .build(); // Process payment (succeeds) AppendToStreamOptions appendOptions = AppendToStreamOptions.get() .streamRevision(currentRevision); WriteResult paymentResult = client.appendToStream("order-12345", appendOptions, paymentProcessedEvent) .get(); // Cancel order (fails due to concurrency conflict) AppendToStreamOptions cancelOptions = AppendToStreamOptions.get() .streamRevision(currentRevision); client.appendToStream("order-12345", cancelOptions, orderCancelledEvent) .get(); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); AppendToStreamOptions options = AppendToStreamOptions.get() .authenticated(credentials); client.appendToStream("some-stream", options, eventData) .get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```java EventStoreDBClientSettings settings = EventStoreDBConnectionString .parseOrThrow("esdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); EventStoreDBClient client = EventStoreDBClient.create(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/delete-stream.md' --- # Deleting Events In EventStoreDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, EventStoreDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in EventStoreDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```java client.deleteStream(streamName, DeleteStreamOptions.get()).get(); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in EventStoreDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```java client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/getting-started.md' --- # Getting started This guide will help you get started with EventStoreDB in your Java application. It covers the basic steps to connect to EventStoreDB, create events, append them to streams, and read them back. ## Required packages Add the `db-client-java` dependency to your project: ::: tabs @tab gradle ```bash implementation 'com.eventstore:db-client-java:5.4.x' ``` @tab maven ```bash com.eventstore db-client-java 5.4.x ``` ::: ## Connecting to EventStoreDB To connect your application to EventStoreDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to EventStoreDB is using secure mode (which is the default). However, if your EventStoreDB instance is running in insecure mode, you must explicitly set `tls=false` in your [connection string](#connection-string) or client configuration. ::: EventStoreDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`esdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`esdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `esdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `esdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, EventStoreDB supports gossip on single-node deployments, so `esdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` esdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` esdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` esdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | | `dnsDiscover` | `true`, `false` | `false` | Enable DNS-based cluster discovery. When `true`, resolves hostnames to discover cluster nodes. Use with `feature=dns-lookup` for full DNS resolution. | | `feature` | `dns-lookup` | None | Enable specific client features. Use `dns-lookup` with `dnsDiscover=true` to resolve hostnames to multiple IP addresses for cluster discovery. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `esdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```java EventStoreDBClientSettings settings = EventStoreDBConnectionString.parseOrThrow("esdb://localhost:2113?tls=false"); EventStoreDBClient client = EventStoreDBClient.create(settings); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to EventStoreDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```java public class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } } OrderPlaced event = new OrderPlaced("order-456", "customer-789", 249.99, "confirmed"); JsonMapper jsonMapper = new JsonMapper(); EventData eventData = EventData .builderAsJson("OrderPlaced", jsonMapper.writeValueAsBytes(event)) .build(); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```java client.appendToStream("orders", eventData).get(); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ### Synchronous reading ```java import java.util.List; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); ReadResult result = client.readStream("orders", options) .get(); List events = result.getEvents(); ``` ### Asynchronous reading We also provide an asynchronous API for reading events using Java Reactive Streams. ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { // subscription confirmed } @Override public void onNext(ReadMessage readMessage) { // Process the event RecordedEvent event = readMessage.getEvent().getOriginalEvent(); } @Override public void onError(Throwable throwable) { // handle error } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures (synchronous) or `ReadMessage` objects (reactive). The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/observability.md' --- # Observability The Java client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project. Add these to your Maven `pom.xml` or Gradle `build.gradle`: ::: tabs#distribution @tab Maven ```xml io.opentelemetry opentelemetry-sdk 1.40.0 io.opentelemetry opentelemetry-exporter-logging 1.40.0 io.opentelemetry opentelemetry-exporter-otlp 1.40.0 io.opentelemetry.semconv opentelemetry-semconv 1.25.0-alpha ``` @tab Gradle ```groovy dependencies { implementation 'io.opentelemetry:opentelemetry-sdk:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-logging:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.40.0' implementation 'io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha' } ``` ::: ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```java import com.eventstore.dbclient.*; import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class EventStoreObservability { public static void main(String[] args) { // Configure resource with service name Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Create console exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // Configure tracer provider SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .setResource(resource) .build(); // Register OpenTelemetry SDK globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); // Your EventStoreDB client operations will now be traced EventStoreDBClientSettings settings = EventStoreDBConnectionString .parseOrThrow("esdb://localhost:2113?tls=false"); EventStoreDBClient client = EventStoreDBClient.create(settings); } } ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=java). You can configure multiple exporters simultaneously: ```java import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class MultipleExporters { public static void configureOpenTelemetry() { Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Console/logging exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // OTLP exporter for Jaeger/other OTLP-compatible backends OtlpGrpcSpanExporter otlpExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build(); // Configure tracer provider with multiple exporters SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .addSpanProcessor(SimpleSpanProcessor.create(otlpExporter)) .setResource(resource) .build(); // Register globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); } } ``` For detailed configuration options, refer to the [OpenTelemetry Java documentation](https://opentelemetry.io/docs/languages/java/). ## Understanding Traces ### What Gets Traced The Java client automatically creates traces for append, catch-up and persistent subscription operations when OpenTelemetry is configured globally. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | EventStoreDB server address | `localhost` | | `server.port` | EventStoreDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client The Java client provides a `PersistentSubscriptionsClient` that you can use to manage persistent subscriptions. ```java EventStoreDBClientSettings settings = EventStoreDBConnectionString.parseOrThrow("esdb://localhost:2113?tls=false"); PersistentSubscriptionsClient client = PersistentSubscriptionsClient.create(settings); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```java client.createToStream( "stream-name", "subscription-group", CreatePersistentSubscriptionToStreamOptions.get() .fromStart()); ``` ### Subscribing to `$all` ```java client.createToAll( "subscription-group", CreatePersistentSubscriptionToAllOptions.get() .fromStart()); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```java client.subscribeToStream( "stream-name", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); } @Override public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```java client.subscribeToAll( "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` The `SubscribeToAll` method is identical to the `SubscribeToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```java client.subscribeToStream( "test-stream", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } }); ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```java client.updateToStream( "stream-name", "subscription-group", UpdatePersistentSubscriptionToStreamOptions.get() .resolveLinkTos() .checkpointLowerBound(20)); ``` ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```java client.deleteToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/projections.md' --- # Projection management The client provides a way to manage projections in EventStoreDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client The Java client provides a `EventStoreDBProjectionManagementClient` that you can use to manage persistent subscriptions. ```java EventStoreDBClientSettings settings = EventStoreDBConnectionString.parseOrThrow("esdb://localhost:2113?tls=false"); EventStoreDBProjectionManagementClient client = EventStoreDBProjectionManagementClient.create(settings); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```java String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; String name = "countEvents_Create_" + java.util.UUID.randomUUID(); client.create(name, js).get(); ``` Trying to create projections with the same name will result in an error: ```java try { client.create(name, js).get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("Conflict")) { System.out.println(name + " already exists"); } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```java client.restartSubsystem().get(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.enable("$by_category").get(); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.disable("$by_category").get(); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Delete a projection ```java // A projection must be disabled to allow it to be deleted. client.disable(name).get(); // The projection can now be deleted client.delete(name).get(); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```java client.abort("$by_category").get(); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```java try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```java client.reset("$by_category").get(); ``` Resetting a projection that does not exist will result in an error. ```java try { client.reset("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```java String name = "countEvents_Update_" + java.util.UUID.randomUUID(); String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, "fromAll().when()").get(); client.update(name, js).get(); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```java try { client.update("Update Not existing projection", "fromAll().when()").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println("'Update Not existing projection' does not exists and can not be updated"); } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```java ProjectionDetails status = client.getStatus("$by_category").get(); System.out.println( status.getName() + ", " + status.getStatus() + ", " + status.getCheckpointStatus() + ", " + status.getMode() + ", " + status.getProgress()); ``` ## Get state Retrieves the state of a projection. ```java public static class CountResult { private int count; public int getCount() { return count; } public void setCount(final int count){ this.count = count; } } String name = "get_state_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. CountResult result = client .getState(name, CountResult.class) .get(); System.out.println(result); ``` ## Get result Retrieves the result of the named projection and partition. ```java String name = "get_result_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".transformBy((state) => state.count)" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. int result = client .getResult(name, int.class) .get(); System.out.println(result); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/reading-events.md' --- # Reading Events EventStoreDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in EventStoreDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in EventStoreDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the stream * Use `fromEnd()` to begin from the current end of the stream * Use `fromRevision(long revision)` with a specific revision number (64-bit signed integer) ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("orders", options) .get(); ``` You can also start reading from a specific revision in the stream: ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10); ReadResult result = client.readStream("orders", options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readStream("orders", options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readStream("orders", options) .get(); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```java{4} JsonMapper mapper = new JsonMapper(); ReadStreamOptions options = ReadStreamOptions.get() .backwards() .fromEnd(); ReadResult result = client.readStream("orders", options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```java{11-15} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ReadResult result = null; try { result = client.readStream("some-stream", options) .get(); } catch (ExecutionException e) { Throwable innerException = e.getCause(); if (innerException instanceof StreamNotFoundException) { return; } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the transaction log * Use `fromEnd()` to begin from the current end of the transaction log * Use `fromPosition(Position position)` with a specific `Position` object containing commit and prepare coordinates ```java{2-3} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); ``` You can also start reading from a specific position in the transaction log: ```java{1,4-5} Position position = new Position(1000, 1000); ReadAllOptions options = ReadAllOptions.get() .forwards() .fromPosition(position); ReadResult result = client.readAll(options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readAll(options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readAll(options) .get(); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to the `Backwards`: ```java{2} ReadAllOptions options = ReadAllOptions.get() .backwards() .fromEnd(); ReadResult result = client.readAll(options) .get(); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events EventStoreDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```java{10-12} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); if (!recordedEvent.getEventType().startsWith("$")) { // Process the event } } ``` ## Java Reactive Streams The Java Reactive Streams API allows you to read events in a non-blocking manner, which is particularly useful for applications that require high throughput and low latency. The reactive API provides a way to subscribe to streams of events and process them as they arrive. ::: tabs#java @tab Reading from a stream ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Process the event System.out.println("Event: " + event.getEventType()); } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` @tab Reading from $all ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); Publisher publisher = client.readAllReactive(options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Filter out system events if needed if (!event.getEventType().startsWith("$")) { System.out.println("Event: " + event.getEventType()); } } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/java/legacy/v5.4/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); // Handle the event } }; client.subscribeToStream("orders", listener); ``` **`$all` subscription:** ```java client.subscribeToAll(listener); ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```java client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromRevision(20) ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```java client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromPosition(new Position(1056, 1056)) ); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```java // Stream client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromEnd() ); // $all client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromEnd() ); ``` ## Resolving link-to events Link-to events point to events in other streams in EventStoreDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```java client.subscribeToStream( "$et-orders", listener, SubscribeToStreamOptions.get() .fromStart() .resolveLinkTos() ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `onCancelled` callback in the `SubscriptionListener`, which will get called when the subscription breaks. The `onCancelled` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```java client.subscribeToStream( "orders", new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getRevision()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToStreamOptions.get() .fromStart() ); ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```java client.subscribeToAll( new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getPosition()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToAllOptions.get() .fromStart() ); ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `onCaughtUp` callback in the `SubscriptionListener`. ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Processing event: " + event.getOriginalEvent().getEventType()); // Handle the event } @Override public void onCaughtUp(Subscription subscription) { System.out.println("Subscription caught up - now processing live events"); // Trigger any actions needed when caught up } @Override public void onCancelled(Subscription subscription, Throwable exception) { if (exception != null) { System.out.println("Subscription dropped: " + exception.getMessage()); } } }; client.subscribeToStream("orders", listener); ``` ::: tip The `onCaughtUp` callback is only triggered when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); SubscribeToAllOptions options = SubscribeToAllOptions.get().authenticated(credentials); client.subscribeToAll(listener, options); ``` ## Server-side Filtering EventStoreDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("test-") .build(); SubscribeToAllOptions options = SubscribeToAllOptions.get() .filter(filter); client.subscribeToAll(listener, options); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```java String excludeSystemEventsRegex = "^[^\\$].*"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .build(); client.subscribeToAll(listener, SubscribeToAllOptions.get().filter(filter)); ``` ### Filtering by event type **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addEventTypePrefix("customer-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression("^user|^company") .build(); ``` ### Filtering by stream name **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("user-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withStreamNameRegularExpression("^account|^savings") .build(); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }) .build(); ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }, 1000) .build(); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```java {32-43} class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } public String getOrderId() { return orderId; } public String getCustomerId() { return customerId; } public double getTotalAmount() { return totalAmount; } public String getStatus() { return status; } } EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("orders", options, eventData) .get(); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```java {3,15-16} EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.any()); client.appendToStream("orders", options, eventData) .get(); // attempt to append the same event again client.appendToStream("orders", options, eventData) .get(); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. This is automatically set when using the builder methods like `builderAsJson()` or `builderAsBinary()`. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```java EventData eventDataOne = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); EventData eventDataTwo = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-457", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("no-stream-stream", options, eventDataOne) .get(); // attempt to append the same event again client.appendToStream("no-stream-stream", options, eventDataTwo) .get(); ``` There are several available expected revision options: * `StreamState.any()` - No concurrency check * `StreamState.noStream()` - Stream should not exist * `StreamState.streamExists()` - Stream should exist * `StreamState.streamRevision(long revision)` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. First, let's define the event classes for our ecommerce example: ```java public class PaymentProcessed { private String orderId; private String paymentId; private double amount; private String paymentMethod; public PaymentProcessed(String orderId, String paymentId, double amount, String paymentMethod) { this.orderId = orderId; this.paymentId = paymentId; this.amount = amount; this.paymentMethod = paymentMethod; } // getters omitted for brevity } public class OrderCancelled { private String orderId; private String reason; private String comment; public OrderCancelled(String orderId, String reason, String comment) { this.orderId = orderId; this.reason = reason; this.comment = comment; } // getters omitted for brevity } ``` Now, here's how to implement optimistic concurrency control: ```java ReadStreamOptions readOptions = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("order-12345", readOptions) .get(); // Get the current revision to use for optimistic concurrency long currentRevision = result.getLastStreamPosition(); // Two concurrent operations trying to update the same order EventData paymentProcessedEvent = EventData .builderAsJson( UUID.randomUUID(), "PaymentProcessed", new PaymentProcessed("order-12345", "payment-789", 149.99, "VISA")) .build(); EventData orderCancelledEvent = EventData .builderAsJson( UUID.randomUUID(), "OrderCancelled", new OrderCancelled("order-12345", "customer-request", "Customer changed mind")) .build(); // Process payment (succeeds) AppendToStreamOptions appendOptions = AppendToStreamOptions.get() .streamState(currentRevision); WriteResult paymentResult = client.appendToStream("order-12345", appendOptions, paymentProcessedEvent) .get(); // Cancel order (fails due to concurrency conflict) AppendToStreamOptions cancelOptions = AppendToStreamOptions.get() .streamState(currentRevision); client.appendToStream("order-12345", cancelOptions, orderCancelledEvent) .get(); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); AppendToStreamOptions options = AppendToStreamOptions.get() .authenticated(credentials); client.appendToStream("some-stream", options, eventData) .get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or KurrentDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```java KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); KurrentDBClient client = KurrentDBClient.create(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```java client.deleteStream(streamName, DeleteStreamOptions.get()).get(); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```java client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Java application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `kurrentdb-client` dependency to your project: ::: tabs @tab gradle ```groovy implementation 'io.kurrent:kurrentdb-client:1.0.x' ``` @tab maven ```xml io.kurrent kurrentdb-client 1.0.x ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, KurrentDB supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | | `feature` | `dns-lookup` | None | Enable specific client features. Use `dns-lookup` with `dnsDiscover=true` to resolve hostnames to multiple IP addresses for cluster discovery. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```java public class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } } OrderPlaced event = new OrderPlaced("order-456", "customer-789", 249.99, "confirmed"); JsonMapper jsonMapper = new JsonMapper(); EventData eventData = EventData .builderAsJson("OrderPlaced", jsonMapper.writeValueAsBytes(event)) .build(); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```java client.appendToStream("orders", eventData).get(); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ### Synchronous reading ```java import java.util.List; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); ReadResult result = client.readStream("orders", options) .get(); List events = result.getEvents(); ``` ### Asynchronous reading We also provide an asynchronous API for reading events using Java Reactive Streams. ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { // subscription confirmed } @Override public void onNext(ReadMessage readMessage) { // Process the event RecordedEvent event = readMessage.getEvent().getOriginalEvent(); } @Override public void onError(Throwable throwable) { // handle error } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures (synchronous) or `ReadMessage` objects (reactive). The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/observability.md' --- # Observability The Java client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project. Add these to your Maven `pom.xml` or Gradle `build.gradle`: ::: tabs#distribution @tab Maven ```xml io.opentelemetry opentelemetry-sdk 1.40.0 io.opentelemetry opentelemetry-exporter-logging 1.40.0 io.opentelemetry opentelemetry-exporter-otlp 1.40.0 io.opentelemetry.semconv opentelemetry-semconv 1.25.0-alpha ``` @tab Gradle ```groovy dependencies { implementation 'io.opentelemetry:opentelemetry-sdk:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-logging:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.40.0' implementation 'io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha' } ``` ::: ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```java import com.eventstore.dbclient.*; import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class EventStoreObservability { public static void main(String[] args) { // Configure resource with service name Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Create console exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // Configure tracer provider SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .setResource(resource) .build(); // Register OpenTelemetry SDK globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); // Your KurrentDB client operations will now be traced KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); } } ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=java). You can configure multiple exporters simultaneously: ```java import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class MultipleExporters { public static void configureOpenTelemetry() { Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Console/logging exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // OTLP exporter for Jaeger/other OTLP-compatible backends OtlpGrpcSpanExporter otlpExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build(); // Configure tracer provider with multiple exporters SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .addSpanProcessor(SimpleSpanProcessor.create(otlpExporter)) .setResource(resource) .build(); // Register globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); } } ``` For detailed configuration options, refer to the [OpenTelemetry Java documentation](https://opentelemetry.io/docs/languages/java/). ## Understanding Traces ### What Gets Traced The Java client automatically creates traces for append, catch-up and persistent subscription operations when OpenTelemetry is configured globally. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client The Java client provides a `PersistentSubscriptionsClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); PersistentSubscriptionsClient client = PersistentSubscriptionsClient.create(settings); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```java client.createToStream( "stream-name", "subscription-group", CreatePersistentSubscriptionToStreamOptions.get() .fromStart()); ``` ### Subscribing to `$all` ```java client.createToAll( "subscription-group", CreatePersistentSubscriptionToAllOptions.get() .fromStart()); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```java client.subscribeToStream( "stream-name", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); } @Override public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```java client.subscribeToAll( "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` The `SubscribeToAll` method is identical to the `SubscribeToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```java client.subscribeToStream( "test-stream", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } }); ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```java client.updateToStream( "stream-name", "subscription-group", UpdatePersistentSubscriptionToStreamOptions.get() .resolveLinkTos() .checkpointLowerBound(20)); ``` ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```java client.deleteToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client The Java client provides a `KurrentDBProjectionManagementClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBProjectionManagementClient client = KurrentDBProjectionManagementClient.create(settings); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```java String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; String name = "countEvents_Create_" + java.util.UUID.randomUUID(); client.create(name, js).get(); ``` Trying to create projections with the same name will result in an error: ```java try { client.create(name, js).get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("Conflict")) { System.out.println(name + " already exists"); } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```java client.restartSubsystem().get(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.enable("$by_category").get(); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.disable("$by_category").get(); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Delete a projection ```java // A projection must be disabled to allow it to be deleted. client.disable(name).get(); // The projection can now be deleted client.delete(name).get(); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```java client.abort("$by_category").get(); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```java try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```java client.reset("$by_category").get(); ``` Resetting a projection that does not exist will result in an error. ```java try { client.reset("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```java String name = "countEvents_Update_" + java.util.UUID.randomUUID(); String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, "fromAll().when()").get(); client.update(name, js).get(); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```java try { client.update("Update Not existing projection", "fromAll().when()").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println("'Update Not existing projection' does not exists and can not be updated"); } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```java ProjectionDetails status = client.getStatus("$by_category").get(); System.out.println( status.getName() + ", " + status.getStatus() + ", " + status.getCheckpointStatus() + ", " + status.getMode() + ", " + status.getProgress()); ``` ## Get state Retrieves the state of a projection. ```java public static class CountResult { private int count; public int getCount() { return count; } public void setCount(final int count){ this.count = count; } } String name = "get_state_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. CountResult result = client .getState(name, CountResult.class) .get(); System.out.println(result); ``` ## Get result Retrieves the result of the named projection and partition. ```java String name = "get_result_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".transformBy((state) => state.count)" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. int result = client .getResult(name, int.class) .get(); System.out.println(result); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the stream * Use `fromEnd()` to begin from the current end of the stream * Use `fromRevision(long revision)` with a specific revision number (64-bit signed integer) ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("orders", options) .get(); ``` You can also start reading from a specific revision in the stream: ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10); ReadResult result = client.readStream("orders", options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readStream("orders", options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readStream("orders", options) .get(); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```java{4} JsonMapper mapper = new JsonMapper(); ReadStreamOptions options = ReadStreamOptions.get() .backwards() .fromEnd(); ReadResult result = client.readStream("orders", options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```java{11-15} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ReadResult result = null; try { result = client.readStream("some-stream", options) .get(); } catch (ExecutionException e) { Throwable innerException = e.getCause(); if (innerException instanceof StreamNotFoundException) { return; } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the transaction log * Use `fromEnd()` to begin from the current end of the transaction log * Use `fromPosition(Position position)` with a specific `Position` object containing commit and prepare coordinates ```java{2-3} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); ``` You can also start reading from a specific position in the transaction log: ```java{1,4-5} Position position = new Position(1000, 1000); ReadAllOptions options = ReadAllOptions.get() .forwards() .fromPosition(position); ReadResult result = client.readAll(options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readAll(options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readAll(options) .get(); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to the `Backwards`: ```java{2} ReadAllOptions options = ReadAllOptions.get() .backwards() .fromEnd(); ReadResult result = client.readAll(options) .get(); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```java{10-12} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); if (!recordedEvent.getEventType().startsWith("$")) { // Process the event } } ``` ## Java Reactive Streams The Java Reactive Streams API allows you to read events in a non-blocking manner, which is particularly useful for applications that require high throughput and low latency. The reactive API provides a way to subscribe to streams of events and process them as they arrive. ::: tabs#java @tab Reading from a stream ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Process the event System.out.println("Event: " + event.getEventType()); } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` @tab Reading from $all ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); Publisher publisher = client.readAllReactive(options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Filter out system events if needed if (!event.getEventType().startsWith("$")) { System.out.println("Event: " + event.getEventType()); } } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` ::: --- --- url: 'https://docs.kurrent.io/clients/java/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); // Handle the event } }; client.subscribeToStream("orders", listener); ``` **`$all` subscription:** ```java client.subscribeToAll(listener); ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```java client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromRevision(20) ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```java client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromPosition(new Position(1056, 1056)) ); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```java // Stream client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromEnd() ); // $all client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromEnd() ); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```java client.subscribeToStream( "$et-orders", listener, SubscribeToStreamOptions.get() .fromStart() .resolveLinkTos() ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `onCancelled` callback in the `SubscriptionListener`, which will get called when the subscription breaks. The `onCancelled` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```java client.subscribeToStream( "orders", new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getRevision()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToStreamOptions.get() .fromStart() ); ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```java client.subscribeToAll( new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getPosition()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToAllOptions.get() .fromStart() ); ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `onCaughtUp` callback in the `SubscriptionListener`. ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Processing event: " + event.getOriginalEvent().getEventType()); // Handle the event } @Override public void onCaughtUp(Subscription subscription) { System.out.println("Subscription caught up - now processing live events"); // Trigger any actions needed when caught up } @Override public void onCancelled(Subscription subscription, Throwable exception) { if (exception != null) { System.out.println("Subscription dropped: " + exception.getMessage()); } } }; client.subscribeToStream("orders", listener); ``` ::: tip The `onCaughtUp` callback is only triggered when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); SubscribeToAllOptions options = SubscribeToAllOptions.get().authenticated(credentials); client.subscribeToAll(listener, options); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("test-") .build(); SubscribeToAllOptions options = SubscribeToAllOptions.get() .filter(filter); client.subscribeToAll(listener, options); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```java String excludeSystemEventsRegex = "^[^\\$].*"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .build(); client.subscribeToAll(listener, SubscribeToAllOptions.get().filter(filter)); ``` ### Filtering by event type **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addEventTypePrefix("customer-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression("^user|^company") .build(); ``` ### Filtering by stream name **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("user-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withStreamNameRegularExpression("^account|^savings") .build(); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }) .build(); ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }, 1000) .build(); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```java {32-43} class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } public String getOrderId() { return orderId; } public String getCustomerId() { return customerId; } public double getTotalAmount() { return totalAmount; } public String getStatus() { return status; } } EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("orders", options, eventData) .get(); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```java {3,15-16} EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.any()); client.appendToStream("orders", options, eventData) .get(); // attempt to append the same event again client.appendToStream("orders", options, eventData) .get(); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. This is automatically set when using the builder methods like `builderAsJson()` or `builderAsBinary()`. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```java EventData eventDataOne = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); EventData eventDataTwo = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-457", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("no-stream-stream", options, eventDataOne) .get(); // attempt to append the same event again client.appendToStream("no-stream-stream", options, eventDataTwo) .get(); ``` There are several available expected revision options: * `StreamState.any()` - No concurrency check * `StreamState.noStream()` - Stream should not exist * `StreamState.streamExists()` - Stream should exist * `StreamState.streamRevision(long revision)` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. First, let's define the event classes for our ecommerce example: ```java public class PaymentProcessed { private String orderId; private String paymentId; private double amount; private String paymentMethod; public PaymentProcessed(String orderId, String paymentId, double amount, String paymentMethod) { this.orderId = orderId; this.paymentId = paymentId; this.amount = amount; this.paymentMethod = paymentMethod; } // getters omitted for brevity } public class OrderCancelled { private String orderId; private String reason; private String comment; public OrderCancelled(String orderId, String reason, String comment) { this.orderId = orderId; this.reason = reason; this.comment = comment; } // getters omitted for brevity } ``` Now, here's how to implement optimistic concurrency control: ```java ReadStreamOptions readOptions = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("order-12345", readOptions) .get(); // Get the current revision to use for optimistic concurrency long currentRevision = result.getLastStreamPosition(); // Two concurrent operations trying to update the same order EventData paymentProcessedEvent = EventData .builderAsJson( UUID.randomUUID(), "PaymentProcessed", new PaymentProcessed("order-12345", "payment-789", 149.99, "VISA")) .build(); EventData orderCancelledEvent = EventData .builderAsJson( UUID.randomUUID(), "OrderCancelled", new OrderCancelled("order-12345", "customer-request", "Customer changed mind")) .build(); // Process payment (succeeds) AppendToStreamOptions appendOptions = AppendToStreamOptions.get() .streamState(currentRevision); WriteResult paymentResult = client.appendToStream("order-12345", appendOptions, paymentProcessedEvent) .get(); // Cancel order (fails due to concurrency conflict) AppendToStreamOptions cancelOptions = AppendToStreamOptions.get() .streamState(currentRevision); client.appendToStream("order-12345", cancelOptions, orderCancelledEvent) .get(); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); AppendToStreamOptions options = AppendToStreamOptions.get() .authenticated(credentials); client.appendToStream("some-stream", options, eventData) .get(); ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ```java JsonMapper mapper = new JsonMapper(); Map metadata = new HashMap<>(); metadata.put("source", "OrderProcessingSystem"); byte[] metadataBytes = mapper.writeValueAsBytes(metadata); EventData orderEvent = EventData .builderAsJson("OrderCreated", mapper.writeValueAsBytes(new OrderCreated("12345", 99.99))) .metadataAsBytes(metadataBytes) .build(); EventData inventoryEvent = EventData .builderAsJson("ProductPurchased", mapper.writeValueAsBytes(new ProductPurchased("ABC123", 2, 19.99))) .metadataAsBytes(metadataBytes) .build(); List requests = Arrays.asList( new AppendStreamRequest( "order-stream-1", Collections.singletonList(orderEvent).iterator(), StreamState.any() ), new AppendStreamRequest( "product-stream-1", Collections.singletonList(inventoryEvent).iterator(), StreamState.any() ) ); MultiAppendWriteResult result = client.multiStreamAppend(requests.iterator()).get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or KurrentDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```java KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); KurrentDBClient client = KurrentDBClient.create(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```java client.deleteStream(streamName, DeleteStreamOptions.get()).get(); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```java client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Java application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `kurrentdb-client` dependency to your project: ::: tabs @tab gradle ```groovy implementation 'io.kurrent:kurrentdb-client:1.1.x' ``` @tab maven ```xml io.kurrent kurrentdb-client 1.1.x ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, KurrentDB supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | | `feature` | `dns-lookup` | None | Enable specific client features. Use `dns-lookup` with `dnsDiscover=true` to resolve hostnames to multiple IP addresses for cluster discovery. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```java public class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } } OrderPlaced event = new OrderPlaced("order-456", "customer-789", 249.99, "confirmed"); JsonMapper jsonMapper = new JsonMapper(); EventData eventData = EventData .builderAsJson("OrderPlaced", jsonMapper.writeValueAsBytes(event)) .build(); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```java client.appendToStream("orders", eventData).get(); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ### Synchronous reading ```java import java.util.List; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); ReadResult result = client.readStream("orders", options) .get(); List events = result.getEvents(); ``` ### Asynchronous reading We also provide an asynchronous API for reading events using Java Reactive Streams. ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { // subscription confirmed } @Override public void onNext(ReadMessage readMessage) { // Process the event RecordedEvent event = readMessage.getEvent().getOriginalEvent(); } @Override public void onError(Throwable throwable) { // handle error } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures (synchronous) or `ReadMessage` objects (reactive). The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/observability.md' --- # Observability The Java client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project. Add these to your Maven `pom.xml` or Gradle `build.gradle`: ::: tabs#distribution @tab Maven ```xml io.opentelemetry opentelemetry-sdk 1.40.0 io.opentelemetry opentelemetry-exporter-logging 1.40.0 io.opentelemetry opentelemetry-exporter-otlp 1.40.0 io.opentelemetry.semconv opentelemetry-semconv 1.25.0-alpha ``` @tab Gradle ```groovy dependencies { implementation 'io.opentelemetry:opentelemetry-sdk:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-logging:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.40.0' implementation 'io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha' } ``` ::: ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```java import com.eventstore.dbclient.*; import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class EventStoreObservability { public static void main(String[] args) { // Configure resource with service name Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Create console exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // Configure tracer provider SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .setResource(resource) .build(); // Register OpenTelemetry SDK globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); // Your KurrentDB client operations will now be traced KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); } } ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=java). You can configure multiple exporters simultaneously: ```java import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class MultipleExporters { public static void configureOpenTelemetry() { Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Console/logging exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // OTLP exporter for Jaeger/other OTLP-compatible backends OtlpGrpcSpanExporter otlpExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build(); // Configure tracer provider with multiple exporters SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .addSpanProcessor(SimpleSpanProcessor.create(otlpExporter)) .setResource(resource) .build(); // Register globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); } } ``` For detailed configuration options, refer to the [OpenTelemetry Java documentation](https://opentelemetry.io/docs/languages/java/). ## Understanding Traces ### What Gets Traced The Java client automatically creates traces for append, catch-up and persistent subscription operations when OpenTelemetry is configured globally. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client The Java client provides a `PersistentSubscriptionsClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); PersistentSubscriptionsClient client = PersistentSubscriptionsClient.create(settings); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```java client.createToStream( "stream-name", "subscription-group", CreatePersistentSubscriptionToStreamOptions.get() .fromStart()); ``` ### Subscribing to `$all` ```java client.createToAll( "subscription-group", CreatePersistentSubscriptionToAllOptions.get() .fromStart()); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```java client.subscribeToStream( "stream-name", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); } @Override public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```java client.subscribeToAll( "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` The `SubscribeToAll` method is identical to the `SubscribeToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```java client.subscribeToStream( "test-stream", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } }); ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```java client.updateToStream( "stream-name", "subscription-group", UpdatePersistentSubscriptionToStreamOptions.get() .resolveLinkTos() .checkpointLowerBound(20)); ``` ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```java client.deleteToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client The Java client provides a `KurrentDBProjectionManagementClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBProjectionManagementClient client = KurrentDBProjectionManagementClient.create(settings); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```java String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; String name = "countEvents_Create_" + java.util.UUID.randomUUID(); client.create(name, js).get(); ``` Trying to create projections with the same name will result in an error: ```java try { client.create(name, js).get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("Conflict")) { System.out.println(name + " already exists"); } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```java client.restartSubsystem().get(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.enable("$by_category").get(); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.disable("$by_category").get(); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Delete a projection ```java // A projection must be disabled to allow it to be deleted. client.disable(name).get(); // The projection can now be deleted client.delete(name).get(); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```java client.abort("$by_category").get(); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```java try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```java client.reset("$by_category").get(); ``` Resetting a projection that does not exist will result in an error. ```java try { client.reset("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```java String name = "countEvents_Update_" + java.util.UUID.randomUUID(); String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, "fromAll().when()").get(); client.update(name, js).get(); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```java try { client.update("Update Not existing projection", "fromAll().when()").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println("'Update Not existing projection' does not exists and can not be updated"); } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```java ProjectionDetails status = client.getStatus("$by_category").get(); System.out.println( status.getName() + ", " + status.getStatus() + ", " + status.getCheckpointStatus() + ", " + status.getMode() + ", " + status.getProgress()); ``` ## Get state Retrieves the state of a projection. ```java public static class CountResult { private int count; public int getCount() { return count; } public void setCount(final int count){ this.count = count; } } String name = "get_state_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. CountResult result = client .getState(name, CountResult.class) .get(); System.out.println(result); ``` ## Get result Retrieves the result of the named projection and partition. ```java String name = "get_result_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".transformBy((state) => state.count)" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. int result = client .getResult(name, int.class) .get(); System.out.println(result); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the stream * Use `fromEnd()` to begin from the current end of the stream * Use `fromRevision(long revision)` with a specific revision number (64-bit signed integer) ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("orders", options) .get(); ``` You can also start reading from a specific revision in the stream: ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10); ReadResult result = client.readStream("orders", options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readStream("orders", options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readStream("orders", options) .get(); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```java{4} JsonMapper mapper = new JsonMapper(); ReadStreamOptions options = ReadStreamOptions.get() .backwards() .fromEnd(); ReadResult result = client.readStream("orders", options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```java{11-15} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ReadResult result = null; try { result = client.readStream("some-stream", options) .get(); } catch (ExecutionException e) { Throwable innerException = e.getCause(); if (innerException instanceof StreamNotFoundException) { return; } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the transaction log * Use `fromEnd()` to begin from the current end of the transaction log * Use `fromPosition(Position position)` with a specific `Position` object containing commit and prepare coordinates ```java{2-3} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); ``` You can also start reading from a specific position in the transaction log: ```java{1,4-5} Position position = new Position(1000, 1000); ReadAllOptions options = ReadAllOptions.get() .forwards() .fromPosition(position); ReadResult result = client.readAll(options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readAll(options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readAll(options) .get(); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to the `Backwards`: ```java{2} ReadAllOptions options = ReadAllOptions.get() .backwards() .fromEnd(); ReadResult result = client.readAll(options) .get(); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```java{10-12} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); if (!recordedEvent.getEventType().startsWith("$")) { // Process the event } } ``` ## Java Reactive Streams The Java Reactive Streams API allows you to read events in a non-blocking manner, which is particularly useful for applications that require high throughput and low latency. The reactive API provides a way to subscribe to streams of events and process them as they arrive. ::: tabs#java @tab Reading from a stream ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Process the event System.out.println("Event: " + event.getEventType()); } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` @tab Reading from $all ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); Publisher publisher = client.readAllReactive(options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Filter out system events if needed if (!event.getEventType().startsWith("$")) { System.out.println("Event: " + event.getEventType()); } } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/java/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); // Handle the event } }; client.subscribeToStream("orders", listener); ``` **`$all` subscription:** ```java client.subscribeToAll(listener); ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```java client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromRevision(20) ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```java client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromPosition(new Position(1056, 1056)) ); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```java // Stream client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromEnd() ); // $all client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromEnd() ); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```java client.subscribeToStream( "$et-orders", listener, SubscribeToStreamOptions.get() .fromStart() .resolveLinkTos() ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `onCancelled` callback in the `SubscriptionListener`, which will get called when the subscription breaks. The `onCancelled` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```java client.subscribeToStream( "orders", new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getRevision()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToStreamOptions.get() .fromStart() ); ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```java client.subscribeToAll( new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getPosition()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToAllOptions.get() .fromStart() ); ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `onCaughtUp` callback in the `SubscriptionListener`. ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Processing event: " + event.getOriginalEvent().getEventType()); // Handle the event } @Override public void onCaughtUp(Subscription subscription) { System.out.println("Subscription caught up - now processing live events"); // Trigger any actions needed when caught up } @Override public void onCancelled(Subscription subscription, Throwable exception) { if (exception != null) { System.out.println("Subscription dropped: " + exception.getMessage()); } } }; client.subscribeToStream("orders", listener); ``` ::: tip The `onCaughtUp` callback is only triggered when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); SubscribeToAllOptions options = SubscribeToAllOptions.get().authenticated(credentials); client.subscribeToAll(listener, options); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("test-") .build(); SubscribeToAllOptions options = SubscribeToAllOptions.get() .filter(filter); client.subscribeToAll(listener, options); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```java String excludeSystemEventsRegex = "^[^\\$].*"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .build(); client.subscribeToAll(listener, SubscribeToAllOptions.get().filter(filter)); ``` ### Filtering by event type **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addEventTypePrefix("customer-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression("^user|^company") .build(); ``` ### Filtering by stream name **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("user-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withStreamNameRegularExpression("^account|^savings") .build(); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }) .build(); ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }, 1000) .build(); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```java {38-49} import io.kurrent.dbclient.AppendToStreamOptions; import io.kurrent.dbclient.EventData; import io.kurrent.dbclient.StreamState; import java.util.UUID; class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } public String getOrderId() { return orderId; } public String getCustomerId() { return customerId; } public double getTotalAmount() { return totalAmount; } public String getStatus() { return status; } } EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("orders", options, eventData) .get(); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```java {3,15-16} EventData eventData = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.any()); client.appendToStream("orders", options, eventData) .get(); // attempt to append the same event again client.appendToStream("orders", options, eventData) .get(); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. This is automatically set when using the builder methods like `builderAsJson()` or `builderAsBinary()`. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```java EventData eventDataOne = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-456", "customer-789", 249.99, "confirmed")) .build(); EventData eventDataTwo = EventData .builderAsJson( UUID.randomUUID(), "OrderPlaced", new OrderPlaced("order-457", "customer-789", 249.99, "confirmed")) .build(); AppendToStreamOptions options = AppendToStreamOptions.get() .streamState(StreamState.noStream()); client.appendToStream("no-stream-stream", options, eventDataOne) .get(); // attempt to append the same event again client.appendToStream("no-stream-stream", options, eventDataTwo) .get(); ``` There are several available expected revision options: * `StreamState.any()` - No concurrency check * `StreamState.noStream()` - Stream should not exist * `StreamState.streamExists()` - Stream should exist * `StreamState.streamRevision(long revision)` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. First, let's define the event classes for our ecommerce example: ```java public class PaymentProcessed { private String orderId; private String paymentId; private double amount; private String paymentMethod; public PaymentProcessed(String orderId, String paymentId, double amount, String paymentMethod) { this.orderId = orderId; this.paymentId = paymentId; this.amount = amount; this.paymentMethod = paymentMethod; } // getters omitted for brevity } public class OrderCancelled { private String orderId; private String reason; private String comment; public OrderCancelled(String orderId, String reason, String comment) { this.orderId = orderId; this.reason = reason; this.comment = comment; } // getters omitted for brevity } ``` Now, here's how to implement optimistic concurrency control: ```java ReadStreamOptions readOptions = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("order-12345", readOptions) .get(); // Get the current revision to use for optimistic concurrency long currentRevision = result.getLastStreamPosition(); // Two concurrent operations trying to update the same order EventData paymentProcessedEvent = EventData .builderAsJson( UUID.randomUUID(), "PaymentProcessed", new PaymentProcessed("order-12345", "payment-789", 149.99, "VISA")) .build(); EventData orderCancelledEvent = EventData .builderAsJson( UUID.randomUUID(), "OrderCancelled", new OrderCancelled("order-12345", "customer-request", "Customer changed mind")) .build(); // Process payment (succeeds) AppendToStreamOptions appendOptions = AppendToStreamOptions.get() .streamState(currentRevision); WriteResult paymentResult = client.appendToStream("order-12345", appendOptions, paymentProcessedEvent) .get(); // Cancel order (fails due to concurrency conflict) AppendToStreamOptions cancelOptions = AppendToStreamOptions.get() .streamState(currentRevision); client.appendToStream("order-12345", cancelOptions, orderCancelledEvent) .get(); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); AppendToStreamOptions options = AppendToStreamOptions.get() .authenticated(credentials); client.appendToStream("some-stream", options, eventData) .get(); ``` ## Atomic appends KurrentDB provides two operations for appending events to one or more streams in a single atomic transaction: `appendRecords` and `multiStreamAppend`. Both guarantee that either all writes succeed or the entire operation fails, but they differ in how records are organized, ordered, and validated. | | `appendRecords` | `multiStreamAppend` | |------------------------|-----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | **Available since** | KurrentDB 26.1 | KurrentDB 25.1 | | **Record ordering** | Interleaved. Records from different streams can be mixed, and their exact order is preserved in the global log. | Grouped. All records for a stream are sent together; ordering across streams is not guaranteed. | | **Consistency checks** | Decoupled. Can validate the state of any stream, including streams not being written to. | Coupled. Expected state is specified per stream being written to. | ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ### AppendRecords ::: note This feature is only available in KurrentDB 26.1 and later. ::: `appendRecords` appends events to one or more streams atomically. Each record specifies which stream it targets, and the exact order of records is preserved in the global log across all streams. #### Single stream The simplest usage appends events to a single stream: ```java EventData eventOne = EventData .builderAsJson("OrderPlaced", "{\"orderId\": \"123\"}".getBytes()) .build(); EventData eventTwo = EventData .builderAsJson("OrderShipped", "{\"orderId\": \"123\"}".getBytes()) .build(); client.appendRecords("order-123", Arrays.asList(eventOne, eventTwo)).get(); ``` When no expected state is provided, no consistency check is performed, which is equivalent to `StreamState.any()`. You can also pass an expected stream state for optimistic concurrency: ```java client.appendRecords("order-123", StreamState.noStream(), Arrays.asList(eventOne, eventTwo)).get(); ``` #### Multiple streams Use `AppendRecord` to target different streams. Records can be interleaved freely, and the global log preserves the exact order you specify: ```java List records = Arrays.asList( new AppendRecord("order-stream", EventData .builderAsJson("OrderCreated", "{\"orderId\": \"123\"}".getBytes()) .build()), new AppendRecord("inventory-stream", EventData .builderAsJson("ItemReserved", "{\"itemId\": \"abc\", \"quantity\": 2}".getBytes()) .build()), new AppendRecord("order-stream", EventData .builderAsJson("OrderConfirmed", "{\"orderId\": \"123\"}".getBytes()) .build()) ); client.appendRecords(records).get(); ``` #### Consistency checks Consistency checks let you validate the state of any stream, including streams you are not writing to, before the append is committed. All checks are evaluated atomically: if any check fails, the entire operation is rejected and an `AppendConsistencyViolationException` is thrown with details about every failing check and the actual state observed. ```java List records = Collections.singletonList( new AppendRecord("order-stream", EventData .builderAsJson("OrderConfirmed", "{\"orderId\": \"123\"}".getBytes()) .build()) ); // ensure the inventory stream exists before confirming the order, // even though we are not writing to it List checks = Collections.singletonList( new ConsistencyCheck.StreamStateCheck("inventory-stream", StreamState.streamExists()) ); client.appendRecords(records, checks).get(); ``` Because checks are decoupled from writes, you can validate the state of streams you are not writing to, enabling patterns where a business decision depends on the state of multiple streams but the resulting event is written to only one of them. ### MultiStreamAppend ::: note This feature is only available in KurrentDB 25.1 and later. ::: `multiStreamAppend` appends events to one or more streams atomically. Records are grouped per stream using `AppendStreamRequest`, where each request specifies a stream name, an expected state, and the events for that stream. ```java JsonMapper mapper = new JsonMapper(); Map metadata = new HashMap<>(); metadata.put("source", "OrderProcessingSystem"); byte[] metadataBytes = mapper.writeValueAsBytes(metadata); EventData orderEvent = EventData .builderAsJson("OrderCreated", mapper.writeValueAsBytes(new OrderCreated("12345", 99.99))) .metadataAsBytes(metadataBytes) .build(); EventData inventoryEvent = EventData .builderAsJson("ProductPurchased", mapper.writeValueAsBytes(new ProductPurchased("ABC123", 2, 19.99))) .metadataAsBytes(metadataBytes) .build(); List requests = Arrays.asList( new AppendStreamRequest( "order-stream-1", Collections.singletonList(orderEvent).iterator(), StreamState.any() ), new AppendStreamRequest( "product-stream-1", Collections.singletonList(inventoryEvent).iterator(), StreamState.any() ) ); MultiStreamAppendResponse result = client.multiStreamAppend(requests.iterator()).get(); ``` Each stream can only appear once in the request. The expected state is validated per stream before the transaction is committed. The result returns the position of the last appended record in the transaction and a collection of responses for each stream. --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or KurrentDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```java KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}"); KurrentDBClient client = KurrentDBClient.create(settings); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```java client.deleteStream(streamName, DeleteStreamOptions.get()).get(); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```java client.tombstoneStream(streamName, DeleteStreamOptions.get()).get(); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Java application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `kurrentdb-client` dependency to your project: ::: tabs @tab gradle ```groovy implementation 'io.kurrent:kurrentdb-client:1.1.x' ``` @tab maven ```xml io.kurrent kurrentdb-client 1.1.x ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, KurrentDB supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | | `feature` | `dns-lookup` | None | Enable specific client features. Use `dns-lookup` with `dnsDiscover=true` to resolve hostnames to multiple IP addresses for cluster discovery. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```java import io.kurrent.dbclient.KurrentDBClient; import io.kurrent.dbclient.KurrentDBClientSettings; import io.kurrent.dbclient.KurrentDBConnectionString; KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```java import io.kurrent.dbclient.EventData; import com.fasterxml.jackson.databind.json.JsonMapper; public class OrderPlaced { private String orderId; private String customerId; private double totalAmount; private String status; public OrderPlaced(String orderId, String customerId, double totalAmount, String status) { this.orderId = orderId; this.customerId = customerId; this.totalAmount = totalAmount; this.status = status; } } OrderPlaced event = new OrderPlaced("order-456", "customer-789", 249.99, "confirmed"); JsonMapper jsonMapper = new JsonMapper(); EventData eventData = EventData .builderAsJson("OrderPlaced", jsonMapper.writeValueAsBytes(event)) .build(); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```java client.appendToStream("orders", eventData).get(); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `orders` stream. ### Synchronous reading ```java import java.util.List; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); ReadResult result = client.readStream("orders", options) .get(); List events = result.getEvents(); ``` ### Asynchronous reading We also provide an asynchronous API for reading events using Java Reactive Streams. ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .maxCount(10); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { // subscription confirmed } @Override public void onNext(ReadMessage readMessage) { // Process the event RecordedEvent event = readMessage.getEvent().getOriginalEvent(); } @Override public void onError(Throwable throwable) { // handle error } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` When you read events from the stream, you get a collection of `ResolvedEvent` structures (synchronous) or `ReadMessage` objects (reactive). The event payload is returned as a byte array and needs to be deserialized. See more advanced scenarios in [reading events documentation](./reading-events.md). --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/observability.md' --- # Observability The Java client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project. Add these to your Maven `pom.xml` or Gradle `build.gradle`: ::: tabs#distribution @tab Maven ```xml io.opentelemetry opentelemetry-sdk 1.40.0 io.opentelemetry opentelemetry-exporter-logging 1.40.0 io.opentelemetry opentelemetry-exporter-otlp 1.40.0 io.opentelemetry.semconv opentelemetry-semconv 1.25.0-alpha ``` @tab Gradle ```groovy dependencies { implementation 'io.opentelemetry:opentelemetry-sdk:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-logging:1.40.0' implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.40.0' implementation 'io.opentelemetry.semconv:opentelemetry-semconv:1.25.0-alpha' } ``` ::: ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```java import com.eventstore.dbclient.*; import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class EventStoreObservability { public static void main(String[] args) { // Configure resource with service name Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Create console exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // Configure tracer provider SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .setResource(resource) .build(); // Register OpenTelemetry SDK globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); // Your KurrentDB client operations will now be traced KurrentDBClientSettings settings = KurrentDBConnectionString .parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBClient client = KurrentDBClient.create(settings); } } ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=java). You can configure multiple exporters simultaneously: ```java import io.opentelemetry.exporter.logging.LoggingSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; public class MultipleExporters { public static void configureOpenTelemetry() { Resource resource = Resource.getDefault().toBuilder() .put(SERVICE_NAME, "my-eventstore-app") .build(); // Console/logging exporter LoggingSpanExporter consoleExporter = LoggingSpanExporter.create(); // OTLP exporter for Jaeger/other OTLP-compatible backends OtlpGrpcSpanExporter otlpExporter = OtlpGrpcSpanExporter.builder() .setEndpoint("http://localhost:4317") .build(); // Configure tracer provider with multiple exporters SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(consoleExporter)) .addSpanProcessor(SimpleSpanProcessor.create(otlpExporter)) .setResource(resource) .build(); // Register globally OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .buildAndRegisterGlobal(); } } ``` For detailed configuration options, refer to the [OpenTelemetry Java documentation](https://opentelemetry.io/docs/languages/java/). ## Understanding Traces ### What Gets Traced The Java client automatically creates traces for append, catch-up and persistent subscription operations when OpenTelemetry is configured globally. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace of the exception | | --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a client The Java client provides a `PersistentSubscriptionsClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); PersistentSubscriptionsClient client = PersistentSubscriptionsClient.create(settings); ``` ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```java client.createToStream( "stream-name", "subscription-group", CreatePersistentSubscriptionToStreamOptions.get() .fromStart()); ``` ### Subscribing to `$all` ```java client.createToAll( "subscription-group", CreatePersistentSubscriptionToAllOptions.get() .fromStart()); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```java client.subscribeToStream( "stream-name", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); } @Override public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```java client.subscribeToAll( "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } public void onCancelled(PersistentSubscription subscription, Throwable exception) { if (exception == null) { System.out.println("Subscription is cancelled"); return; } System.out.println("Subscription was dropped due to " + exception.getMessage()); } }); ``` The `SubscribeToAll` method is identical to the `SubscribeToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```java client.subscribeToStream( "test-stream", "subscription-group", new PersistentSubscriptionListener() { @Override public void onEvent(PersistentSubscription subscription, int retryCount, ResolvedEvent event) { try { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); subscription.ack(event); } catch (Exception ex) { subscription.nack(NackAction.Park, ex.getMessage(), event); } } }); ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Unknown` | The client does not know what action to take. Let the server decide. | | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```java client.updateToStream( "stream-name", "subscription-group", UpdatePersistentSubscriptionToStreamOptions.get() .resolveLinkTos() .checkpointLowerBound(20)); ``` ## Persistent subscription settings Both the `Create` and `Update` methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```java client.deleteToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Creating a client The Java client provides a `KurrentDBProjectionManagementClient` that you can use to manage persistent subscriptions. ```java KurrentDBClientSettings settings = KurrentDBConnectionString.parseOrThrow("kurrentdb://localhost:2113?tls=false"); KurrentDBProjectionManagementClient client = KurrentDBProjectionManagementClient.create(settings); ``` ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```java String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; String name = "countEvents_Create_" + java.util.UUID.randomUUID(); client.create(name, js).get(); ``` Trying to create projections with the same name will result in an error: ```java try { client.create(name, js).get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("Conflict")) { System.out.println(name + " already exists"); } } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```java client.restartSubsystem().get(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.enable("$by_category").get(); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```java client.disable("$by_category").get(); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```java try { client.disable("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Delete a projection ```java // A projection must be disabled to allow it to be deleted. client.disable(name).get(); // The projection can now be deleted client.delete(name).get(); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```java client.abort("$by_category").get(); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```java try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```java client.reset("$by_category").get(); ``` Resetting a projection that does not exist will result in an error. ```java try { client.reset("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```java String name = "countEvents_Update_" + java.util.UUID.randomUUID(); String js = "fromAll()" + ".when({" + " $init: function() {" + " return {" + " count: 0" + " };" + " }," + " $any: function(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, "fromAll().when()").get(); client.update(name, js).get(); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```java try { client.update("Update Not existing projection", "fromAll().when()").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println("'Update Not existing projection' does not exists and can not be updated"); } } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```java List details = client.list().get(); for (ProjectionDetails detail: details) { System.out.println( detail.getName() + ", " + detail.getStatus() + ", " + detail.getCheckpointStatus() + ", " + detail.getMode() + ", " + detail.getProgress()); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```java ProjectionDetails status = client.getStatus("$by_category").get(); System.out.println( status.getName() + ", " + status.getStatus() + ", " + status.getCheckpointStatus() + ", " + status.getMode() + ", " + status.getProgress()); ``` ## Get state Retrieves the state of a projection. ```java public static class CountResult { private int count; public int getCount() { return count; } public void setCount(final int count){ this.count = count; } } String name = "get_state_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. CountResult result = client .getState(name, CountResult.class) .get(); System.out.println(result); ``` ## Get result Retrieves the result of the named projection and partition. ```java String name = "get_result_example"; String js = "fromAll()" + ".when({" + " $init() {" + " return {" + " count: 0," + " };" + " }," + " $any(s, e) {" + " s.count += 1;" + " }" + "})" + ".transformBy((state) => state.count)" + ".outputState();"; client.create(name, js).get(); Thread.sleep(500); //give it some time to process and have a state. int result = client .getResult(name, int.class) .get(); System.out.println(result); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the stream * Use `fromEnd()` to begin from the current end of the stream * Use `fromRevision(long revision)` with a specific revision number (64-bit signed integer) ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); ReadResult result = client.readStream("orders", options) .get(); ``` You can also start reading from a specific revision in the stream: ```java{3} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10); ReadResult result = client.readStream("orders", options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readStream("orders", options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readStream("orders", options) .get(); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the *stream position* to the end: ```java{4} JsonMapper mapper = new JsonMapper(); ReadStreamOptions options = ReadStreamOptions.get() .backwards() .fromEnd(); ReadResult result = client.readStream("orders", options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStreamResult`, which contains a property `ReadState`. This property can have the value `StreamNotFound` or `Ok`. It is important to check the value of this field before attempting to iterate an empty stream, as it will throw an exception. For example: ```java{11-15} ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromRevision(10) .maxCount(20); ReadResult result = null; try { result = client.readStream("some-stream", options) .get(); } catch (ExecutionException e) { Throwable innerException = e.getCause(); if (innerException instanceof StreamNotFoundException) { return; } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `fromStart()` to begin from the very beginning of the transaction log * Use `fromEnd()` to begin from the current end of the transaction log * Use `fromPosition(Position position)` with a specific `Position` object containing commit and prepare coordinates ```java{2-3} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); ``` You can also start reading from a specific position in the transaction log: ```java{1,4-5} Position position = new Position(1000, 1000); ReadAllOptions options = ReadAllOptions.get() .forwards() .fromPosition(position); ReadResult result = client.readAll(options) .get(); ``` You can then iterate synchronously through the result: ```java import com.fasterxml.jackson.databind.json.JsonMapper; JsonMapper mapper = new JsonMapper(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); System.out.println(mapper.writeValueAsString(recordedEvent.getEventData())); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromRevision(10) .maxCount(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .resolveLinkTos(); ReadResult result = client.readAll(options) .get(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```java{4} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart() .authenticated("admin", "changeit"); ReadResult result = client.readAll(options) .get(); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to the `Backwards`: ```java{2} ReadAllOptions options = ReadAllOptions.get() .backwards() .fromEnd(); ReadResult result = client.readAll(options) .get(); ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```java{10-12} ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); ReadResult result = client.readAll(options) .get(); for (ResolvedEvent resolvedEvent : result.getEvents()) { RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); if (!recordedEvent.getEventType().startsWith("$")) { // Process the event } } ``` ## Java Reactive Streams The Java Reactive Streams API allows you to read events in a non-blocking manner, which is particularly useful for applications that require high throughput and low latency. The reactive API provides a way to subscribe to streams of events and process them as they arrive. ::: tabs#java @tab Reading from a stream ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadStreamOptions options = ReadStreamOptions.get() .forwards() .fromStart(); Publisher publisher = client.readStreamReactive("orders", options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Process the event System.out.println("Event: " + event.getEventType()); } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` @tab Reading from $all ```java import org.reactivestreams.Subscriber; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import java.util.concurrent.CountDownLatch; ReadAllOptions options = ReadAllOptions.get() .forwards() .fromStart(); Publisher publisher = client.readAllReactive(options); final CountDownLatch latch = new CountDownLatch(1); publisher.subscribe(new Subscriber() { @Override public void onSubscribe(Subscription subscription) { } @Override public void onNext(ReadMessage readMessage) { RecordedEvent event = readMessage.getEvent().getOriginalEvent(); // Filter out system events if needed if (!event.getEventType().startsWith("$")) { System.out.println("Event: " + event.getEventType()); } } @Override public void onError(Throwable throwable) { // Handle error latch.countDown(); } @Override public void onComplete() { latch.countDown(); } }); latch.await(); ``` ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/java/v1.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Received event" + event.getOriginalEvent().getRevision() + "@" + event.getOriginalEvent().getStreamId()); // Handle the event } }; client.subscribeToStream("orders", listener); ``` **`$all` subscription:** ```java client.subscribeToAll(listener); ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```java client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromRevision(20) ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```java client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromPosition(new Position(1056, 1056)) ); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```java // Stream client.subscribeToStream( "orders", listener, SubscribeToStreamOptions.get() .fromEnd() ); // $all client.subscribeToAll( listener, SubscribeToAllOptions.get() .fromEnd() ); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```java client.subscribeToStream( "$et-orders", listener, SubscribeToStreamOptions.get() .fromStart() .resolveLinkTos() ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `onCancelled` callback in the `SubscriptionListener`, which will get called when the subscription breaks. The `onCancelled` callback allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```java client.subscribeToStream( "orders", new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getRevision()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToStreamOptions.get() .fromStart() ); ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```java client.subscribeToAll( new SubscriptionListener() { StreamPosition checkpoint = StreamPosition.start(); @Override public void onEvent(Subscription subscription, ResolvedEvent event) { HandleEvent(event); checkpoint = StreamPosition.position(event.getOriginalEvent().getPosition()); } @Override public void onCancelled(Subscription subscription, Throwable exception) { // Subscription was dropped by the user. if (exception == null) return; System.out.println("Subscription was dropped due to " + exception.getMessage()); Resubscribe(checkpoint); } }, SubscribeToAllOptions.get() .fromStart() ); ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `onCaughtUp` callback in the `SubscriptionListener`. ```java SubscriptionListener listener = new SubscriptionListener() { @Override public void onEvent(Subscription subscription, ResolvedEvent event) { System.out.println("Processing event: " + event.getOriginalEvent().getEventType()); // Handle the event } @Override public void onCaughtUp(Subscription subscription) { System.out.println("Subscription caught up - now processing live events"); // Trigger any actions needed when caught up } @Override public void onCancelled(Subscription subscription, Throwable exception) { if (exception != null) { System.out.println("Subscription dropped: " + exception.getMessage()); } } }; client.subscribeToStream("orders", listener); ``` ::: tip The `onCaughtUp` callback is only triggered when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```java UserCredentials credentials = new UserCredentials("admin", "changeit"); SubscribeToAllOptions options = SubscribeToAllOptions.get().authenticated(credentials); client.subscribeToAll(listener, options); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("test-") .build(); SubscribeToAllOptions options = SubscribeToAllOptions.get() .filter(filter); client.subscribeToAll(listener, options); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```java String excludeSystemEventsRegex = "^[^\\$].*"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .build(); client.subscribeToAll(listener, SubscribeToAllOptions.get().filter(filter)); ``` ### Filtering by event type **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addEventTypePrefix("customer-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression("^user|^company") .build(); ``` ### Filtering by stream name **By prefix:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .addStreamNamePrefix("user-") .build(); ``` **By regular expression:** ```java SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withStreamNameRegularExpression("^account|^savings") .build(); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }) .build(); ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```java String excludeSystemEventsRegex = "/^[^\\$].*/"; SubscriptionFilter filter = SubscriptionFilter.newBuilder() .withEventTypeRegularExpression(excludeSystemEventsRegex) .withCheckpointer( new Checkpointer() { @Override public CompletableFuture onCheckpoint(Subscription subscription, Position position) { // Save commit position to a persistent store as a checkpoint System.out.println("checkpoint taken at {position.getCommitUnsigned}"); return CompletableFuture.completedFuture(null); } }, 1000) .build(); ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: ## Configuring Backpressure The client allows you to configure backpressure to control how many events are buffered on the client side before requesting more from the server. | Option | Description | Default Value | |----------------|-----------------------------------------------------------------------------|---------------| | batchSize | The maximum number of events the client will request from the server in a single batch. | 512 | | thresholdRatio | The fraction of the `batchSize` at which the client will send a new request for more events. | 0.25 | By default, the client requests up to 512 events at a time. It will automatically request more events when the number of buffered (unprocessed) events falls below 25% of the batch size. --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/appending-events.md' --- # Appending events When you start working with EventStoreDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to EventStoreDB is to create an `EventData` object and call `appendToStream` method. ```ts {32-43} import { v4 as uuid } from "uuid"; const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event, { expectedRevision: NO_STREAM, }); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to EventStoreDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, EventStoreDB will only append one of the events to the stream. For example, the following code will only append a single event: ```ts const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event); // attempt to append the same event again await client.appendToStream("orders", event); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of EventStoreDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. EventStoreDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. You can use existing methods `jsonEvent` or `binaryEvent` to create the `EventData` object, which will set the content type accordingly. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform EventStoreDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```ts{28-30} const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-123", paymentId: "payment-789", amount: 99.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-123-stream", orderPlacedEvent, { streamState: NO_STREAM, }); // attempt to append another event to the same stream expecting it to not exist await client.appendToStream("order-123-stream", paymentProcessedEvent, { streamState: NO_STREAM, }); ``` There are several available expected revision options: * `any` - No concurrency check * `no_stream` - Stream should not exist * `stream_exists` - Stream should exist * `bigint` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from EventStoreDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```ts {6,9,26-28,41-43} const events = client.readStream("order-stream", { fromRevision: START, direction: FORWARDS, }); let revision: AppendStreamState = NO_STREAM; for await (const { event } of events) { revision = event?.revision ?? revision; } const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-456", customerId: "customer-789", totalAmount: 149.99, items: [ { productId: "prod-123", quantity: 2, price: 49.99 }, { productId: "prod-456", quantity: 1, price: 49.99 } ] }, }); await client.appendToStream("order-stream", orderPlacedEvent, { expectedRevision: revision, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-456", paymentId: "payment-789", amount: 149.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-stream", paymentProcessedEvent, { expectedRevision: revision, }); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```ts const credentials = { username: "admin", password: "changeit", }; await client.appendToStream("some-stream", event, { credentials, }); ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```ts const connectionString = `esdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}`; const client = EventStoreDBClient.connectionString(connectionString); ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/delete-stream.md' --- # Deleting Events In EventStoreDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, EventStoreDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in EventStoreDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```ts await client.deleteStream(streamName); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in EventStoreDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```ts await client.tombstoneStream(streamName); ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/getting-started.md' --- # Getting started This guide will help you get started with EventStoreDB in your Java application. It covers the basic steps to connect to EventStoreDB, create events, append them to streams, and read them back. ## Required packages Add the `@eventstore/db-client` dependency to your project: ::: tabs @tab NPM ```bash npm install --save @eventstore/db-client@~6.2 ``` @tab Yarn ```bash yarn add @eventstore/db-client@~6.2 ``` ::: ## Connecting to EventStoreDB To connect your application to EventStoreDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to EventStoreDB is using secure mode (which is the default). However, if your EventStoreDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: EventStoreDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`esdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`esdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `esdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `esdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, esdb supports gossip on single-node deployments, so `esdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` esdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` esdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` esdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `esdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```ts const client = EventStoreDBClient.connectionString`esdb://localhost:2113?tls=false`; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to EventStoreDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```ts type OrderCreated = JSONEventType< "OrderCreated", { orderId: string; customerId: string; items: Array<{ productId: string; productName: string; quantity: number; price: number; }>; totalAmount: number; orderDate: string; } >; const event = jsonEvent({ type: "OrderCreated", data: { orderId: uuid(), customerId: "customer-123", items: [ { productId: "product-456", productName: "Wireless Headphones", quantity: 1, price: 99.99 }, { productId: "product-789", productName: "USB Cable", quantity: 2, price: 15.99 } ], totalAmount: 131.97, orderDate: new Date().toISOString(), }, }); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```ts await client.appendToStream("order-123", event); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `order-123` stream. ```ts const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(events); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/observability.md' --- # Observability The Node.js client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project: ```bash npm install --save @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @eventstore/opentelemetry ``` For exporters, you might also want to install: ```bash npm install --save @opentelemetry/sdk-trace-node npm install --save @opentelemetry/exporter-trace-otlp-http npm install --save @opentelemetry/semantic-conventions ``` ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```ts const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { EventStoreDBInstrumentation } = require('@eventstore/opentelemetry'); const provider = new NodeTracerProvider(); provider.register(); registerInstrumentations({ instrumentations: [ new EventStoreDBInstrumentation(), ], }); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=js). You can configure multiple exporters simultaneously: ```ts import { InMemorySpanExporter, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const memoryExporter = new InMemorySpanExporter(); const otlpExporter = new OTLPTraceExporter({ url: "http://localhost:4317" }); // change this to your OTLP receiver address const consoleExporter = new ConsoleSpanExporter(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); ``` For detailed configuration options, refer to the [OpenTelemetry Node.js documentation](https://opentelemetry.io/docs/languages/js/). ## Understanding Traces ### What Gets Traced The Node.js client automatically creates traces for append and subscription operations when the EventStoreDB instrumentation is registered. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | --------------------------------- | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `eventstoredb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.eventstoredb.stream` | Stream name or identifier | `user-events-123` | | `db.eventstoredb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.eventstoredb.event.id` | Event identifier | `event-456` | | `db.eventstoredb.event.type` | Event type identifier | `user.created` | | `server.address` | EventStoreDB server address | `localhost` | | `server.port` | EventStoreDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@eventstore/db-client"; await client.createPersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults() ); ``` ### Subscribing to `$all` ```ts import { persistentSubscriptionToAllSettingsFromDefaults, streamNameFilter } from "@eventstore/db-client"; await client.createPersistentSubscriptionToAll( "group-name", persistentSubscriptionToAllSettingsFromDefaults(), { filter: streamNameFilter({ prefixes: ["test"] }) } ); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```ts import { PARK } from "@eventstore/db-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```ts const subscription = client.subscribeToPersistentSubscriptionToAll("group-name"); try { for await (const event of subscription) { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The `subscribeToPersistentSubscriptionToAll` method is identical to the `subscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```ts import { PARK } from "@eventstore/db-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log( `handling event ${event.event?.type} with retryCount ${event.retryCount}` ); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `park` | Park the message and do not resend. Put it on poison queue. | | `retry` | Explicitly retry the message. | | `skip` | Skip this message do not resend and do not put in poison queue. | | `stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@eventstore/db-client"; await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```ts await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/projections.md' --- # Projection management The client provides a way to manage projections in EventStoreDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```ts const name = `countEvents_Create_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, projection); ``` Trying to create projections with the same name will result in an error: ```ts try { await client.createProjection(name, projection); } catch (err) { if (!isCommandError(err) || !err.message.includes("Conflict")) throw err; console.log(`${name} already exists`); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```ts await client.restartSubsystem(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.enableProjection("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.enableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.disableProjection("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.disableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Delete a projection ```ts // A projection must be disabled to allow it to be deleted. await client.disableProjection(name); // The projection can now be deleted await client.deleteProjection(name); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```ts await client.abortProjection(name); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```ts try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```ts await client.resetProjection(name); ``` Resetting a projection that does not exist will result in an error. ```ts const projectionName = "projection that does not exist"; try { await client.resetProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```ts const name = `countEvents_Update_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, "fromAll().when()"); await client.updateProjection(name, projection); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.updateProjection(projectionName, "fromAll().when()"); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ::: note This is currently not available in the nodejs client ::: ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projections = await client.listProjections(); for (const { name, status, checkpointStatus, progress } of projections) { console.log(name, status, checkpointStatus, progress); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projection = await client.getProjectionStatus(name); console.log( projection.name, projection.status, projection.checkpointStatus, projection.progress ); ``` ## Get state Retrieves the state of a projection. ```ts interface CountProjectionState { count: number; } const name = `get_state_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to count event await delay(500); const state = await client.getProjectionState(name); console.log(`Counted ${state.count} events.`); ``` ## Get result Retrieves the result of the named projection and partition. ```ts const name = `get_result_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to have a result. await delay(500); const result = await client.getProjectionResult(name); console.log(`Counted ${result} events.`); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/reading-events.md' --- # Reading Events EventStoreDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in EventStoreDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in EventStoreDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `start` to begin from the very beginning of the stream * Use `end` to begin from the current end of the stream * Use `fromRevision` with a specific revision number (64-bit signed integer) ```ts{2-3} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` You can also start reading from a specific revision in the stream: ```ts{3} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: 10, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```ts{4} const events = client.readStream("some-stream", { direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```ts{2-3} const events = client.readStream("some-stream", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `StreamingRead` that you can iterate over. When iterating over events from a non-existent stream, it will throw a `StreamNotFoundError` exception. It is important to handle this exception when attempting to iterate a stream that may not exist. For example: ```ts{12-14} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: 10, maxCount: 20, }); try { for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } } catch (error) { if (error instanceof StreamNotFoundError) { return; } throw error; } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```ts{2-3} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` You can also start reading from a specific position in the transaction log: ```ts{3} const events = client.readAll({ direction: FORWARDS, fromPosition: 20, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```ts{4} const events = client.readAll({ direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("some-stream", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `BACKWARDS`: ```ts{2-3} const events = client.readStream("some-stream", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events EventStoreDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```ts{8-9} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); for await (const resolvedEvent of events) { if (resolvedEvent.event?.type.startsWith("$")) continue; console.log(resolvedEvent.event?.type); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/legacy/v6.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```ts const subscription = client.subscribeToStream("some-stream"); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); // handle event } ``` **`$all` subscription:** ```ts const subscription = client.subscribeToAll(); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}`); // handle event } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```ts const subscription = client.subscribeToStream( "some-stream", { fromRevision: BigInt(20), } ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```ts const subscription = client.subscribeToAll({ fromPosition: { commit: BigInt(1056), prepare: BigInt(1056), }, }); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```ts // Stream const subscription = client.subscribeToStream("orders", { fromRevision: END }); // $all const subscription = client.subscribeToAll({ fromPosition: END }); ``` ## Resolving link-to events Link-to events point to events in other streams in EventStoreDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```ts const subscription = client.subscribeToStream( "$et-orders", { fromRevision: START, resolveLinkTos: true, } ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```ts{7,14-16} import { ReadRevision, START } from "@eventstore/db-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToStream( "orders", { fromRevision: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```ts{6,13-15} import { ReadRevision, START } from "@eventstore/db-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToAll( { fromPosition: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```ts{10-12} const subscription = client.subscribeToStream("orders"); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) .on("caughtUp", () => { console.log("Subscription caught up - now processing live events"); }) ``` ::: tip The `caughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```ts{4-7} const subscription = client.subscribeToStream( "orders", { credentials: { username: "admin", password: "changeit", }, } ); ``` ## Server-side Filtering EventStoreDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```ts const subscription = client.subscribeToAll({ filter: streamNameFilter({ prefixes: ["test-", "other-"] }), }); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```ts{4} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents(), }) .on("data", (resolvedEvent) => { console.log( `Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); }); ``` ### Filtering by event type **By prefix:** ```ts const filter = eventTypeFilter({ prefixes: ["customer-"], }); ``` **By regular expression:** ```ts const filter = eventTypeFilter({ regex: "^user|^company", }); ``` ### Filtering by stream name **By prefix:** ```ts const filter = streamNameFilter({ prefixes: ["user-"], }); ``` **By regular expression:** ```ts const filter = streamNameFilter({ regex: "^account|^savings", }); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents({ async checkpointReached(subscription, position) { // do something with the position console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: eventTypeFilter({ regex: "^[^$].*", checkpointInterval: 1000, checkpointReached(subscription, position) { // Save commit position to a persistent store as a checkpoint console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```ts {32-43} import { v4 as uuid } from "uuid"; const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event, { streamState NO_STREAM, }); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```ts const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event); // attempt to append the same event again await client.appendToStream("orders", event); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. You can use existing methods `jsonEvent` or `binaryEvent` to create the `EventData` object, which will set the content type accordingly. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```ts{28-30} const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-123", paymentId: "payment-789", amount: 99.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-123-stream", orderPlacedEvent, { streamState: NO_STREAM, }); // attempt to append another event to the same stream expecting it to not exist await client.appendToStream("order-123-stream", paymentProcessedEvent, { streamState: NO_STREAM, }); ``` There are several available expected revision options: * `any` - No concurrency check * `no_stream` - Stream should not exist * `stream_exists` - Stream should exist * `bigint` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```ts {6,9,26-28,41-43} const events = client.readStream("order-stream", { fromRevision: START, direction: FORWARDS, }); let revision: AppendStreamState = NO_STREAM; for await (const { event } of events) { revision = event?.revision ?? revision; } const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-456", customerId: "customer-789", totalAmount: 149.99, items: [ { productId: "prod-123", quantity: 2, price: 49.99 }, { productId: "prod-456", quantity: 1, price: 49.99 } ] }, }); await client.appendToStream("order-stream", orderPlacedEvent, { streamState revision, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-456", paymentId: "payment-789", amount: 149.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-stream", paymentProcessedEvent, { streamState revision, }); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```ts const credentials = { username: "admin", password: "changeit", }; await client.appendToStream("some-stream", event, { credentials, }); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```ts const connectionString = `kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}`; const client = KurrentDBClient.connectionString(connectionString); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```ts await client.deleteStream(streamName); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```ts await client.tombstoneStream(streamName); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Node.js application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `@kurrent/kurrentdb-client` dependency to your project: ::: tabs @tab NPM ```bash npm install --save @kurrent/kurrentdb-client@~1.0 ``` @tab Yarn ```bash yarn add @kurrent/kurrentdb-client@~1.0 ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting to a single node, or to a multi-node cluster using multiple gossip seed endpoints * **`kurrentdb+discover://`** - for connecting using DNS discovery with a single DNS endpoint When using `kurrentdb://` with multiple endpoints separated by commas, the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. If one of the nodes is down, the client will try another endpoint. With `kurrentdb+discover://`, the client resolves a single DNS endpoint to discover the cluster topology. This is useful when you have a DNS `A` record pointing to your cluster nodes. ::: warning Only one host with +discover When using `kurrentdb+discover://`, only a single host should be provided. If multiple hosts are specified, only the first one will be used for discovery and the rest will be ignored. If you need to specify multiple endpoints for redundancy, use `kurrentdb://` without `+discover` instead. ::: ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For multi-node clusters where you know the individual node addresses, use multiple gossip seed endpoints: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` The client will use the Gossip API to discover the cluster topology and select the best node based on the configured node preference. For cluster connections using DNS discovery, use a single DNS endpoint: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Timeout in seconds for each gossip request during cluster discovery. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | `10000` | Default timeout for client operations, in milliseconds. | | `keepAliveInterval` | Number | `10000` | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Number | `10000` | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```ts const client = KurrentDBClient.connectionString`kurrentdb://localhost:2113?tls=false`; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```ts type OrderCreated = JSONEventType< "OrderCreated", { orderId: string; customerId: string; items: Array<{ productId: string; productName: string; quantity: number; price: number; }>; totalAmount: number; orderDate: string; } >; const event = jsonEvent({ type: "OrderCreated", data: { orderId: uuid(), customerId: "customer-123", items: [ { productId: "product-456", productName: "Wireless Headphones", quantity: 1, price: 99.99 }, { productId: "product-789", productName: "USB Cable", quantity: 2, price: 15.99 } ], totalAmount: 131.97, orderDate: new Date().toISOString(), }, }); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```ts await client.appendToStream("order-123", event); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `order-123` stream. ```ts const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(events); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/observability.md' --- # Observability The Node.js client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project: ```bash npm install --save @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @kurrent/opentelemetry ``` For exporters, you might also want to install: ```bash npm install --save @opentelemetry/sdk-trace-node npm install --save @opentelemetry/exporter-trace-otlp-http npm install --save @opentelemetry/semantic-conventions ``` ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```ts const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { KurrentDBInstrumentation } = require('@eventstore/opentelemetry'); const provider = new NodeTracerProvider(); provider.register(); registerInstrumentations({ instrumentations: [ new KurrentDBInstrumentation(), ], }); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=js). You can configure multiple exporters simultaneously: ```ts import { InMemorySpanExporter, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const memoryExporter = new InMemorySpanExporter(); const otlpExporter = new OTLPTraceExporter({ url: "http://localhost:4317" }); // change this to your OTLP receiver address const consoleExporter = new ConsoleSpanExporter(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); ``` For detailed configuration options, refer to the [OpenTelemetry Node.js documentation](https://opentelemetry.io/docs/languages/js/). ## Understanding Traces ### What Gets Traced The Node.js client automatically creates traces for append and subscription operations when the KurrentDB instrumentation is registered. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | ------------------------------ | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `kurrentdb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | | `db.kurrentdb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.kurrentdb.event.id` | Event identifier | `event-456` | | `db.kurrentdb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace | | --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults() ); ``` ### Subscribing to `$all` ```ts import { persistentSubscriptionToAllSettingsFromDefaults, streamNameFilter } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToAll( "group-name", persistentSubscriptionToAllSettingsFromDefaults(), { filter: streamNameFilter({ prefixes: ["test"] }) } ); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```ts const subscription = client.subscribeToPersistentSubscriptionToAll("group-name"); try { for await (const event of subscription) { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The `subscribeToPersistentSubscriptionToAll` method is identical to the `subscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log( `handling event ${event.event?.type} with retryCount ${event.retryCount}` ); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `park` | Park the message and do not resend. Put it on poison queue. | | `retry` | Explicitly retry the message. | | `skip` | Skip this message do not resend and do not put in poison queue. | | `stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```ts await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```ts const name = `countEvents_Create_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, projection); ``` Trying to create projections with the same name will result in an error: ```ts try { await client.createProjection(name, projection); } catch (err) { if (!isCommandError(err) || !err.message.includes("Conflict")) throw err; console.log(`${name} already exists`); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```ts await client.restartSubsystem(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.enableProjection("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.enableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.disableProjection("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.disableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Delete a projection ```ts // A projection must be disabled to allow it to be deleted. await client.disableProjection(name); // The projection can now be deleted await client.deleteProjection(name); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```ts await client.abortProjection(name); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```ts try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```ts await client.resetProjection(name); ``` Resetting a projection that does not exist will result in an error. ```ts const projectionName = "projection that does not exist"; try { await client.resetProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```ts const name = `countEvents_Update_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, "fromAll().when()"); await client.updateProjection(name, projection); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.updateProjection(projectionName, "fromAll().when()"); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ::: note This is currently not available in the nodejs client ::: ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projections = await client.listProjections(); for (const { name, status, checkpointStatus, progress } of projections) { console.log(name, status, checkpointStatus, progress); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projection = await client.getProjectionStatus(name); console.log( projection.name, projection.status, projection.checkpointStatus, projection.progress ); ``` ## Get state Retrieves the state of a projection. ```ts interface CountProjectionState { count: number; } const name = `get_state_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to count event await delay(500); const state = await client.getProjectionState(name); console.log(`Counted ${state.count} events.`); ``` ## Get result Retrieves the result of the named projection and partition. ```ts const name = `get_result_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to have a result. await delay(500); const result = await client.getProjectionResult(name); console.log(`Counted ${result} events.`); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `start` to begin from the very beginning of the stream * Use `end` to begin from the current end of the stream * Use `fromRevision` with a specific revision number (64-bit signed integer) ```ts{2-3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` You can also start reading from a specific revision in the stream: ```ts{3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readStream("order-123", { direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `StreamingRead` that you can iterate over. When iterating over events from a non-existent stream, it will throw a `StreamNotFoundError` exception. It is important to handle this exception when attempting to iterate a stream that may not exist. For example: ```ts{12-14} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 20, }); try { for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } } catch (error) { if (error instanceof StreamNotFoundError) { return; } throw error; } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```ts{2-3} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` You can also start reading from a specific position in the transaction log: ```ts{3} const events = client.readAll({ direction: FORWARDS, fromPosition: 20, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readAll({ direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `BACKWARDS`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```ts{8-9} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); for await (const resolvedEvent of events) { if (resolvedEvent.event?.type.startsWith("$")) continue; console.log(resolvedEvent.event?.type); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```ts const subscription = client.subscribeToStream("some-stream"); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); // handle event } ``` **`$all` subscription:** ```ts const subscription = client.subscribeToAll(); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}`); // handle event } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```ts const subscription = client.subscribeToStream( "some-stream", { fromRevision: BigInt(20), } ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```ts const subscription = client.subscribeToAll({ fromPosition: { commit: BigInt(1056), prepare: BigInt(1056), }, }); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```ts // Stream const subscription = client.subscribeToStream("orders", { fromRevision: END }); // $all const subscription = client.subscribeToAll({ fromPosition: END }); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```ts const subscription = client.subscribeToStream( "$et-orders", { fromRevision: START, resolveLinkTos: true, } ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```ts{7,14-16} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToStream( "orders", { fromRevision: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```ts{6,13-15} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToAll( { fromPosition: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```ts{10-12} const subscription = client.subscribeToStream("orders"); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) .on("caughtUp", () => { console.log("Subscription caught up - now processing live events"); }) ``` ::: tip The `caughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```ts{4-7} const subscription = client.subscribeToStream( "orders", { credentials: { username: "admin", password: "changeit", }, } ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```ts const subscription = client.subscribeToAll({ filter: streamNameFilter({ prefixes: ["test-", "other-"] }), }); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```ts{4} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents(), }) .on("data", (resolvedEvent) => { console.log( `Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); }); ``` ### Filtering by event type **By prefix:** ```ts const filter = eventTypeFilter({ prefixes: ["customer-"], }); ``` **By regular expression:** ```ts const filter = eventTypeFilter({ regex: "^user|^company", }); ``` ### Filtering by stream name **By prefix:** ```ts const filter = streamNameFilter({ prefixes: ["user-"], }); ``` **By regular expression:** ```ts const filter = streamNameFilter({ regex: "^account|^savings", }); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents({ async checkpointReached(subscription, position) { // do something with the position console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: eventTypeFilter({ regex: "^[^$].*", checkpointInterval: 1000, checkpointReached(subscription, position) { // Save commit position to a persistent store as a checkpoint console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```ts {32-43} import { v4 as uuid } from "uuid"; const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event, { streamState NO_STREAM, }); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```ts const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event); // attempt to append the same event again await client.appendToStream("orders", event); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. You can use existing methods `jsonEvent` or `binaryEvent` to create the `EventData` object, which will set the content type accordingly. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```ts{28-30} const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-123", paymentId: "payment-789", amount: 99.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-123-stream", orderPlacedEvent, { streamState: NO_STREAM, }); // attempt to append another event to the same stream expecting it to not exist await client.appendToStream("order-123-stream", paymentProcessedEvent, { streamState: NO_STREAM, }); ``` There are several available expected revision options: * `any` - No concurrency check * `no_stream` - Stream should not exist * `stream_exists` - Stream should exist * `bigint` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```ts {6,9,26-28,41-43} const events = client.readStream("order-stream", { fromRevision: START, direction: FORWARDS, }); let revision: AppendStreamState = NO_STREAM; for await (const { event } of events) { revision = event?.revision ?? revision; } const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-456", customerId: "customer-789", totalAmount: 149.99, items: [ { productId: "prod-123", quantity: 2, price: 49.99 }, { productId: "prod-456", quantity: 1, price: 49.99 } ] }, }); await client.appendToStream("order-stream", orderPlacedEvent, { streamState revision, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-456", paymentId: "payment-789", amount: 149.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-stream", paymentProcessedEvent, { streamState revision, }); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```ts const credentials = { username: "admin", password: "changeit", }; await client.appendToStream("some-stream", event, { credentials, }); ``` ## Append to multiple streams ::: note This feature is only available in KurrentDB 25.1 and later. ::: You can append events to multiple streams in a single atomic operation. Either all streams are updated, or the entire operation fails. ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ```ts import { jsonEvent } from "@kurrent/kurrentdb-client"; import { v4 as uuid } from "uuid"; const metadata = { source: "OrderProcessingSystem", version: "1.0" }; const requests = [ { streamName: "order-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "OrderCreated", data: { orderId: "12345", amount: 99.99 }, metadata }) ] }, { streamName: "inventory-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "ItemReserved", data: { itemId: "ABC123", quantity: 2 }, metadata }) ] } ]; await client.multiStreamAppend(requests); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```ts const connectionString = `kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}`; const client = KurrentDBClient.connectionString(connectionString); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```ts await client.deleteStream(streamName); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```ts await client.tombstoneStream(streamName); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Java application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `@kurrent/kurrentdb-client` dependency to your project: ::: tabs @tab NPM ```bash npm install --save @kurrent/kurrentdb-client@~1.1 ``` @tab Yarn ```bash yarn add @kurrent/kurrentdb-client@~1.1 ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting to a single node, or to a multi-node cluster using multiple gossip seed endpoints * **`kurrentdb+discover://`** - for connecting using DNS discovery with a single DNS endpoint When using `kurrentdb://` with multiple endpoints separated by commas, the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. If one of the nodes is down, the client will try another endpoint. With `kurrentdb+discover://`, the client resolves a single DNS endpoint to discover the cluster topology. This is useful when you have a DNS `A` record pointing to your cluster nodes. ::: warning Only one host with +discover When using `kurrentdb+discover://`, only a single host should be provided. If multiple hosts are specified, only the first one will be used for discovery and the rest will be ignored. If you need to specify multiple endpoints for redundancy, use `kurrentdb://` without `+discover` instead. ::: ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For multi-node clusters where you know the individual node addresses, use multiple gossip seed endpoints: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` The client will use the Gossip API to discover the cluster topology and select the best node based on the configured node preference. For cluster connections using DNS discovery, use a single DNS endpoint: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Timeout in seconds for each gossip request during cluster discovery. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | `10000` | Default timeout for client operations, in milliseconds. | | `keepAliveInterval` | Number | `10000` | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Number | `10000` | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```ts const client = KurrentDBClient.connectionString`kurrentdb://localhost:2113?tls=false`; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```ts type OrderCreated = JSONEventType< "OrderCreated", { orderId: string; customerId: string; items: Array<{ productId: string; productName: string; quantity: number; price: number; }>; totalAmount: number; orderDate: string; } >; const event = jsonEvent({ type: "OrderCreated", data: { orderId: uuid(), customerId: "customer-123", items: [ { productId: "product-456", productName: "Wireless Headphones", quantity: 1, price: 99.99 }, { productId: "product-789", productName: "USB Cable", quantity: 2, price: 15.99 } ], totalAmount: 131.97, orderDate: new Date().toISOString(), }, }); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```ts await client.appendToStream("order-123", event); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `order-123` stream. ```ts const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(events); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/observability.md' --- # Observability The Node.js client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project: ```bash npm install --save @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @kurrent/opentelemetry ``` For exporters, you might also want to install: ```bash npm install --save @opentelemetry/sdk-trace-node npm install --save @opentelemetry/exporter-trace-otlp-http npm install --save @opentelemetry/semantic-conventions ``` ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```ts const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { KurrentDBInstrumentation } = require('@eventstore/opentelemetry'); const provider = new NodeTracerProvider(); provider.register(); registerInstrumentations({ instrumentations: [ new KurrentDBInstrumentation(), ], }); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=js). You can configure multiple exporters simultaneously: ```ts import { InMemorySpanExporter, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const memoryExporter = new InMemorySpanExporter(); const otlpExporter = new OTLPTraceExporter({ url: "http://localhost:4317" }); // change this to your OTLP receiver address const consoleExporter = new ConsoleSpanExporter(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); ``` For detailed configuration options, refer to the [OpenTelemetry Node.js documentation](https://opentelemetry.io/docs/languages/js/). ## Understanding Traces ### What Gets Traced The Node.js client automatically creates traces for append and subscription operations when the KurrentDB instrumentation is registered. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | ------------------------------ | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `kurrentdb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | | `db.kurrentdb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.kurrentdb.event.id` | Event identifier | `event-456` | | `db.kurrentdb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace | | --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults() ); ``` ### Subscribing to `$all` ```ts import { persistentSubscriptionToAllSettingsFromDefaults, streamNameFilter } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToAll( "group-name", persistentSubscriptionToAllSettingsFromDefaults(), { filter: streamNameFilter({ prefixes: ["test"] }) } ); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```ts const subscription = client.subscribeToPersistentSubscriptionToAll("group-name"); try { for await (const event of subscription) { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The `subscribeToPersistentSubscriptionToAll` method is identical to the `subscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log( `handling event ${event.event?.type} with retryCount ${event.retryCount}` ); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `park` | Park the message and do not resend. Put it on poison queue. | | `retry` | Explicitly retry the message. | | `skip` | Skip this message do not resend and do not put in poison queue. | | `stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```ts await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```ts const name = `countEvents_Create_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, projection); ``` Trying to create projections with the same name will result in an error: ```ts try { await client.createProjection(name, projection); } catch (err) { if (!isCommandError(err) || !err.message.includes("Conflict")) throw err; console.log(`${name} already exists`); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```ts await client.restartSubsystem(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.enableProjection("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.enableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.disableProjection("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.disableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Delete a projection ```ts // A projection must be disabled to allow it to be deleted. await client.disableProjection(name); // The projection can now be deleted await client.deleteProjection(name); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```ts await client.abortProjection(name); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```ts try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```ts await client.resetProjection(name); ``` Resetting a projection that does not exist will result in an error. ```ts const projectionName = "projection that does not exist"; try { await client.resetProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```ts const name = `countEvents_Update_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, "fromAll().when()"); await client.updateProjection(name, projection); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.updateProjection(projectionName, "fromAll().when()"); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ::: note This is currently not available in the nodejs client ::: ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projections = await client.listProjections(); for (const { name, status, checkpointStatus, progress } of projections) { console.log(name, status, checkpointStatus, progress); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projection = await client.getProjectionStatus(name); console.log( projection.name, projection.status, projection.checkpointStatus, projection.progress ); ``` ## Get state Retrieves the state of a projection. ```ts interface CountProjectionState { count: number; } const name = `get_state_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to count event await delay(500); const state = await client.getProjectionState(name); console.log(`Counted ${state.count} events.`); ``` ## Get result Retrieves the result of the named projection and partition. ```ts const name = `get_result_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to have a result. await delay(500); const result = await client.getProjectionResult(name); console.log(`Counted ${result} events.`); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `start` to begin from the very beginning of the stream * Use `end` to begin from the current end of the stream * Use `fromRevision` with a specific revision number (64-bit signed integer) ```ts{2-3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` You can also start reading from a specific revision in the stream: ```ts{3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readStream("order-123", { direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `StreamingRead` that you can iterate over. When iterating over events from a non-existent stream, it will throw a `StreamNotFoundError` exception. It is important to handle this exception when attempting to iterate a stream that may not exist. For example: ```ts{12-14} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 20, }); try { for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } } catch (error) { if (error instanceof StreamNotFoundError) { return; } throw error; } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```ts{2-3} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` You can also start reading from a specific position in the transaction log: ```ts{3} const events = client.readAll({ direction: FORWARDS, fromPosition: 20, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readAll({ direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `BACKWARDS`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```ts{8-9} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); for await (const resolvedEvent of events) { if (resolvedEvent.event?.type.startsWith("$")) continue; console.log(resolvedEvent.event?.type); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```ts const subscription = client.subscribeToStream("some-stream"); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); // handle event } ``` **`$all` subscription:** ```ts const subscription = client.subscribeToAll(); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}`); // handle event } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```ts const subscription = client.subscribeToStream( "some-stream", { fromRevision: BigInt(20), } ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```ts const subscription = client.subscribeToAll({ fromPosition: { commit: BigInt(1056), prepare: BigInt(1056), }, }); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```ts // Stream const subscription = client.subscribeToStream("orders", { fromRevision: END }); // $all const subscription = client.subscribeToAll({ fromPosition: END }); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```ts const subscription = client.subscribeToStream( "$et-orders", { fromRevision: START, resolveLinkTos: true, } ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```ts{7,14-16} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToStream( "orders", { fromRevision: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```ts{6,13-15} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToAll( { fromPosition: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```ts{10-12} const subscription = client.subscribeToStream("orders"); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) .on("caughtUp", () => { console.log("Subscription caught up - now processing live events"); }) ``` ::: tip The `caughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```ts{4-7} const subscription = client.subscribeToStream( "orders", { credentials: { username: "admin", password: "changeit", }, } ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```ts const subscription = client.subscribeToAll({ filter: streamNameFilter({ prefixes: ["test-", "other-"] }), }); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```ts{4} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents(), }) .on("data", (resolvedEvent) => { console.log( `Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); }); ``` ### Filtering by event type **By prefix:** ```ts const filter = eventTypeFilter({ prefixes: ["customer-"], }); ``` **By regular expression:** ```ts const filter = eventTypeFilter({ regex: "^user|^company", }); ``` ### Filtering by stream name **By prefix:** ```ts const filter = streamNameFilter({ prefixes: ["user-"], }); ``` **By regular expression:** ```ts const filter = streamNameFilter({ regex: "^account|^savings", }); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents({ async checkpointReached(subscription, position) { // do something with the position console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: eventTypeFilter({ regex: "^[^$].*", checkpointInterval: 1000, checkpointReached(subscription, position) { // Save commit position to a persistent store as a checkpoint console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```ts {32-43} import { v4 as uuid } from "uuid"; const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event, { streamState: NO_STREAM, }); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```ts const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event); // attempt to append the same event again await client.appendToStream("orders", event); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. You can use existing methods `jsonEvent` or `binaryEvent` to create the `EventData` object, which will set the content type accordingly. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```ts{28-30} const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-123", paymentId: "payment-789", amount: 99.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-123-stream", orderPlacedEvent, { streamState: NO_STREAM, }); // attempt to append another event to the same stream expecting it to not exist await client.appendToStream("order-123-stream", paymentProcessedEvent, { streamState: NO_STREAM, }); ``` There are several available expected revision options: * `any` - No concurrency check * `no_stream` - Stream should not exist * `stream_exists` - Stream should exist * `bigint` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```ts const events = client.readStream("order-12345", { fromRevision: START, direction: FORWARDS, }); // Get the current revision to use for optimistic concurrency let revision: AppendStreamState = NO_STREAM; for await (const { event } of events) { revision = event?.revision ?? revision; } // Two concurrent operations trying to update the same order const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-12345", paymentId: "payment-789", amount: 149.99, paymentMethod: "credit_card" }, }); const orderCancelledEvent = jsonEvent({ id: uuid(), type: "OrderCancelled", data: { orderId: "order-12345", reason: "customer-request", comment: "Customer changed mind" }, }); // Process payment (succeeds) await client.appendToStream("order-12345", paymentProcessedEvent, { streamState: revision, }); // Cancel order (fails due to concurrency conflict) await client.appendToStream("order-12345", orderCancelledEvent, { streamState: revision, }); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```ts const credentials = { username: "admin", password: "changeit", }; await client.appendToStream("some-stream", event, { credentials, }); ``` ## Atomic appends KurrentDB provides two operations for appending events to one or more streams in a single atomic transaction: `appendRecords` and `multiStreamAppend`. Both guarantee that either all writes succeed or the entire operation fails, but they differ in how records are organized, ordered, and validated. | | `appendRecords` | `multiStreamAppend` | |---|---|---| | **Available since** | KurrentDB 26.1 | KurrentDB 25.1 | | **Record ordering** | Interleaved. Records from different streams can be mixed, and their exact order is preserved in the global log. | Grouped. All records for a stream are sent together; ordering across streams is not guaranteed. | | **Consistency checks** | Decoupled. Can validate the state of any stream, including streams not being written to. | Coupled. Expected state is specified per stream being written to. | | **Protocol** | Unary RPC. All records and checks sent in a single request. | Client-streaming RPC. Records are streamed per stream. | ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ### appendRecords ::: note This feature is only available in KurrentDB 26.1 and later. ::: `appendRecords` appends events to one or more streams atomically. Each record specifies which stream it targets, and the exact order of records is preserved in the global log across all streams. #### Single stream The simplest usage appends events to a single stream: ```ts import { jsonEvent, STREAM_STATE, NO_STREAM } from "@kurrent/kurrentdb-client"; import { v4 as uuid } from "uuid"; const records = [ { streamName: "order-123", record: jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "123", amount: 99.99 }, }), }, { streamName: "order-123", record: jsonEvent({ id: uuid(), type: "OrderShipped", data: { orderId: "123" }, }), }, ]; await client.appendRecords(records); ``` You can also pass consistency checks for optimistic concurrency: ```ts await client.appendRecords(records, [ { type: STREAM_STATE, streamName: "order-123", expectedState: NO_STREAM }, ]); ``` #### Multiple streams Records can target different streams and be interleaved freely. The global log preserves the exact order you specify: ```ts const records = [ { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderCreated", data: { orderId: "123" }, }), }, { streamName: "inventory-stream", record: jsonEvent({ id: uuid(), type: "ItemReserved", data: { itemId: "abc", quantity: 2 }, }), }, { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderConfirmed", data: { orderId: "123" }, }), }, ]; await client.appendRecords(records); ``` #### Consistency checks Consistency checks let you validate the state of any stream, including streams you are not writing to, before the append is committed. All checks are evaluated atomically: if any check fails, the entire operation is rejected and an `AppendConsistencyViolationError` is thrown with details about every failing check and the actual state observed. ```ts import { STREAM_STATE, STREAM_EXISTS } from "@kurrent/kurrentdb-client"; const records = [ { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderConfirmed", data: { orderId: "123" }, }), }, ]; const checks = [ // ensure the inventory stream exists before confirming the order, // even though we are not writing to it { type: STREAM_STATE, streamName: "inventory-stream", expectedState: STREAM_EXISTS, }, ]; await client.appendRecords(records, checks); ``` This decoupling of checks from writes enables [Dynamic Consistency Boundary](https://www.eventstore.com/blog/dynamic-consistency-boundary) patterns, where a business decision depends on the state of multiple streams but the resulting event is written to only one of them. ### multiStreamAppend ::: note This feature is only available in KurrentDB 25.1 and later. ::: `multiStreamAppend` appends events to one or more streams atomically. Records are grouped per stream using `AppendStreamRequest`, where each request specifies a stream name, an expected state, and the events for that stream. ```ts import { jsonEvent } from "@kurrent/kurrentdb-client"; import { v4 as uuid } from "uuid"; const metadata = { source: "OrderProcessingSystem", version: "1.0", }; const requests = [ { streamName: "order-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "OrderCreated", data: { orderId: "12345", amount: 99.99 }, metadata, }), ], }, { streamName: "inventory-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "ItemReserved", data: { itemId: "ABC123", quantity: 2 }, metadata, }), ], }, ]; await client.multiStreamAppend(requests); ``` Each stream can only appear once in the request. The expected state is validated per stream before the transaction is committed. --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```ts const connectionString = `kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}`; const client = KurrentDBClient.connectionString(connectionString); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```ts await client.deleteStream(streamName); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```ts await client.tombstoneStream(streamName); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Node.js application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `@kurrent/kurrentdb-client` dependency to your project: ::: tabs @tab NPM ```bash npm install --save @kurrent/kurrentdb-client@~1.1 ``` @tab Yarn ```bash yarn add @kurrent/kurrentdb-client@~1.1 ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting to a single node, or to a multi-node cluster using multiple gossip seed endpoints * **`kurrentdb+discover://`** - for connecting using DNS discovery with a single DNS endpoint When using `kurrentdb://` with multiple endpoints separated by commas, the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. If one of the nodes is down, the client will try another endpoint. With `kurrentdb+discover://`, the client resolves a single DNS endpoint to discover the cluster topology. This is useful when you have a DNS `A` record pointing to your cluster nodes. ::: warning Only one host with +discover When using `kurrentdb+discover://`, only a single host should be provided. If multiple hosts are specified, only the first one will be used for discovery and the rest will be ignored. If you need to specify multiple endpoints for redundancy, use `kurrentdb://` without `+discover` instead. ::: ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For multi-node clusters where you know the individual node addresses, use multiple gossip seed endpoints: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` The client will use the Gossip API to discover the cluster topology and select the best node based on the configured node preference. For cluster connections using DNS discovery, use a single DNS endpoint: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Timeout in seconds for each gossip request during cluster discovery. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | `10000` | Default timeout for client operations, in milliseconds. | | `keepAliveInterval` | Number | `10000` | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Number | `10000` | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```ts const client = KurrentDBClient.connectionString`kurrentdb://localhost:2113?tls=false`; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```ts type OrderCreated = JSONEventType< "OrderCreated", { orderId: string; customerId: string; items: Array<{ productId: string; productName: string; quantity: number; price: number; }>; totalAmount: number; orderDate: string; } >; const event = jsonEvent({ type: "OrderCreated", data: { orderId: uuid(), customerId: "customer-123", items: [ { productId: "product-456", productName: "Wireless Headphones", quantity: 1, price: 99.99 }, { productId: "product-789", productName: "USB Cable", quantity: 2, price: 15.99 } ], totalAmount: 131.97, orderDate: new Date().toISOString(), }, }); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```ts await client.appendToStream("order-123", event); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `order-123` stream. ```ts const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(events); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/observability.md' --- # Observability The Node.js client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project: ```bash npm install --save @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @kurrent/opentelemetry ``` For exporters, you might also want to install: ```bash npm install --save @opentelemetry/sdk-trace-node npm install --save @opentelemetry/exporter-trace-otlp-http npm install --save @opentelemetry/semantic-conventions ``` ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```ts const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { KurrentDBInstrumentation } = require('@eventstore/opentelemetry'); const provider = new NodeTracerProvider(); provider.register(); registerInstrumentations({ instrumentations: [ new KurrentDBInstrumentation(), ], }); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=js). You can configure multiple exporters simultaneously: ```ts import { InMemorySpanExporter, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const memoryExporter = new InMemorySpanExporter(); const otlpExporter = new OTLPTraceExporter({ url: "http://localhost:4317" }); // change this to your OTLP receiver address const consoleExporter = new ConsoleSpanExporter(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); ``` For detailed configuration options, refer to the [OpenTelemetry Node.js documentation](https://opentelemetry.io/docs/languages/js/). ## Understanding Traces ### What Gets Traced The Node.js client automatically creates traces for append and subscription operations when the KurrentDB instrumentation is registered. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | ------------------------------ | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `kurrentdb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | | `db.kurrentdb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.kurrentdb.event.id` | Event identifier | `event-456` | | `db.kurrentdb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace | | --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults() ); ``` ### Subscribing to `$all` ```ts import { persistentSubscriptionToAllSettingsFromDefaults, streamNameFilter } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToAll( "group-name", persistentSubscriptionToAllSettingsFromDefaults(), { filter: streamNameFilter({ prefixes: ["test"] }) } ); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```ts const subscription = client.subscribeToPersistentSubscriptionToAll("group-name"); try { for await (const event of subscription) { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The `subscribeToPersistentSubscriptionToAll` method is identical to the `subscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log( `handling event ${event.event?.type} with retryCount ${event.retryCount}` ); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `park` | Park the message and do not resend. Put it on poison queue. | | `retry` | Explicitly retry the message. | | `skip` | Skip this message do not resend and do not put in poison queue. | | `stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```ts await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```ts const name = `countEvents_Create_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, projection); ``` Trying to create projections with the same name will result in an error: ```ts try { await client.createProjection(name, projection); } catch (err) { if (!isCommandError(err) || !err.message.includes("Conflict")) throw err; console.log(`${name} already exists`); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```ts await client.restartSubsystem(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.enableProjection("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.enableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.disableProjection("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.disableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Delete a projection ```ts // A projection must be disabled to allow it to be deleted. await client.disableProjection(name); // The projection can now be deleted await client.deleteProjection(name); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```ts await client.abortProjection(name); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```ts try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```ts await client.resetProjection(name); ``` Resetting a projection that does not exist will result in an error. ```ts const projectionName = "projection that does not exist"; try { await client.resetProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```ts const name = `countEvents_Update_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, "fromAll().when()"); await client.updateProjection(name, projection); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.updateProjection(projectionName, "fromAll().when()"); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ::: note This is currently not available in the nodejs client ::: ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projections = await client.listProjections(); for (const { name, status, checkpointStatus, progress } of projections) { console.log(name, status, checkpointStatus, progress); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projection = await client.getProjectionStatus(name); console.log( projection.name, projection.status, projection.checkpointStatus, projection.progress ); ``` ## Get state Retrieves the state of a projection. ```ts interface CountProjectionState { count: number; } const name = `get_state_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to count event await delay(500); const state = await client.getProjectionState(name); console.log(`Counted ${state.count} events.`); ``` ## Get result Retrieves the result of the named projection and partition. ```ts const name = `get_result_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to have a result. await delay(500); const result = await client.getProjectionResult(name); console.log(`Counted ${result} events.`); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `start` to begin from the very beginning of the stream * Use `end` to begin from the current end of the stream * Use `fromRevision` with a specific revision number (64-bit signed integer) ```ts{2-3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` You can also start reading from a specific revision in the stream: ```ts{3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readStream("order-123", { direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `StreamingRead` that you can iterate over. When iterating over events from a non-existent stream, it will throw a `StreamNotFoundError` exception. It is important to handle this exception when attempting to iterate a stream that may not exist. For example: ```ts{12-14} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 20, }); try { for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } } catch (error) { if (error instanceof StreamNotFoundError) { return; } throw error; } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```ts{2-3} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` You can also start reading from a specific position in the transaction log: ```ts{3} const events = client.readAll({ direction: FORWARDS, fromPosition: 20, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readAll({ direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `BACKWARDS`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```ts{8-9} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); for await (const resolvedEvent of events) { if (resolvedEvent.event?.type.startsWith("$")) continue; console.log(resolvedEvent.event?.type); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```ts const subscription = client.subscribeToStream("some-stream"); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); // handle event } ``` **`$all` subscription:** ```ts const subscription = client.subscribeToAll(); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}`); // handle event } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```ts const subscription = client.subscribeToStream( "some-stream", { fromRevision: BigInt(20), } ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```ts const subscription = client.subscribeToAll({ fromPosition: { commit: BigInt(1056), prepare: BigInt(1056), }, }); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```ts // Stream const subscription = client.subscribeToStream("orders", { fromRevision: END }); // $all const subscription = client.subscribeToAll({ fromPosition: END }); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```ts const subscription = client.subscribeToStream( "$et-orders", { fromRevision: START, resolveLinkTos: true, } ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```ts{7,14-16} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToStream( "orders", { fromRevision: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```ts{6,13-15} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToAll( { fromPosition: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```ts{10-12} const subscription = client.subscribeToStream("orders"); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) .on("caughtUp", () => { console.log("Subscription caught up - now processing live events"); }) ``` ::: tip The `caughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```ts{4-7} const subscription = client.subscribeToStream( "orders", { credentials: { username: "admin", password: "changeit", }, } ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```ts const subscription = client.subscribeToAll({ filter: streamNameFilter({ prefixes: ["test-", "other-"] }), }); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```ts{4} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents(), }) .on("data", (resolvedEvent) => { console.log( `Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); }); ``` ### Filtering by event type **By prefix:** ```ts const filter = eventTypeFilter({ prefixes: ["customer-"], }); ``` **By regular expression:** ```ts const filter = eventTypeFilter({ regex: "^user|^company", }); ``` ### Filtering by stream name **By prefix:** ```ts const filter = streamNameFilter({ prefixes: ["user-"], }); ``` **By regular expression:** ```ts const filter = streamNameFilter({ regex: "^account|^savings", }); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents({ async checkpointReached(subscription, position) { // do something with the position console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: eventTypeFilter({ regex: "^[^$].*", checkpointInterval: 1000, checkpointReached(subscription, position) { // Save commit position to a persistent store as a checkpoint console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `appendToStream` method. ```ts {32-43} import { v4 as uuid } from "uuid"; const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event, { streamState: NO_STREAM, }); ``` `appendToStream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### eventId This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```ts const event = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); await client.appendToStream("orders", event); // attempt to append the same event again await client.appendToStream("orders", event); ``` ### eventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### eventData Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### userMetadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### contentType The content type indicates whether the event is stored as JSON or binary format. You can use existing methods `jsonEvent` or `binaryEvent` to create the `EventData` object, which will set the content type accordingly. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```ts{28-30} const orderPlacedEvent = jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "order-123", customerId: "customer-456", totalAmount: 99.99, status: "placed" }, }); const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-123", paymentId: "payment-789", amount: 99.99, paymentMethod: "credit_card" }, }); await client.appendToStream("order-123-stream", orderPlacedEvent, { streamState: NO_STREAM, }); // attempt to append another event to the same stream expecting it to not exist await client.appendToStream("order-123-stream", paymentProcessedEvent, { streamState: NO_STREAM, }); ``` There are several available expected revision options: * `any` - No concurrency check * `no_stream` - Stream should not exist * `stream_exists` - Stream should exist * `bigint` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```ts const events = client.readStream("order-12345", { fromRevision: START, direction: FORWARDS, }); // Get the current revision to use for optimistic concurrency let revision: AppendStreamState = NO_STREAM; for await (const { event } of events) { revision = event?.revision ?? revision; } // Two concurrent operations trying to update the same order const paymentProcessedEvent = jsonEvent({ id: uuid(), type: "PaymentProcessed", data: { orderId: "order-12345", paymentId: "payment-789", amount: 149.99, paymentMethod: "credit_card" }, }); const orderCancelledEvent = jsonEvent({ id: uuid(), type: "OrderCancelled", data: { orderId: "order-12345", reason: "customer-request", comment: "Customer changed mind" }, }); // Process payment (succeeds) await client.appendToStream("order-12345", paymentProcessedEvent, { streamState: revision, }); // Cancel order (fails due to concurrency conflict) await client.appendToStream("order-12345", orderCancelledEvent, { streamState: revision, }); ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```ts const credentials = { username: "admin", password: "changeit", }; await client.appendToStream("some-stream", event, { credentials, }); ``` ## Atomic appends KurrentDB provides two operations for appending events to one or more streams in a single atomic transaction: `appendRecords` and `multiStreamAppend`. Both guarantee that either all writes succeed or the entire operation fails, but they differ in how records are organized, ordered, and validated. | | `appendRecords` | `multiStreamAppend` | |---|---|---| | **Available since** | KurrentDB 26.1 | KurrentDB 25.1 | | **Record ordering** | Interleaved. Records from different streams can be mixed, and their exact order is preserved in the global log. | Grouped. All records for a stream are sent together; ordering across streams is not guaranteed. | | **Consistency checks** | Decoupled. Can validate the state of any stream, including streams not being written to. | Coupled. Expected state is specified per stream being written to. | | **Protocol** | Unary RPC. All records and checks sent in a single request. | Client-streaming RPC. Records are streamed per stream. | ::: warning Metadata must be a valid JSON object, using string keys and string values only. Binary metadata is not supported in this version to maintain compatibility with KurrentDB's metadata handling. This restriction will be lifted in the next major release. ::: ### appendRecords ::: note This feature is only available in KurrentDB 26.1 and later. ::: `appendRecords` appends events to one or more streams atomically. Each record specifies which stream it targets, and the exact order of records is preserved in the global log across all streams. #### Single stream The simplest usage appends events to a single stream: ```ts import { jsonEvent, STREAM_STATE, NO_STREAM } from "@kurrent/kurrentdb-client"; import { v4 as uuid } from "uuid"; const records = [ { streamName: "order-123", record: jsonEvent({ id: uuid(), type: "OrderPlaced", data: { orderId: "123", amount: 99.99 }, }), }, { streamName: "order-123", record: jsonEvent({ id: uuid(), type: "OrderShipped", data: { orderId: "123" }, }), }, ]; await client.appendRecords(records); ``` You can also pass consistency checks for optimistic concurrency: ```ts await client.appendRecords(records, [ { type: STREAM_STATE, streamName: "order-123", expectedState: NO_STREAM }, ]); ``` #### Multiple streams Records can target different streams and be interleaved freely. The global log preserves the exact order you specify: ```ts const records = [ { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderCreated", data: { orderId: "123" }, }), }, { streamName: "inventory-stream", record: jsonEvent({ id: uuid(), type: "ItemReserved", data: { itemId: "abc", quantity: 2 }, }), }, { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderConfirmed", data: { orderId: "123" }, }), }, ]; await client.appendRecords(records); ``` #### Consistency checks Consistency checks let you validate the state of any stream, including streams you are not writing to, before the append is committed. All checks are evaluated atomically: if any check fails, the entire operation is rejected and an `AppendConsistencyViolationError` is thrown with details about every failing check and the actual state observed. ```ts import { STREAM_STATE, STREAM_EXISTS } from "@kurrent/kurrentdb-client"; const records = [ { streamName: "order-stream", record: jsonEvent({ id: uuid(), type: "OrderConfirmed", data: { orderId: "123" }, }), }, ]; const checks = [ // ensure the inventory stream exists before confirming the order, // even though we are not writing to it { type: STREAM_STATE, streamName: "inventory-stream", expectedState: STREAM_EXISTS, }, ]; await client.appendRecords(records, checks); ``` This decoupling of checks from writes enables [Dynamic Consistency Boundary](https://www.eventstore.com/blog/dynamic-consistency-boundary) patterns, where a business decision depends on the state of multiple streams but the resulting event is written to only one of them. ### multiStreamAppend ::: note This feature is only available in KurrentDB 25.1 and later. ::: `multiStreamAppend` appends events to one or more streams atomically. Records are grouped per stream using `AppendStreamRequest`, where each request specifies a stream name, an expected state, and the events for that stream. ```ts import { jsonEvent } from "@kurrent/kurrentdb-client"; import { v4 as uuid } from "uuid"; const metadata = { source: "OrderProcessingSystem", version: "1.0", }; const requests = [ { streamName: "order-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "OrderCreated", data: { orderId: "12345", amount: 99.99 }, metadata, }), ], }, { streamName: "inventory-stream-1", expectedState: "any", events: [ jsonEvent({ id: uuid(), type: "ItemReserved", data: { itemId: "ABC123", quantity: 2 }, metadata, }), ], }, ]; await client.multiStreamAppend(requests); ``` Each stream can only appear once in the request. The expected state is validated per stream before the transaction is committed. --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```ts const connectionString = `kurrentdb://admin:changeit@{endpoint}?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}`; const client = KurrentDBClient.connectionString(connectionString); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```ts await client.deleteStream(streamName); ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```ts await client.tombstoneStream(streamName); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Node.js application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the `@kurrent/kurrentdb-client` dependency to your project: ::: tabs @tab NPM ```bash npm install --save @kurrent/kurrentdb-client@~1.1 ``` @tab Yarn ```bash yarn add @kurrent/kurrentdb-client@~1.1 ``` ::: ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting to a single node, or to a multi-node cluster using multiple gossip seed endpoints * **`kurrentdb+discover://`** - for connecting using DNS discovery with a single DNS endpoint When using `kurrentdb://` with multiple endpoints separated by commas, the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. If one of the nodes is down, the client will try another endpoint. With `kurrentdb+discover://`, the client resolves a single DNS endpoint to discover the cluster topology. This is useful when you have a DNS `A` record pointing to your cluster nodes. ::: warning Only one host with +discover When using `kurrentdb+discover://`, only a single host should be provided. If multiple hosts are specified, only the first one will be used for discovery and the rest will be ignored. If you need to specify multiple endpoints for redundancy, use `kurrentdb://` without `+discover` instead. ::: ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For multi-node clusters where you know the individual node addresses, use multiple gossip seed endpoints: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` The client will use the Gossip API to discover the cluster topology and select the best node based on the configured node preference. For cluster connections using DNS discovery, use a single DNS endpoint: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Timeout in seconds for each gossip request during cluster discovery. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `false` when connecting to an untrusted node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | `10000` | Default timeout for client operations, in milliseconds. | | `keepAliveInterval` | Number | `10000` | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Number | `10000` | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```ts const client = KurrentDBClient.connectionString`kurrentdb://localhost:2113?tls=false`; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```ts type OrderCreated = JSONEventType< "OrderCreated", { orderId: string; customerId: string; items: Array<{ productId: string; productName: string; quantity: number; price: number; }>; totalAmount: number; orderDate: string; } >; const event = jsonEvent({ type: "OrderCreated", data: { orderId: uuid(), customerId: "customer-123", items: [ { productId: "product-456", productName: "Wireless Headphones", quantity: 1, price: 99.99 }, { productId: "product-789", productName: "USB Cable", quantity: 2, price: 15.99 } ], totalAmount: 131.97, orderDate: new Date().toISOString(), }, }); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `order-123`. ```ts await client.appendToStream("order-123", event); ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the `order-123` stream. ```ts const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(events); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/observability.md' --- # Observability The Node.js client provides observability capabilities through OpenTelemetry integration. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support. ## Prerequisites You'll need to add OpenTelemetry dependencies to your project: ```bash npm install --save @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @kurrent/opentelemetry ``` For exporters, you might also want to install: ```bash npm install --save @opentelemetry/sdk-trace-node npm install --save @opentelemetry/exporter-trace-otlp-http npm install --save @opentelemetry/semantic-conventions ``` ## Basic Configuration Configure OpenTelemetry by creating and registering the SDK with appropriate exporters. Here's a minimal setup: ```ts const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { KurrentDBInstrumentation } = require('@eventstore/opentelemetry'); const provider = new NodeTracerProvider(); provider.register(); registerInstrumentations({ instrumentations: [ new KurrentDBInstrumentation(), ], }); ``` ## Trace Exporters OpenTelemetry supports various exporters to send trace data to different observability platforms. You can find a list of available exporters in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=js). You can configure multiple exporters simultaneously: ```ts import { InMemorySpanExporter, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; const memoryExporter = new InMemorySpanExporter(); const otlpExporter = new OTLPTraceExporter({ url: "http://localhost:4317" }); // change this to your OTLP receiver address const consoleExporter = new ConsoleSpanExporter(); provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(consoleExporter)); provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter)); ``` For detailed configuration options, refer to the [OpenTelemetry Node.js documentation](https://opentelemetry.io/docs/languages/js/). ## Understanding Traces ### What Gets Traced The Node.js client automatically creates traces for append and subscription operations when the KurrentDB instrumentation is registered. ### Trace Attributes Each trace includes metadata to help with debugging and monitoring: | Attribute | Description | Example | | ------------------------------ | -------------------------------------- | ------------------------------------- | | `db.user` | Database user name | `admin` | | `db.system` | Database system identifier | `kurrentdb` | | `db.operation` | Type of operation performed | `streams.append`, `streams.subscribe` | | `db.kurrentdb.stream` | Stream name or identifier | `user-events-123` | | `db.kurrentdb.subscription.id` | Subscription identifier | `user-events-123-sub` | | `db.kurrentdb.event.id` | Event identifier | `event-456` | | `db.kurrentdb.event.type` | Event type identifier | `user.created` | | `server.address` | KurrentDB server address | `localhost` | | `server.port` | KurrentDB server port | `2113` | | `exception.type` | Exception type if an error occurred | | | `exception.message` | Exception message if an error occurred | | | `exception.stacktrace` | Stack trace | | --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults() ); ``` ### Subscribing to `$all` ```ts import { persistentSubscriptionToAllSettingsFromDefaults, streamNameFilter } from "@kurrent/kurrentdb-client"; await client.createPersistentSubscriptionToAll( "group-name", persistentSubscriptionToAllSettingsFromDefaults(), { filter: streamNameFilter({ prefixes: ["test"] }) } ); ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```ts const subscription = client.subscribeToPersistentSubscriptionToAll("group-name"); try { for await (const event of subscription) { console.log(`handling event ${event.event?.type} with retryCount ${event.retryCount}`); // handle event await subscription.ack(event); } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The `subscribeToPersistentSubscriptionToAll` method is identical to the `subscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```ts import { PARK } from "@kurrent/kurrentdb-client"; const subscription = client.subscribeToPersistentSubscriptionToStream("stream-name", "group-name"); try { for await (const event of subscription) { try { console.log( `handling event ${event.event?.type} with retryCount ${event.retryCount}` ); // handle event await subscription.ack(event); } catch (error) { await subscription.nack(PARK, error.toString(), event); } } } catch (error) { console.log(`Subscription was dropped. ${error}`); } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `park` | Park the message and do not resend. Put it on poison queue. | | `retry` | Explicitly retry the message. | | `skip` | Skip this message do not resend and do not put in poison queue. | | `stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```ts import { persistentSubscriptionToStreamSettingsFromDefaults } from "@kurrent/kurrentdb-client"; await client.updatePersistentSubscriptionToStream( "stream-name", "group-name", persistentSubscriptionToStreamSettingsFromDefaults({ resolveLinkTos: true, checkPointLowerBound: 20, }) ); ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```ts await client.deletePersistentSubscriptionToStream("stream-name", "subscription-group"); ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/projections.md' --- # Projection management The client provides a way to manage projections in KurrentDB. For a detailed explanation of projections, see the [server documentation](@server/features/projections/README.md). ## Create a projection Creates a projection that runs until the last event in the store, and then continues processing new events as they are appended to the store. The query parameter contains the JavaScript you want created as a projection. Projections have explicit names, and you can enable or disable them via this name. ```ts const name = `countEvents_Create_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, projection); ``` Trying to create projections with the same name will result in an error: ```ts try { await client.createProjection(name, projection); } catch (err) { if (!isCommandError(err) || !err.message.includes("Conflict")) throw err; console.log(`${name} already exists`); } ``` ## Restart the subsystem It is possible to restart the entire projection subsystem using the projections management client API. The user must be in the `$ops` or `$admin` group to perform this operation. ```ts await client.restartSubsystem(); ``` ## Enable a projection This Enables an existing projection by name. Once enabled, the projection will start to process events even after restarting the server or the projection subsystem. You must have access to a projection to enable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.enableProjection("$by_category"); ``` You can only enable an existing projection. When you try to enable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.enableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Disable a projection Disables a projection, this will save the projection checkpoint. Once disabled, the projection will not process events even after restarting the server or the projection subsystem. You must have access to a projection to disable it, see the [ACL documentation](@server/security/user-authorization.md). ```ts await client.disableProjection("$by_category"); ``` You can only disable an existing projection. When you try to disable a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.disableProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Delete a projection ```ts // A projection must be disabled to allow it to be deleted. await client.disableProjection(name); // The projection can now be deleted await client.deleteProjection(name); ``` ## Abort a projection Aborts a projection, this will not save the projection's checkpoint. ```ts await client.abortProjection(name); ``` You can only abort an existing projection. When you try to abort a non-existing projection, you'll get an error: ```ts try { client.abort("projection that does not exists").get(); } catch (ExecutionException ex) { if (ex.getMessage().contains("NotFound")) { System.out.println(ex.getMessage()); } } ``` ## Reset a projection Resets a projection, which causes deleting the projection checkpoint. This will force the projection to start afresh and re-emit events. Streams that are written to from the projection will also be soft-deleted. ```ts await client.resetProjection(name); ``` Resetting a projection that does not exist will result in an error. ```ts const projectionName = "projection that does not exist"; try { await client.resetProjection(projectionName); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## Update a projection Updates a projection with a given name. The query parameter contains the new JavaScript. Updating system projections using this operation is not supported at the moment. ```ts const name = `countEvents_Update_${uuid()}`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .outputState()`; await client.createProjection(name, "fromAll().when()"); await client.updateProjection(name, projection); ``` You can only update an existing projection. When you try to update a non-existing projection, you'll get an error: ```ts const projectionName = "projection that does not exist"; try { await client.updateProjection(projectionName, "fromAll().when()"); } catch (err) { if (!isCommandError(err) || !err.message.includes("NotFound")) throw err; console.log(`${projectionName} does not exist`); } ``` ## List all projections Returns a list of all projections, user defined & system projections. See the [projection details](#projection-details) section for an explanation of the returned values. ::: note This is currently not available in the nodejs client ::: ## List continuous projections Returns a list of all continuous projections. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projections = await client.listProjections(); for (const { name, status, checkpointStatus, progress } of projections) { console.log(name, status, checkpointStatus, progress); } ``` ## Get status Gets the status of a named projection. See the [projection details](#projection-details) section for an explanation of the returned values. ```ts const projection = await client.getProjectionStatus(name); console.log( projection.name, projection.status, projection.checkpointStatus, projection.progress ); ``` ## Get state Retrieves the state of a projection. ```ts interface CountProjectionState { count: number; } const name = `get_state_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to count event await delay(500); const state = await client.getProjectionState(name); console.log(`Counted ${state.count} events.`); ``` ## Get result Retrieves the result of the named projection and partition. ```ts const name = `get_result_example`; const projection = ` fromAll() .when({ $init() { return { count: 0, }; }, $any(s, e) { s.count += 1; } }) .transformBy((state) => state.count) .outputState()`; await client.createProjection(name, projection); // Give it some time to have a result. await delay(500); const result = await client.getProjectionResult(name); console.log(`Counted ${result} events.`); ``` ## Projection Details The `list`, and `getStatus` methods return detailed statistics and information about projections. Below is an explanation of the fields included in the projection details: | Field | Description | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name`, `effectiveName` | The name of the projection | | `status` | A human readable string of the current statuses of the projection (see below) | | `stateReason` | A human readable string explaining the reason of the current projection state | | `checkpointStatus` | A human readable string explaining the current operation performed on the checkpoint : `requested`, `writing` | | `mode` | `Continuous`, `OneTime` , `Transient` | | `coreProcessingTime` | The total time, in ms, the projection took to handle events since the last restart | | `progress` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed | | `writesInProgress` | The number of write requests to emitted streams currently in progress, these writes can be batches of events | | `readsInProgress` | The number of read requests currently in progress | | `partitionsCached` | The number of cached projection partitions | | `position` | The Position of the last processed event | | `lastCheckpoint` | The Position of the last checkpoint of this projection | | `eventsProcessedAfterRestart` | The number of events processed since the last restart of this projection | | `bufferedEvents` | The number of events in the projection read buffer | | `writePendingEventsBeforeCheckpoint` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written | | `writePendingEventsAfterCheckpoint` | The number of events to be appended to emitted streams since the last checkpoint | | `version` | This is used internally, the version is increased when the projection is edited or reset | | `epoch` | This is used internally, the epoch is increased when the projection is reset | The `status` string is a combination of the following values. The first 3 are the most common one, as the other one are transient values while the projection is initialised or stopped | Value | Description | |--------------------|-------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events | | Stopped | The projection is stopped and is no longer processing new events | | Faulted | An error occurred in the projection, `StateReason` will give the fault details, the projection is not processing events | | Initial | This is the initial state, before the projection is fully initialised | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting | | FaultedStopping | This happens before the projection is stopped due to an error in the projection | | Stopping | The projection is being stopped | | CompletingPhase | This happens while the projection is stopping | | PhaseCompleted | This happens while the projection is stopping | --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `start` to begin from the very beginning of the stream * Use `end` to begin from the current end of the stream * Use `fromRevision` with a specific revision number (64-bit signed integer) ```ts{2-3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` You can also start reading from a specific revision in the stream: ```ts{3} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readStream("order-123", { direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `StreamingRead` that you can iterate over. When iterating over events from a non-existent stream, it will throw a `StreamNotFoundError` exception. It is important to handle this exception when attempting to iterate a stream that may not exist. For example: ```ts{12-14} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: 10, maxCount: 20, }); try { for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } } catch (error) { if (error instanceof StreamNotFoundError) { return; } throw error; } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```ts{2-3} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` You can also start reading from a specific position in the transaction log: ```ts{3} const events = client.readAll({ direction: FORWARDS, fromPosition: 20, maxCount: 10, }); ``` You can then iterate synchronously through the result: ```ts for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. ```ts{4} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```ts{4} const events = client.readAll({ direction: BACKWARDS, fromPosition: END, resolveLinkTos: true, maxCount: 10, }); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```ts{4-7} const events = client.readStream("order-123", { direction: FORWARDS, fromRevision: START, credentials: { username: "admin", password: "changeit", }, maxCount: 10, }); ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `BACKWARDS`: ```ts{2-3} const events = client.readStream("order-123", { direction: BACKWARDS, fromRevision: END, maxCount: 10, }); for await (const resolvedEvent of events) { console.log(resolvedEvent.event?.data); } ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `eventType` property. ```ts{8-9} const events = client.readAll({ direction: FORWARDS, fromPosition: START, maxCount: 10, }); for await (const resolvedEvent of events) { if (resolvedEvent.event?.type.startsWith("$")) continue; console.log(resolvedEvent.event?.type); } ``` --- --- url: 'https://docs.kurrent.io/clients/node/v1.3/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```ts const subscription = client.subscribeToStream("some-stream"); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); // handle event } ``` **`$all` subscription:** ```ts const subscription = client.subscribeToAll(); for await (const resolvedEvent of subscription) { console.log(`Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}`); // handle event } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolveLinkTos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`fromStart()`, `fromEnd()` or a 64-bit signed integer representing the revision number): ```ts const subscription = client.subscribeToStream( "some-stream", { fromRevision: BigInt(20), } ); ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```ts const subscription = client.subscribeToAll({ fromPosition: { commit: BigInt(1056), prepare: BigInt(1056), }, }); ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```ts // Stream const subscription = client.subscribeToStream("orders", { fromRevision: END }); // $all const subscription = client.subscribeToAll({ fromPosition: END }); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolveLinkTos` parameter to `true`: ```ts const subscription = client.subscribeToStream( "$et-orders", { fromRevision: START, resolveLinkTos: true, } ); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. As you rarely would want to reprocess all the events again, you'd need to store the current position of the subscription somewhere, and then use it to restore the subscription from the point where it dropped off: ```ts{7,14-16} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToStream( "orders", { fromRevision: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` When subscribed to `$all` you want to keep the event's position in the `$all` stream. As mentioned previously, the `$all` stream position consists of two big integers (prepare and commit positions), not one: ```ts{6,13-15} import { ReadRevision, START } from "@kurrent/kurrentdb-client"; let checkpoint: ReadRevision = START; const subscription = client.subscribeToAll( { fromPosition: checkpoint } ); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) ``` ## Handling Subscription State Changes ::: info KurrentDB 23.10.0+ This feature requires KurrentDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```ts{10-12} const subscription = client.subscribeToStream("orders"); subscription .on("data", (resolvedEvent) => { // Handle the event }) .on("error", (error) => { console.error("Subscription error:", error); }) .on("caughtUp", () => { console.log("Subscription caught up - now processing live events"); }) ``` ::: tip The `caughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```ts{4-7} const subscription = client.subscribeToStream( "orders", { credentials: { username: "admin", password: "changeit", }, } ); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```ts const subscription = client.subscribeToAll({ filter: streamNameFilter({ prefixes: ["test-", "other-"] }), }); ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```ts{4} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents(), }) .on("data", (resolvedEvent) => { console.log( `Received event ${resolvedEvent.event?.revision}@${resolvedEvent.event?.streamId}` ); }); ``` ### Filtering by event type **By prefix:** ```ts const filter = eventTypeFilter({ prefixes: ["customer-"], }); ``` **By regular expression:** ```ts const filter = eventTypeFilter({ regex: "^user|^company", }); ``` ### Filtering by stream name **By prefix:** ```ts const filter = streamNameFilter({ prefixes: ["user-"], }); ``` **By regular expression:** ```ts const filter = streamNameFilter({ regex: "^account|^savings", }); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: excludeSystemEvents({ async checkpointReached(subscription, position) { // do something with the position console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```ts{4-11} const subscription = client .subscribeToAll({ fromPosition: START, filter: eventTypeFilter({ regex: "^[^$].*", checkpointInterval: 1000, checkpointReached(subscription, position) { // Save commit position to a persistent store as a checkpoint console.log(`checkpoint taken at ${position.commit}`); }, }); }) ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create a `NewEvent` object and call the `append_to_stream()` method. ```python import uuid from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Construct a new event object event = NewEvent( type='OrderCreated', data=b'{"order_id": "' + str(uuid.uuid4()).encode() + b'"}', id=uuid.uuid4() ) # Append the event to a stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event] ) ``` The `append_to_stream()` method takes a sequence of new event objects that can contain JSON or binary data, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with NewEvent Events appended to KurrentDB must be wrapped in a `NewEvent` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need two required arguments: **type** and **data**, and three optional arguments: **metadata**, **content\_type**, and **id**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```python import uuid from kurrentdbclient import NewEvent, StreamState # Create an event with a specific ID event_id = uuid.uuid4() event = NewEvent( type='OrderCreated', data=b'{"order_id": "1"}', id=event_id ) # Append the event client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event] ) # Attempt to append the same event again - this will be idempotent client.append_to_stream( stream_name="order-123", current_version=0, events=[event] ) ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is not part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `'application/json'` (default) and `'application/octet-stream'` when creating your `NewEvent` object. ## Handling concurrency When appending events to a stream, you can supply a *current version*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, a `WrongCurrentVersionError` exception will be raised. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```python # First append - stream doesn't exist yet event1 = NewEvent( type='OrderCreated', data=b'{"order_id": "1"}', id=uuid.uuid4() ) client.append_to_stream( stream_name="order-456", current_version=StreamState.NO_STREAM, events=[event1] ) # Second append - this will raise WrongCurrentVersionError # because the stream now exists event2 = NewEvent( type='OrderCreated', data=b'{"order_id": "2"}', id=uuid.uuid4() ) try: client.append_to_stream( stream_name="order-456", current_version=StreamState.NO_STREAM, # Stream exists now! events=[event2] ) except WrongCurrentVersionError: print("Stream already exists!") ``` There are several available expected version options: * `StreamState.ANY` - No concurrency check * `StreamState.NO_STREAM` - Stream should not exist * `StreamState.EXISTS` - Stream should exist * Integer value - Stream should be at specific version This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```python # Get the current state of the stream recorded_events = client.get_stream("order-789") current_version = len(recorded_events) - 1 if recorded_events else StreamState.NO_STREAM # Update the order event = NewEvent( type='OrderUpdated', data=b'{"order_id": "1", "status": "processing"}', id=uuid.uuid4() ) # Append with concurrency control client.append_to_stream( stream_name="order-789", current_version=current_version, events=[event] ) # Try to append another event with the same current_version # This will fail if someone else has written to the stream event2 = NewEvent( type='OrderUpdated', data=b'{"order_id": "2", "status": "shipped"}', id=uuid.uuid4() ) try: client.append_to_stream( stream_name="order-789", current_version=current_version, # This might be stale now events=[event2] ) except WrongCurrentVersionError: print("Someone else modified the stream, need to retry") ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```python from kurrentdbclient import KurrentDBClient # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit" ) # Use credentials for this specific operation commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event], credentials=credentials ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs client = KurrentDBClient(uri="kurrentdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}") ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```python from kurrentdbclient import KurrentDBClient # Get the current version of the stream current_version = client.get_current_version(stream_name="some-stream") # Soft delete the stream commit_position = client.delete_stream( stream_name="some-stream", current_version=current_version ) ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```python from kurrentdbclient import KurrentDBClient # Get the current version of the stream current_version = client.get_current_version(stream_name="some-stream") # Hard delete (tombstone) the stream commit_position = client.tombstone_stream( stream_name="some-stream", current_version=current_version ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Java application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following package to your virtual environment: ```bash pip install kurrentdbclient ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since EventStoreDB 22.10, the database supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```py client = KurrentDBClient(uri="kurrentdb://admin:changeit@localhost:2113?tls=false") ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event This package defines a `NewEvent` class and a `RecordedEvent` class. The `NewEvent` class should be used when writing events to the database. The `RecordedEvent` class is used when reading events from the database. ```py new_event = NewEvent( type='OrderCreated', data=b'{"name": "Greg"}', ) ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```py import uuid from kurrentdbclient import NewEvent, StreamState # Define a new stream name. stream_name = str(uuid.uuid4()) # Append the new events to the new stream. commit_position = client.append_to_stream( stream_name=stream_name, current_version=StreamState.NO_STREAM, events=[event1], ) ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the stream. ```py events = client.get_stream( stream_name=stream_name ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to all events, which includes all events in the database. ### Subscribing to a Specific Stream ```python from kurrentdbclient import KurrentDBClient # Create a persistent subscription to a specific stream client.create_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) ``` ### Subscribing to All Events ```python # Create a persistent subscription to all events with filtering client.create_subscription_to_all( group_name="subscription-group", filter_include=("test.*",), filter_by_stream_name=True ) ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to all events supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for all events similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The client will automatically manage the buffer size and flow control to ensure optimal performance. The server distributes events to consumers based on the configured consumer strategy. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```python # Connect to a persistent subscription for a specific stream subscription = client.read_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") ``` ### Connecting to all events The code below shows how to connect to an existing subscription group for all events: ```python # Connect to a persistent subscription for all events subscription = client.read_subscription_to_all( group_name="subscription-group" ) # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") ``` The `read_subscription_to_all()` method is identical to the `read_subscription_to_stream()` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```python # Connect to persistent subscription subscription = client.read_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) # Process events with proper acknowledgement for event in subscription: try: # Process the event process_event(event) # Acknowledge successful processing subscription.ack(event) except ProcessingError as e: # Handle processing failure print(f"Processing failed: {e}") subscription.nack(event, action="retry") except CriticalError as e: # Handle critical failure print(f"Critical error: {e}") subscription.nack(event, action="park") ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `"park"` | Park the message and do not resend. Put it on poison queue. | | `"retry"` | Explicitly retry the message. | | `"skip"` | Skip this message do not resend and do not put in poison queue. | | `"stop"` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system by-category projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```python # Update a persistent subscription to a stream client.update_subscription_to_stream( group_name="subscription-group", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) # Update a persistent subscription to all events client.update_subscription_to_all( group_name="subscription-group", resolve_links=True, min_checkpoint_count=20 ) ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `from_end` | Whether to start the subscription from the end of the stream. | `False` (start from the beginning) | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `message_timeout` | The amount of time after which to consider a message as timed out and retried. | `30.0` (seconds) | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `max_subscriber_count` | The maximum number of subscribers allowed. | `0` (unbounded) | | `consumer_strategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `"RoundRobin"` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```python # Delete a persistent subscription to a stream client.delete_subscription( group_name="subscription-group", stream_name="order-123" ) # Delete a persistent subscription to all events client.delete_subscription( group_name="subscription-group" ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from all events to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **stream position** is an integer that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential position (0, 1, 2, 3...). The **commit position** represents the event's location in KurrentDB's global transaction log and is a single integer value that indicates where the event was committed in the log. These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from all events](#reading-from-all-events) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name and optionally specify a stream position from which to start. You can use the `get_stream()` method for simple cases or `read_stream()` for streaming large result sets. ```python from kurrentdbclient import KurrentDBClient # Read all events from a stream events = client.get_stream(stream_name="some-stream") # Iterate through the events for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` You can also start reading from a specific position in the stream: ```python # Read from a specific stream position events = client.get_stream( stream_name="some-stream", stream_position=10, limit=20 ) # Or use read_stream for streaming read_response = client.read_stream( stream_name="some-stream", stream_position=10, limit=20 ) # Iterate through the streaming response for event in read_response: print(f"Event: {event.type}") ``` There are a number of additional arguments you can provide when reading a stream. #### limit Passing in the limit allows you to restrict the number of events that are returned. In the example below, we read a maximum of 20 events from the stream: ```python events = client.get_stream( stream_name="some-stream", stream_position=10, limit=20 ) ``` #### resolve\_links When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to `True` will tell KurrentDB to return the event as well as the event linking to it. ```python events = client.get_stream( stream_name="some-stream", resolve_links=True ) ``` #### credentials The credentials used to read the data can override the default credentials set on the connection. ```python # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit" ) # Use credentials for reading events = client.get_stream( stream_name="some-stream", credentials=credentials ) ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read events backwards, set the `backwards` parameter to `True`: ```python # Read all events backwards from the end events = client.get_stream( stream_name="some-stream", backwards=True ) # Read backwards from a specific position events = client.get_stream( stream_name="some-stream", stream_position=100, backwards=True, limit=10 ) ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream that doesn't exist will raise a `NotFoundError` exception. It is important to handle this error when attempting to read a stream that may not exist. For example: ```python from kurrentdbclient.exceptions import NotFoundError try: events = client.get_stream(stream_name="order-0") for event in events: print(f"Event: {event.type}") except NotFoundError: print("Stream does not exist") except Exception as e: print(f"Error reading stream: {e}") ``` ## Reading from all events Reading from all events is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from all events. Additionally, you need to provide a commit position instead of a stream position when reading from all events. ### Reading forwards The simplest way to read all events forwards is to use the `read_all()` method. You can specify a commit position from which you want to start: ```python # Read all events from the beginning read_response = client.read_all() # Iterate through all events for event in read_response: print(f"Event: {event.type} from stream {event.stream_name}") ``` You can also start reading from a specific position in the global log: ```python # Read from a specific commit position read_response = client.read_all( commit_position=1110, limit=100 ) # Iterate through the events for event in read_response: print(f"Event: {event.type} at commit position {event.commit_position}") ``` There are a number of additional arguments you can provide when reading all events. #### limit Passing in the limit allows you to restrict the number of events that are returned. In the example below, we read a maximum of 100 events: ```python read_response = client.read_all( commit_position=1110, limit=100 ) ``` #### resolve\_links When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to `True` will tell KurrentDB to return the event as well as the event linking to it. ```python read_response = client.read_all(resolve_links=True) ``` #### credentials The credentials used to read the data can override the default credentials set on the connection. ```python # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit" ) # Use credentials for reading read_response = client.read_all(credentials=credentials) ``` ### Reading backwards In addition to reading all events forwards, you can read them backwards. To read all events backwards, set the `backwards` parameter to `True`: ```python # Read all events backwards from the end read_response = client.read_all(backwards=True) # Read backwards from a specific commit position read_response = client.read_all( commit_position=5000, backwards=True, limit=50 ) ``` :::tip Read one event backwards to find the last position in the global log. ::: ### Handling system events KurrentDB will also return system events when reading from all events. In most cases you can ignore these events, and they are filtered out by default. All system events begin with `$` and can be easily ignored. If you want to include system events, you can override the default filter: ```python # Include system events by overriding the default filter read_response = client.read_all(filter_exclude=()) # Process events and check for system events for event in read_response: if event.type.startswith("$"): print(f"System event: {event.type}") continue print(f"User event: {event.type}") ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to all events in the database using catch-up subscriptions. **Stream subscription:** ```python from kurrentdbclient import KurrentDBClient # Subscribe to a specific stream subscription = client.subscribe_to_stream(stream_name="order-123") # Iterate over events as they arrive for event in subscription: stream_name = event.stream_name stream_position = event.stream_position # Handle the event print(f"Received event: {event.type} at position {stream_position}") # Stop condition (for example purposes) if some_condition: subscription.stop() break ``` **Subscribe to all events:** ```python # Subscribe to all events in the database subscription = client.subscribe_to_all() # Iterate over events as they arrive for event in subscription: stream_name = event.stream_name commit_position = event.commit_position # Handle the event print(f"Received event: {event.type} from stream {stream_name}") # Stop condition (for example purposes) if some_condition: subscription.stop() break ``` When you subscribe to a stream with link events (e.g., category streams), set `resolve_links` to `True`. ```python # Subscribe with link resolution subscription = client.subscribe_to_stream( stream_name="$ce-order", resolve_links=True ) ``` ## Subscribing from a Position Both stream and all-events subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (integer representing the stream position): ```python # Subscribe from a specific stream position subscription = client.subscribe_to_stream( stream_name="order-123", stream_position=20 ) ``` **All events from specific position:** For all events, provide a commit position: ```python # Subscribe to all events from a specific commit position subscription = client.subscribe_to_all(commit_position=1056) ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```python # Stream - subscribe from the end subscription = client.subscribe_to_stream( stream_name="order-123", from_end=True ) # All events - subscribe from the end subscription = client.subscribe_to_all(from_end=True) ``` ## Resolving link events Link events point to events in other streams in KurrentDB. These are generally created by projections such as the by-event-type projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve links. By default, link events are not resolved. You can change this behaviour by setting the `resolve_links` parameter to `True`: ```python # Subscribe with link resolution enabled subscription = client.subscribe_to_stream( stream_name="$ce-order", resolve_links=True ) ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. You can handle this by catching exceptions and implementing retry logic in your application. The subscription can drop for various reasons, including network issues, server problems, or if the subscription is too slow to process events. ### Handling Dropped Subscriptions An application which hosts the subscription can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. You should implement retry logic to handle such cases. ```python import time from kurrentdbclient.exceptions import StreamNotFoundError def create_subscription_with_retry(client, stream_name, max_retries=5): retries = 0 while retries < max_retries: try: subscription = client.subscribe_to_stream(stream_name) return subscription except Exception as e: retries += 1 if retries >= max_retries: raise e print(f"Subscription failed, retrying in 5 seconds... ({retries}/{max_retries})") time.sleep(5) # Usage subscription = create_subscription_with_retry(client, "order-123") ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition by using the `include_caught_up` parameter when creating the subscription. ```python # Subscribe with caught-up notifications subscription = client.subscribe_to_stream( stream_name="order-123", include_caught_up=True ) for item in subscription: if hasattr(item, 'is_caught_up') and item.is_caught_up: print("Subscription has caught up to live events") else: # Regular event processing print(f"Processing event: {item.type}") ``` ::: tip The caught-up notification is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this notification will be sent right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to all events or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client. ```python # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit" ) # Use credentials for subscription subscription = client.subscribe_to_all(credentials=credentials) ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to all events to only receive the events you care about. You can filter by event type or stream name using regular expressions. Server-side filtering is currently only available when subscribing to all events. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```python # Filter by stream name prefix subscription = client.subscribe_to_all( filter_include=('test-.*', 'other-.*'), filter_by_stream_name=True ) ``` ### Filtering out system events System events are prefixed with `$` and are filtered out by default when subscribing to all events. If you want to include them, you can override the default filter: ```python # Include system events by using an empty exclude filter subscription = client.subscribe_to_all(filter_exclude=()) ``` ### Filtering by event type **By prefix:** ```python # Filter by event type prefix subscription = client.subscribe_to_all( filter_include=('customer-.*',), filter_by_stream_name=False # This is the default ) ``` **By regular expression:** ```python # Filter by event type using regex subscription = client.subscribe_to_all( filter_include=(r'^user.*|^company.*',) ) ``` ### Filtering by stream name **By prefix:** ```python # Filter by stream name prefix subscription = client.subscribe_to_all( filter_include=('user-.*',), filter_by_stream_name=True ) ``` **By regular expression:** ```python # Filter by stream name using regex (exclude system streams) subscription = client.subscribe_to_all( filter_include=(r'^[^$].*',), filter_by_stream_name=True ) ``` ## Checkpointing When a catch-up subscription is used to process all events containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the all-events stream that your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit position. ### Updating checkpoints at regular intervals You can implement checkpointing by periodically saving the commit position of processed events to a persistent store. ```python import time def process_events_with_checkpointing(client, checkpoint_store): # Get the last checkpoint last_commit_position = checkpoint_store.get_last_checkpoint() # Subscribe from the last checkpoint subscription = client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) events_processed = 0 checkpoint_interval = 100 # Save checkpoint every 100 events for item in subscription: if hasattr(item, 'is_checkpoint') and item.is_checkpoint: # This is a checkpoint message from the server checkpoint_store.save_checkpoint(item.commit_position) print(f"Checkpoint saved at position {item.commit_position}") else: # Regular event processing print(f"Processing event: {item.type} at position {item.commit_position}") events_processed += 1 # Save checkpoint at regular intervals if events_processed % checkpoint_interval == 0: checkpoint_store.save_checkpoint(item.commit_position) print(f"Checkpoint saved at position {item.commit_position}") ``` ### Configuring the checkpoint interval You can adjust how often the server sends checkpoint notifications by configuring the subscription: ```python # Subscribe with custom checkpoint configuration subscription = client.subscribe_to_all( include_checkpoints=True, # Checkpoints will be sent by the server periodically ) ``` By implementing checkpointing, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The server automatically sends checkpoint notifications at regular intervals to help you implement exactly-once processing patterns. ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/appending-events.md' --- # Appending events KurrentDB is an append-only event store database. Events in KurrentDB are organized within individual streams. There are two client methods for writing events to KurrentDB. * [`append_to_stream()`](#append-to-stream) * [`multi_append_to_stream()`](#multi-append-to-stream) The [Getting started](./getting-started.md#writing-to-kurrentdb) page introduced the topic of writing to KurrentDB. Let's explore what KurrentDB can do in more detail. ::: info Requires leader If you are using a KurrentDB cluster, please note, events can be appended only to leader nodes. ::: ## Append to stream Events in KurrentDB are organized in "streams". You can use the `append_to_stream()` method to append new events to a stream in KurrentDB. ::: info What is a stream? A stream in KurrentDB is a sequence of recorded events, each with a unique integer position. Each stream has a unique name. The positions of events in a stream are gapless. Stream positions in KurrentDB are "zero-based". The first event in a stream has position `0`, the second event has position `1`, the third has position `2`, and so on. ::: ### Description When appending events with `append_to_stream()`, you must provide a stream name and an iterable of `NewEvent` objects. You must also specify what optimistic concurrency control you want KurrentDB to activate before recording the new events. Optionally, you can also specify a timeout for the completion of the operation, and override credentials given in the "user info" part of the connection string. The `append_to_stream()` method is atomic, which means that either all or none of the new events will be recorded. Use the `current_version` parameter to active or deactivate optimistic concurrency control. ::: tip Please study the [Optimistic concurrency control](#optimistic-concurrency-control) section below for more information about optimistic concurrent controls in KurrentDB. ::: The `append_to_stream()` method is idempotent. If you call `append_to_stream()` twice with the same event IDs and the same `current_version` then the second call will succeed idempotently without creating duplicate events. ### Parameters | Name | Type | Required | Default | Description | |-------------------|----------------------|----------|----------|----------------------------------------------------------------| | `stream_name` | `str` | Yes | | Stream to which new events will be appended. | | `events` | `Iterable[NewEvent]` | Yes | | Events to append to the stream. | | `current_version` | `int \| StreamState` | Yes | | Activate or deactivate optimistic concurrent control. |\ | `timeout` | `float` | No | `None` | Maximum duration, in seconds, for completion of the operation. |\ | `credentials` | `CallCredentials` | No | `None` | Override credentials derived from the connection string. | ### Return value On success, `append_to_stream()` returns a commit position (`int`). The "commit position" is the position in the database of the last event recorded event. The value returned from `append_to_stream()` can be used when redirecting a user to an eventually consistent view. ### Example The example below connects to KurrentDB and appends a new event to a new stream. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct a new event object event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order:123"}', ) # Append the event to a stream commit_position = client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) await client.connect() # Construct a new event object event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order:123"}', ) # Append the event to a stream commit_position = await client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1], ) ``` ::: ## The NewEvent class Use the `NewEvent` dataclass when appending new events to KurrentDB. The `NewEvent` dataclass allows you to specify a event's type and content. Optionally, you can also specify the content type, metadata, and a unique ID. ### Fields | Name | Type | Required | Default | Description | |----------------|---------|----------|----------------------|---------------------------| | `type` | `str` | Yes | | The type of the event | | `data` | `bytes` | Yes | | The content of the event | | `metadata` | `bytes` | No | `b""` | Event metadata | | `content_type` | `str` | No | `"application/json"` | The format of the content | | `id` | `UUID` | No | `uuid.uuid4()` | A unique ID for the event | ### Event type Each new event should be supplied with an event `type`. Usually `NewEvent` objects are serialised representations of different types of domain events. It is common to for the `type` string of a `NewEvent` object to represent a domain event class, as it makes serialising and de-serialising of the event easy. ### Event data Usually the serialized state of a domain event object. If you serialize your domain events as JSON objects, you can take advantage of of KurrentDB's other functionality, such as projections. But you can serialize events using whatever format suits your requirements. The data will be stored as encoded bytes. ### Event metadata Storing additional information alongside your event that is not part of the event itself is supported by KurrentDB. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### Event content type The content type indicates whether the event is stored as JSON or binary format. You can choose between `'application/json'` (default) and `'application/octet-stream'` when creating your `NewEvent` object. For example, if you are using Message Pack or Protobuf to serialise your domain events, or you are serialising with JSON but also using application-level compression or encryption, then you can use `'application/octet-stream'` as the content type. ### Event ID Events can be uniquely identified using the `id` field of `NewEvent`. If two events with the same `UUID` are appended to the same stream with the same optimistic concurrency control, KurrentDB will only append one of the events to the stream. ## Optimistic concurrency control When appending events to a stream, you must supply a `current_version` argument. This informs KurrentDB of the state you expect the stream to be in when appending an event. If the stream isn't in that state, a `WrongCurrentVersionError` exception will be raised. There are several available options for the `current_version` argument: * Integer value - The stream position of the last recorded event * `StreamState.NO_STREAM` - Stream should not exist * `StreamState.EXISTS` - Stream should exist * `StreamState.ANY` - No concurrency check Usually, you will use: either `StreamState.NO_STREAM` when writing new events to a new stream; or the stream position of the last recorded event in the stream when writing subsequent events to an existing stream. This will protect the stream from becoming inconsistent due to conflicting concurrent writers. Alternatively, you can specify `StreamState.EXISTS`, which requires only that the stream already has at least one event. Or, you can fully deactivate concurrency control by specifying `StreamState.ANY`. ### Examples Let's recall that the stream `"order:123"` was created in the [example](#example) above. The example below shows that a second event can be successfully appended with `current_version` as the stream position of the first appended event, which is `0`. ::: tabs @tab sync ```python:no-line-numbers event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) client.append_to_stream( stream_name="order:123", current_version=0, # <-- correct value events=[event2], ) ``` @tab async ```python:no-line-numbers event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) await client.append_to_stream( stream_name="order:123", current_version=0, # <-- correct value events=[event2], ) ``` ::: The example below shows that a third event can be successfully appended with `current_version` as the stream position of the second appended event, which is `1`. ::: tabs @tab sync ```python:no-line-numbers event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) client.append_to_stream( stream_name="order:123", current_version=1, # <-- correct value events=[event3], ) ``` @tab async ```python:no-line-numbers event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) await client.append_to_stream( stream_name="order:123", current_version=1, # <-- correct value events=[event3], ) ``` ::: The operation in the example below fails because we use `StreamState.NO_STREAM` as the value of `current_version`, however the stream already exists. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError try: client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, # <-- wrong value events=[event3], ) except WrongCurrentVersionError: print("Stream already exists!") else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError try: await client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, # <-- wrong value events=[event3], ) except WrongCurrentVersionError: print("Stream already exists!") else: raise Exception("Shouldn't get here") ``` ::: The operation in the example below fails because we use `0` as the value of `current_version`, however the stream position of the last recorded event is now `2`. ::: tabs @tab sync ```python:no-line-numbers try: client.append_to_stream( stream_name="order:123", current_version=0, # <-- incorrect value events=[event3], ) except WrongCurrentVersionError: print("Wrong current version!") else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers try: await client.append_to_stream( stream_name="order:123", current_version=0, # <-- incorrect value events=[event3], ) except WrongCurrentVersionError: print("Wrong current version!") else: raise Exception("Shouldn't get here") ``` ::: ## Idempotent append Under certain conditions, KurrentDB allows append requests to succeed idempotently. Sometimes an append operation can succeed in KurrentDB, but the response can fail to reach the client, perhaps due to a network failure. If you call `append_to_stream()` twice with the same event IDs and the same `current_version` then the second call will succeed idempotently. The examples below show the operations in the previous examples succeeding idempotently. ::: tabs @tab sync ```python:no-line-numbers assert 2 == client.get_current_version("order:123") client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1], ) client.append_to_stream( stream_name="order:123", current_version=0, events=[event2], ) client.append_to_stream( stream_name="order:123", current_version=1, events=[event3], ) assert 2 == client.get_current_version("order:123") ``` @tab async ```python:no-line-numbers assert 2 == await client.get_current_version("order:123") await client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1], ) await client.append_to_stream( stream_name="order:123", current_version=0, events=[event2], ) await client.append_to_stream( stream_name="order:123", current_version=1, events=[event3], ) assert 2 == await client.get_current_version("order:123") ``` ::: The idempotent append behavior means that retries of apparently failed operations, that were actually successful, will be apparently successful without actually having any further effect. The idempotent append behavior can be understood as a kind of "forgiveness" for optimistic concurrency control failures, without which clients would need to probe the database to discover if an apparently failed request actually succeeded. But it also avoids recording duplicate events when optimistic concurrency controls are partially or fully disabled. The examples below show `append_to_stream()` being called with `event1`, `event2`, and `event3` whilst optimistic concurrency controls have been either fully or partially disabled, and that the stream has not changed. ::: tabs @tab sync ```python:no-line-numbers client.append_to_stream( stream_name="order:123", current_version=StreamState.ANY, events=[event1], ) client.append_to_stream( stream_name="order:123", current_version=StreamState.EXISTS, events=[event2], ) client.append_to_stream( stream_name="order:123", current_version=StreamState.EXISTS, events=[event3], ) assert 2 == client.get_current_version("order:123") ``` @tab async ```python:no-line-numbers await client.append_to_stream( stream_name="order:123", current_version=StreamState.ANY, events=[event1], ) await client.append_to_stream( stream_name="order:123", current_version=StreamState.EXISTS, events=[event2], ) await client.append_to_stream( stream_name="order:123", current_version=StreamState.EXISTS, events=[event3], ) assert 2 == await client.get_current_version("order:123") ``` ::: Please note, whilst in many cases this will avoid recording duplicates, this does not protect against recording more than one event with the same ID, for example by appending an event with the same ID, either at a much later time when disabling concurrency controls, or by specifying correctly the position of the last recorded event. ## User credentials You can use the `credentials` parameter of `append_to_stream()` to override the credentials given with the [user info](./getting-started.md#user-info-string) part of a client connection string. The helper method `construct_call_credentials()` constructs a `grpc.CallCredentials` object from a username and password. ::: tabs @tab sync ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = client.append_to_stream( stream_name="order:123", current_version=StreamState.ANY, events=[event3], credentials=credentials, ) ``` @tab async ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = await client.append_to_stream( stream_name="order:123", current_version=StreamState.ANY, events=[event3], credentials=credentials, ) ``` ::: ## Multi-append to stream ::: info Supported by KurrentDB 25.1 and later. ::: You can use the`multi_append_to_stream()` to append new events atomically to multiple streams. ### Description Use the multi-stream append operation when you want to atomically append new events to multiple streams in one call. When appending events with `multi_append_to_stream()` you must provide an iterable of `NewEvents` objects. Optionally, you can also specify a timeout for the completion of the operation, and override credentials given in the "user info" part of the connection string. The `multi_append_to_stream()` method is atomic, which means that either all or none of the new events will be recorded. The `multi_append_to_stream()` method is also idempotent. ### Parameters | Name | Type | Required | Default | Description | |-------------------|-----------------------|----------|---------|----------------------------------------------------------------| | `events` | `Iterable[NewEvents]` | Yes | | Events to append to the stream. | | `timeout` | `float` | No | `None` | Maximum duration, in seconds, for completion of the operation. |\ | `credentials` | `CallCredentials` | No | `None` | Override credentials derived from the connection string. | ### Return value On success, `append_to_stream()` returns a commit position (`int`). The "commit position" is the position in the database of the last event recorded event. The value returned from `multi_append_to_stream()` can be used when redirecting a user to an eventually consistent view. ### Example The example below appends new events to two streams. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvents new_events1 = NewEvents( stream_name="order:123", events=[ NewEvent(type='EventType1', data=b'{}'), NewEvent(type='EventType2', data=b'{}'), ], current_version=2, ) new_events2 = NewEvents( stream_name="order:456", events=[ NewEvent(type='EventType3', data=b'{}'), NewEvent(type='EventType4', data=b'{}'), ], current_version=StreamState.NO_STREAM, ) client.multi_append_to_stream( events=[new_events1, new_events2], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvents new_events1 = NewEvents( stream_name="order:123", events=[ NewEvent(type='EventType1', data=b'{}'), NewEvent(type='EventType2', data=b'{}'), ], current_version=2, ) new_events2 = NewEvents( stream_name="order:456", events=[ NewEvent(type='EventType3', data=b'{}'), NewEvent(type='EventType4', data=b'{}'), ], current_version=StreamState.NO_STREAM, ) await client.multi_append_to_stream( events=[new_events1, new_events2], ) ``` ::: ## The NewEvents class Use the `NewEvents` dataclass when appending event to multiple streams. The `NewEvents` dataclass allows you to specify the stream name, events to be appended to that stream, and optimistic concurrency controls for that stream. ### Fields The `NewEvents` dataclass has three fields, which are effectively the parameters of the `append_to_stream()` method that are missing from the `multi_append_to_stream()` method. | Name | Type | Required | Default | Description | |-------------------|------------------------|----------|---------|-------------------------------------------------------| | `stream_name` | `str` | Yes | | Stream to which new events will be appended. | | `events` | `Iterable[NewEvent]` | Yes | | The `NewEvent` objects to append to the stream. | | `current_version` | `int \| StreamState` | Yes | | Activate or deactivate optimistic concurrent control. | The sections above describe the fields of the `NewEvent` class, and also the behavior of the optimistic concurrency controls, and apply consistently to the operation of `multi_append_to_stream()`. There is one main difference: restrictions on the use of the `metadata` field of `NewEvent`. ### Metadata restrictions for multi-append When appending events with `multi_append_to_stream()`, the `metadata` field of each `NewEvent` must be either an empty `bytes` string or a `bytes` string containing a JSON object whose values are strings. The following metadata values are OK. | | Description | Examples | |---|--------------------------------|-----------------| | ✅ | Empty bytes | `b""` | | ✅ | JSON object with string values | `b'{"a": "1"}'` | The following metadata values are NOT okay and will cause a `ProgrammingError` exception. | | Description | Examples | |---|------------------------------------|-----------------------------------------------| | ❌ | Random bytes | `b'\xf5d\xc5W3^b\xb0(\xf9\x01D\x81\xa7Y\x98'` | | ❌ | JSON string | `b'"abcdef"'` | | ❌ | JSON object with non-string values | `b'{"a": 1}'` or `b'{"a": false}'` | | ❌ | Nested JSON objects | `b'{"a": {}}'` | --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs client = KurrentDBClient(uri="kurrentdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}") ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```python from kurrentdbclient import KurrentDBClient # Get the current version of the stream current_version = client.get_current_version(stream_name="some-stream") # Soft delete the stream commit_position = client.delete_stream( stream_name="some-stream", current_version=current_version ) ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```python from kurrentdbclient import KurrentDBClient # Get the current version of the stream current_version = client.get_current_version(stream_name="some-stream") # Hard delete (tombstone) the stream commit_position = client.tombstone_stream( stream_name="some-stream", current_version=current_version ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/getting-started.md' --- # Getting started This guide will help you get started with the Python client for KurrentDB. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Install package Add the `kurrentdbclient` package to your Python project: ::: tabs @tab uv ```bash:no-line-numbers uv add "kurrentdbclient~=1.1" ``` @tab poetry ```bash:no-line-numbers poetry add "kurrentdbclient~=1.1" ``` @tab pipenv ```bash:no-line-numbers pipenv install "kurrentdbclient~=1.1" ``` @tab pip ```bash:no-line-numbers pip install "kurrentdbclient~=1.1" && pip freeze > requirements.txt ``` ::: Once the Python package has been installed, you can import one of the Python client classes for KurrentDB. ## Import class The Python package `kurrentdbclient` provides two client classes for KurrentDB. * `KurrentDBClient` provides a **blocking** interface, and is suitable for standard sequential code and multi-threaded applications. * `AsyncKurrentDBClient` provides an **asynchronous** interface, and is suitable for high-concurrency applications using Python’s native `asyncio` framework. Both can be imported from the `kurrentdbclient` package. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient ``` ::: The client classes can be constructed with a KurrentDB connection string. ## Connection strings KurrentDB clients use **connection strings** to configure their connection to KurrentDB. KurrentDB connection strings are standardized across all the official KurrentDB clients. :::info When connecting to a production database, ask your server administrator for a valid connection string. ::: ### Two protocols KurrentDB connection strings support two protocols. * **`kurrentdb://`** for **connecting directly** to specific KurrentDB server endpoints. * **`kurrentdb+discover://`** for connecting using cluster discovery **via DNS A records**. With the `kurrentdb://` protocol you can specify one or many endpoints, separated by commas. If you specify only one endpoint, the client will **connect directly and remain with it**. If you specify many endpoints, the client will use them to query for cluster information and **pick an endpoint from the obtained cluster information** for continuing operations, according to the node preference specified by the connection string - see options below. This process will be repeated if the client detects that it needs to reconnect to the cluster. An "endpoint" can be a specified either as a host name or an IP address, with a port number. With the `kurrentdb+discover://` protocol you should specify a fully-qualified domain name of a KurrentDB cluster, with an optional port number. Using the cluster's **DNS A records**, the client will query for cluster information and pick an endpoint from the cluster information for continuing operations, according to the node preference specified by the connection string - see options below. This process will be repeated if the client detects that it needs to reconnect to the cluster. ### User info string Both the `kurrentdb://` and `kurrentdb+discover://` protocols support an optional user info string. If it exists, the user info string must be separated from the rest of the URI with the `"@"` character. The user info string must include a username and a password, separated with the `":"` character. The user info is sent by the client in a "basic auth" authorization header in each gRPC call to a "secure" server. This authorization header is used by the server to authenticate the client. The Python client does not allow call credentials to be transferred to "insecure" servers (option `tls=false`). ### Examples In the examples below, `user` is a username and `pass` is a password. For connecting directly to a single node, use the following format: ```:no-line-numbers kurrentdb://user:pass@node1.example.com:2113 ``` For connecting to a cluster, using specific endpoints to obtain cluster information, whilst observing your node preference for continuing operations, use the following format: ```:no-line-numbers kurrentdb://user:pass@node1.example.com:2113,node2.example.com:2113,node3.example.com:2113 ``` For connecting to a cluster, where `cluster1.example.com` is configured with DNS A records for the cluster endpoints, whilst observing your node preference for continuing operations, use the following format: ```:no-line-numbers kurrentdb+discover://user:pass@cluster1.example.com:2113 ``` ### Options The table below describes optional query parameters that can be used in the connection string to configure the client. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Set to `false` when connecting to KurrentDB running with "insecure" mode. | | `connectionName` | Any string | Random UUID | Connection name | | `maxDiscoverAttempts` | Integer | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Integer | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Integer | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | File system path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Integer | None | Maximum duration, in seconds, for completion of client operations. Can be overridden per operation using the `timeout` parameter of client methods. | | `keepAliveInterval` | Integer | None | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Integer | None | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | File system path | None | User certificate file for X.509 authentication. | | `userKeyFile` | File system path | None | Key file for the user certificate used for X.509 authentication. | :::tip Please note, all option field names and values are case-insensitive. ::: ## Start KurrentDB For local development, you can run KurrentDB in "insecure" mode with Docker. ```bash export KURRENTDB_IMAGE=docker.kurrent.io/kurrent-latest/kurrentdb:latest docker run --rm -p 2113:2113 $KURRENTDB_IMAGE --insecure ``` Then use connection string `kurrentdb://127.0.0.1:2113?tls=false` when connecting to KurrentDB. ## Connect to KurrentDB Construct a client with a connection string. ::: warning If you are using the async client, you will need also to call the async `connect()` method. ::: The example below connects to a KurrentDB server running locally in "insecure" mode. ::: tabs @tab sync ```python uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # immediately connected to KurrentDB ``` @tab async ```python uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) await client.connect() # connect to KurrentDB ``` ::: :::tip The sync and async client classes have identical methods, except the methods of the async client are defined with `async def` and so must be `await`-ed when called. ::: ## Test the connection You can test the connection by getting the database "commit position", which is the position in the database of the last recorded event. You can get the "commit position" by calling the client method `get_commit_position()`. ::: tabs @tab sync ```python client.get_commit_position() ``` @tab async ```python await client.get_commit_position() ``` ::: If you have just started KurrentDB for the first time, the returned value will be zero, `0`, which indicates that no events have been recorded. :::caution If the connection fails, check KurrentDB is running locally in "insecure" mode (see above). ::: ## Writing to KurrentDB KurrentDB is an event store database. So let's get started by appending an event! The client method `append_to_stream()` writes new events in KurrentDB. The example below appends the first new event of stream `"order:123"`. ::: tabs @tab sync ```python from kurrentdbclient import NewEvent, StreamState client.append_to_stream( stream_name="order:123", events=[ NewEvent(type="OrderCreated", data=b'{"name": "Greg"}'), ], current_version=StreamState.NO_STREAM, ) ``` @tab async ```python from kurrentdbclient import NewEvent, StreamState await client.append_to_stream( stream_name="order:123", events=[ NewEvent(type="OrderCreated", data=b'{"name": "Greg"}'), ], current_version=StreamState.NO_STREAM, ) ``` ::: The `stream_name` parameter identifies the "stream" to which events will be appended. The `events` parameter is a list of `NewEvent` objects. The `NewEvent` class is a Python `dataclass`. The `current_version` parameter activates optimistic concurrent control. The `StreamState.NO_STREAM` argument indicates we require the stream has no previously recorded events. See [Appending events](./appending-events.md) for more information about writing to KurrentDB. ## Reading from KurrentDB The client method `read_stream()` returns an iterator of events that have been recorded in KurrentDB. The example below reads and prints the events of stream `"order:123"`. ::: tabs @tab sync ```python for event in client.read_stream("order:123"): print(event) ``` @tab async ```python async for event in await client.read_stream("order:123"): print(event) ``` ::: The first parameter is used to identify the stream. In the example above, the given argument is `"order:123"`. See [Reading events](./appending-events.md) for more information about reading from KurrentDB. --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to all events, which includes all events in the database. ### Subscribing to a Specific Stream ```python from kurrentdbclient import KurrentDBClient # Create a persistent subscription to a specific stream client.create_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) ``` ### Subscribing to All Events ```python # Create a persistent subscription to all events with filtering client.create_subscription_to_all( group_name="subscription-group", filter_include=("test.*",), filter_by_stream_name=True ) ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to all events supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for all events similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The client will automatically manage the buffer size and flow control to ensure optimal performance. The server distributes events to consumers based on the configured consumer strategy. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```python # Connect to a persistent subscription for a specific stream subscription = client.read_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") ``` ### Connecting to all events The code below shows how to connect to an existing subscription group for all events: ```python # Connect to a persistent subscription for all events subscription = client.read_subscription_to_all( group_name="subscription-group" ) # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") ``` The `read_subscription_to_all()` method is identical to the `read_subscription_to_stream()` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```python # Connect to persistent subscription subscription = client.read_subscription_to_stream( group_name="subscription-group", stream_name="order-123" ) # Process events with proper acknowledgement for event in subscription: try: # Process the event process_event(event) # Acknowledge successful processing subscription.ack(event) except ProcessingError as e: # Handle processing failure print(f"Processing failed: {e}") subscription.nack(event, action="retry") except CriticalError as e: # Handle critical failure print(f"Critical error: {e}") subscription.nack(event, action="park") ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `"park"` | Park the message and do not resend. Put it on poison queue. | | `"retry"` | Explicitly retry the message. | | `"skip"` | Skip this message do not resend and do not put in poison queue. | | `"stop"` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system by-category projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```python # Update a persistent subscription to a stream client.update_subscription_to_stream( group_name="subscription-group", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) # Update a persistent subscription to all events client.update_subscription_to_all( group_name="subscription-group", resolve_links=True, min_checkpoint_count=20 ) ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `from_end` | Whether to start the subscription from the end of the stream. | `False` (start from the beginning) | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `message_timeout` | The amount of time after which to consider a message as timed out and retried. | `30.0` (seconds) | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `max_subscriber_count` | The maximum number of subscribers allowed. | `0` (unbounded) | | `consumer_strategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `"RoundRobin"` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```python # Delete a persistent subscription to a stream client.delete_subscription( group_name="subscription-group", stream_name="order-123" ) # Delete a persistent subscription to all events client.delete_subscription( group_name="subscription-group" ) ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from a named stream, and reading from all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **stream position** is an integer that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential position (0, 1, 2, 3...). The **commit position** represents the event's location in KurrentDB's global transaction log and is a single integer value that indicates where the event was committed in the log. These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. There are two client methods for reading events from KurrentDB. * [`read_stream()`](#read-stream) * [`read_all()`](#read-all) ## Read stream You can use the `read_stream()` method to read events from a stream in KurrentDB. ### Description You can read all the events or a sample of the events from an individual stream, starting from any position in the stream, and can read either forward or backward. ### Parameters | Name | Type | Required | Default | Description | |-------------------|------------------------|----------|---------|----------------------------------------------------------------| | `stream_name` | `str` | Yes | | Stream from which events will be read. | | `stream_position` | `int \| None` | No | `None` | Position from which to start reading events. | | `backwards` | `bool` | No | `False` | Activate reading of events in reverse order. |\ | `resolve_links` | `bool` | No | `False` | Activate resolution of "link events". |\ | `limit` | `int \| None` | No | `None` | Maximum number of events to return. |\ | `timeout` | `float` | No | `None` | Maximum duration, in seconds, for completion of the operation. |\ | `credentials` | `CallCredentials` | No | `None` | Override credentials derived from the connection string. | ### Return value On success, `read_stream()` returns an iterable of `RecordedEvent` objects. ::: tip The Python client method `get_stream()` was added as a convenient method that evaluates the iterable response from `read_stream()` as a Python `tuple` of `RecordedEvent` objects. You can use the `get_stream()` method for simple cases or `read_stream()` for streaming large result sets. ::: ### Examples The examples below read events from the stream `"order:123"`. Let's begin by connecting to KurrentDB and appending some events. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order:123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position3 = client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) await client.connect() # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order:123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position3 = await client.append_to_stream( stream_name="order:123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: That gives three events recorded in the stream `"order:123"` that we can read. #### Reading forwards The simplest way to read a stream is use supply a `stream_name` argument, for example `"order:123"`, and read every event already recorded in that stream. This is a typical operation when retrieving events to construct a decision model, such as an event-sourced aggregate. Remember `get_stream()` returns a `tuple` whereas `read_stream()` returns a more basic iterable. ::: tabs @tab sync ```python:no-line-numbers # Get all events from a stream events = client.get_stream(stream_name="order:123") # Iterate through a tuple of events with a 'for' loop for event in events: print(f"Event: {event.type} at position {event.stream_position}") # Or use read_stream() for streaming events = client.read_stream(stream_name="order:123") # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` @tab async ```python:no-line-numbers # Get all events from a stream events = await client.get_stream(stream_name="order:123") # Iterate through a tuple of events with a 'for' loop for event in events: print(f"Event: {event.type} at position {event.stream_position}") # Or use read_stream() for streaming events = await client.read_stream(stream_name="order:123") # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` ::: ::: tip The `read_stream()` method of the sync client returns a Python `Iterable`. However, the async client returns an `AsyncIterable` so you must use an `async for` loop instead. ::: #### Checking if the stream exists Reading a stream that doesn't exist will raise a `NotFoundError` exception. It is important to handle this error when attempting to read a stream that may not exist. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: events = client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: events = await client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: #### Reading from a stream position Use `stream_position` to start reading from a specific position in the stream. This is useful for example when advancing a snapshot of an aggregate to the latest current state. The example below reads from stream position `2`. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific stream position events = client.read_stream( stream_name="order:123", stream_position=2, ) ``` @tab async ```python:no-line-numbers # Read from a specific stream position events = await client.read_stream( stream_name="order:123", stream_position=1, ) ``` ::: ::: tip Without setting `stream_position`, by default the Python clients will from the first stream position when reading forwards, and from the last stream position when reading backwards. ::: #### Reading backwards Set the `backwards` parameter to `True` to read events backwards from a stream: ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end events = client.read_stream( stream_name="order:123", backwards=True, ) # Read backwards from a specific position events = client.read_stream( stream_name="order:123", stream_position=2, backwards=True, ) ``` @tab async ```python:no-line-numbers # Read all events backwards from the end events = await client.read_stream( stream_name="order:123", backwards=True, ) # Read backwards from a specific position events = await client.read_stream( stream_name="order:123", stream_position=2, backwards=True, ) ``` ::: :::tip Read backwards with a limit of `1` to find the last position in the stream. Alternatively, call the convenience method `get_stream_position()`. ::: #### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links` to `True` so that KurrentDB will resolve the "link event" and return the linked event. ::: tabs @tab sync ```python:no-line-numbers events = client.get_stream( stream_name="order:123", resolve_links=True ) ``` @tab async ```python:no-line-numbers events = await client.get_stream( stream_name="order:123", resolve_links=True ) ``` ::: ::: tip You can tell if you are receiving "link events" because the `type` attribute start with `"$>"`. ::: #### Reading a limited number of events Passing in a `limit` argument allows you to restrict the number of events that are returned. In the example below, we read a maximum of two events from the stream: ::: tabs @tab sync ```python:no-line-numbers events = client.get_stream( stream_name="order:123", limit=2 ) ``` @tab async ```python:no-line-numbers events = await client.get_stream( stream_name="order:123", limit=2 ) ``` ::: #### User credentials You can use the `credentials` parameter of `read_stream()` and `get_stream()` to override the credentials given with the [user info](./getting-started.md#user-info-string) part of a client connection string. The helper method `construct_call_credentials()` constructs a `grpc.CallCredentials` object from a username and password. ::: tabs @tab sync ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="user101", password="my-middle-name", # :-) ) # Use credentials for reading events = client.get_stream( stream_name="order:123", credentials=credentials ) ``` @tab async ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="user101", password="my-middle-name", # :-) ) # Use credentials for reading events = await client.get_stream( stream_name="order:123", credentials=credentials ) ``` ::: ## Read all You can use the `read_all()` method to read from all events across the entire event store. ### Description Reading from all events is similar to reading from an individual stream, but please note there are differences. You need to provide a commit position instead of a stream position when reading from all events. When connecting to a "secure" server, you need to use user account credentials that have sufficient permissions to read from all events. You can also provide arguments for filtering events, either to include or exclude events, by either event type (the default) or by stream name. ### Parameters | Name | Type | Required | Default | Description | |-------------------------|--------------------|----------|---------|----------------------------------------------------------------------------| | `commit_position` | `int \| None` | No | `None` | Position from which to start reading events. | | `backwards` | `bool` | No | `False` | Activate reading of events in reverse order. | | `resolve_links` | `bool` | No | `False` | Activate resolution of "link events". | | `filter_exclude` | `Sequence[str]` | No | | Exclude matching events (system generated events are excluded by default). | | `filter_include` | `Sequence[str]` | No | `()` | Include matching events (if set, only matching events will be returned). | | `filter_by_stream_name` | `bool` | No | `False` | Filter by stream name (default is to filter by event type). | | `limit` | `int \| None` | No | `None` | Maximum number of events to return. | | `timeout` | `float` | No | `None` | Maximum duration, in seconds, for completion of the operation. | | `credentials` | `CallCredentials` | No | `None` | Override credentials derived from the connection string. | ### Return value On success, `read_all()` returns an iterable of `RecordedEvent` objects. ### Examples #### Reading forwards The simplest way to read all events forwards is to use the `read_all()` method. ::: tabs @tab sync ```python:no-line-numbers # Read all events from the beginning events = client.read_all() # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` @tab async ```python:no-line-numbers # Read all events from the beginning events = await client.read_all() # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` ::: ::: tip The `read_all()` method of the sync client returns a Python `Iterable`. However, the async client returns an `AsyncIterable` so you must use an `async for` loop instead. ::: #### Reading from a commit position You can also start reading from a specific position in the global log: ::: tabs @tab sync ```python:no-line-numbers # Read from a specific commit position events = client.read_all( commit_position=commit_position3, ) ``` @tab async ```python:no-line-numbers # Read from a specific commit position events = await client.read_all( commit_position=commit_position3, ) ``` ::: ### Reading backwards In addition to reading all events forwards, you can read them backwards. To read all events backwards, set the `backwards` parameter to `True`: ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end events = client.read_all(backwards=True) # Read backwards from a specific commit position events = client.read_all( commit_position=commit_position3, backwards=True, ) ``` @tab async ```python:no-line-numbers # Read all events backwards from the end events = await client.read_all(backwards=True) # Read backwards from a specific commit position events = await client.read_all( commit_position=commit_position3, backwards=True, ) ``` ::: :::tip Read one event backwards to find the last position in the global log. Alternatively, call the convenience method `get_commit_position()`. ::: #### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links` to `True` so that KurrentDB will resolve the "link event" and return the linked event. ::: tabs @tab sync ```python:no-line-numbers events = client.read_all(resolve_links=True) ``` @tab async ```python:no-line-numbers events = await client.read_all(resolve_links=True) ``` ::: #### Reading a limited number of events Passing in a `limit` allows you to restrict the number of events that are returned. In the example below, we read a maximum of 100 events: ::: tabs @tab sync ```python:no-line-numbers events = client.read_all( limit=100, ) ``` @tab async ```python:no-line-numbers events = await client.read_all( limit=100, ) ``` ::: #### User credentials You can use the `credentials` parameter of `read_all()` to override the credentials given with the [user info](./getting-started.md#user-info-string) part of a client connection string. The helper method `construct_call_credentials()` constructs a `grpc.CallCredentials` object from a username and password. ::: tabs @tab sync ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="user101", password="my-middle-name", # :-) ) # Use credentials for reading events = client.read_all(credentials=credentials) ``` @tab async ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="user101", password="my-middle-name", # :-) ) # Use credentials for reading events = await client.read_all(credentials=credentials) ``` ::: ### Filtering events by type and stream name You can read more selectively by supplying an argument for either the `filter_include` or the `filter_exclude` parameters. By default, events will be filtered by `type`. Alternatively, you can filter events by `stream_name` name by setting the `filter_by_stream_name` parameter to `True`. The example below selects all events that have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers events = client.read_all( filter_include="Order.*", ) ``` @tab async ```python:no-line-numbers events = await client.read_all( filter_include="Order.*", ) ``` ::: The example below selects all events that do not have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers events = client.read_all( filter_exclude="Order.*", ) ``` @tab async ```python:no-line-numbers events = await client.read_all( filter_exclude="Order.*", ) ``` ::: The example below selects all events that have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers events = client.read_all( filter_include="order.*", filter_by_stream_name=True, ) ``` @tab async ```python:no-line-numbers events = await client.read_all( filter_include="order.*", filter_by_stream_name=True, ) ``` ::: The example below selects all events that do not have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers events = client.read_all( filter_exclude="order.*", filter_by_stream_name=True, ) ``` @tab async ```python:no-line-numbers events = await client.read_all( filter_exclude="order.*", filter_by_stream_name=True, ) ``` ::: ::: tip The `filter_include` and `filter_exclude` parameters are designed to have exactly the opposite effect from each other, so that a sequence of strings given to `filter_include` will return exactly those events which would be excluded if the same argument value were used with `filter_exclude`. And vice versa, so that a sequence of strings given to `filter_exclude` will return exactly those events that would not be included if the same argument value were used with `filter_include`. ::: ::: tip The `filter_include` parameter takes precedence over `filter_exclude`. That is to say, if you pass arguments for both, the `filter_exclude` argument will be ignored. ::: ::: tip The `filter_include` and `filter_exclude` arguments are typed as `Sequence[str]` which means that you can either pass a single `str`, or a collection of `str`. ::: ::: tip The `str` values should be unanchored regular expression patterns. If you supply a collection of `str`, they will be concatenated together by the Python client as bracketed alternatives in a larger regular expression that is anchored to the start and end of the strings being matched. So there is no need to include the `'^'` and `'$'` anchor assertions. ::: ::: tip Characters that are metacharacters with special meaning in regular expressions, such as `.` `*` `+` `?` `^` `$` `|` `(` `)` `[` `]` `{` `}` `\` must be escaped to be used literally when matching event types and stream names. Python's raw string literals can help to avoid doubling of escape backslashes. For example `r"\$.*"` can be used to match system event types. ::: ::: tip You should use wildcards if you want to match substrings. For example, `"Order.*"` matches all strings that start with `"Order"`. Alternatively,`".*Snapshot"` matches all strings that end with `"Snapshot"`. ::: ::: tip KurrentDB generates "system events" that all have a `type` that begins with `"$"`. They are filtered out by default, along with `PersistentConfig` and `Result` events. If you want to read all events excluding custom events whilst also excluding the default events, then use an argument for `filter_exclude` that adds to the default value DEFAULT\_EXCLUDE\_FILTER. If you especially want to read system events, then you can override the default filter by passing an empty sequence as the `filter_exclude` argument, or by selecting for them with a value of `filter_include`. ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to all events in the database using catch-up subscriptions. **Stream subscription:** ```python from kurrentdbclient import KurrentDBClient # Subscribe to a specific stream subscription = client.subscribe_to_stream(stream_name="order-123") # Iterate over events as they arrive for event in subscription: stream_name = event.stream_name stream_position = event.stream_position # Handle the event print(f"Received event: {event.type} at position {stream_position}") # Stop condition (for example purposes) if some_condition: subscription.stop() break ``` **Subscribe to all events:** ```python # Subscribe to all events in the database subscription = client.subscribe_to_all() # Iterate over events as they arrive for event in subscription: stream_name = event.stream_name commit_position = event.commit_position # Handle the event print(f"Received event: {event.type} from stream {stream_name}") # Stop condition (for example purposes) if some_condition: subscription.stop() break ``` When you subscribe to a stream with link events (e.g., category streams), set `resolve_links` to `True`. ```python # Subscribe with link resolution subscription = client.subscribe_to_stream( stream_name="$ce-order", resolve_links=True ) ``` ## Subscribing from a Position Both stream and all-events subscriptions accept a starting position if you want to read from a specific point onward. If events already exist at the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (integer representing the stream position): ```python # Subscribe from a specific stream position subscription = client.subscribe_to_stream( stream_name="order-123", stream_position=20 ) ``` **All events from specific position:** For all events, provide a commit position: ```python # Subscribe to all events from a specific commit position subscription = client.subscribe_to_all(commit_position=1056) ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```python # Stream - subscribe from the end subscription = client.subscribe_to_stream( stream_name="order-123", from_end=True ) # All events - subscribe from the end subscription = client.subscribe_to_all(from_end=True) ``` ## Resolving link events Link events point to events in other streams in KurrentDB. These are generally created by projections such as the by-event-type projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve links. By default, link events are not resolved. You can change this behaviour by setting the `resolve_links` parameter to `True`: ```python # Subscribe with link resolution enabled subscription = client.subscribe_to_stream( stream_name="$ce-order", resolve_links=True ) ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. You can handle this by catching exceptions and implementing retry logic in your application. The subscription can drop for various reasons, including network issues, server problems, or if the subscription is too slow to process events. ### Handling Dropped Subscriptions An application which hosts the subscription can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. You should implement retry logic to handle such cases. ```python import time from kurrentdbclient.exceptions import StreamNotFoundError def create_subscription_with_retry(client, stream_name, max_retries=5): retries = 0 while retries < max_retries: try: subscription = client.subscribe_to_stream(stream_name) return subscription except Exception as e: retries += 1 if retries >= max_retries: raise e print(f"Subscription failed, retrying in 5 seconds... ({retries}/{max_retries})") time.sleep(5) # Usage subscription = create_subscription_with_retry(client, "order-123") ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition by using the `include_caught_up` parameter when creating the subscription. ```python # Subscribe with caught-up notifications subscription = client.subscribe_to_stream( stream_name="order-123", include_caught_up=True ) for item in subscription: if hasattr(item, 'is_caught_up') and item.is_caught_up: print("Subscription has caught up to live events") else: # Regular event processing print(f"Processing event: {item.type}") ``` ::: tip The caught-up notification is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this notification will be sent right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to all events or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client. ```python # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit" ) # Use credentials for subscription subscription = client.subscribe_to_all(credentials=credentials) ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to all events to only receive the events you care about. You can filter by event type or stream name using regular expressions. Server-side filtering is currently only available when subscribing to all events. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```python # Filter by stream name prefix subscription = client.subscribe_to_all( filter_include=('test-.*', 'other-.*'), filter_by_stream_name=True ) ``` ### Filtering out system events System events are prefixed with `$` and are filtered out by default when subscribing to all events. If you want to include them, you can override the default filter: ```python # Include system events by using an empty exclude filter subscription = client.subscribe_to_all(filter_exclude=()) ``` ### Filtering by event type **By prefix:** ```python # Filter by event type prefix subscription = client.subscribe_to_all( filter_include=('customer-.*',), filter_by_stream_name=False # This is the default ) ``` **By regular expression:** ```python # Filter by event type using regex subscription = client.subscribe_to_all( filter_include=(r'^user.*|^company.*',) ) ``` ### Filtering by stream name **By prefix:** ```python # Filter by stream name prefix subscription = client.subscribe_to_all( filter_include=('user-.*',), filter_by_stream_name=True ) ``` **By regular expression:** ```python # Filter by stream name using regex (exclude system streams) subscription = client.subscribe_to_all( filter_include=(r'^[^$].*',), filter_by_stream_name=True ) ``` ## Checkpointing When a catch-up subscription is used to process all events containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the all-events stream that your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit position. ### Updating checkpoints at regular intervals You can implement checkpointing by periodically saving the commit position of processed events to a persistent store. ```python import time def process_events_with_checkpointing(client, checkpoint_store): # Get the last checkpoint last_commit_position = checkpoint_store.get_last_checkpoint() # Subscribe from the last checkpoint subscription = client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) events_processed = 0 checkpoint_interval = 100 # Save checkpoint every 100 events for item in subscription: if hasattr(item, 'is_checkpoint') and item.is_checkpoint: # This is a checkpoint message from the server checkpoint_store.save_checkpoint(item.commit_position) print(f"Checkpoint saved at position {item.commit_position}") else: # Regular event processing print(f"Processing event: {item.type} at position {item.commit_position}") events_processed += 1 # Save checkpoint at regular intervals if events_processed % checkpoint_interval == 0: checkpoint_store.save_checkpoint(item.commit_position) print(f"Checkpoint saved at position {item.commit_position}") ``` ### Configuring the checkpoint interval You can adjust how often the server sends checkpoint notifications by configuring the subscription: ```python # Subscribe with custom checkpoint configuration subscription = client.subscribe_to_all( include_checkpoints=True, # Checkpoints will be sent by the server periodically ) ``` By implementing checkpointing, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The server automatically sends checkpoint notifications at regular intervals to help you implement exactly-once processing patterns. ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/appending-events.md' --- # Appending Events This guide describes the Python client methods for recording new events in KurrentDB. ## Introduction In KurrentDB, events are appended to [streams](#what-is-a-stream). The Python client for KurrentDB has two methods for writing new events: * [`append_to_stream()`](#append-to-stream) – write a collection of events to a named stream * [`multi_append_to_stream()`](#multi-append-to-stream) – write many collections of events, each to a different stream ::: info Requires leader When connecting to a KurrentDB cluster, events can be written only to the leader node. ::: These methods are atomic and [idempotent](#idempotent-append-behavior). All or none of the new events will be recorded once. The Python client for KurrentDB also has methods for getting and setting [stream metadata](@server/features/streams.md#metadata-and-reserved-names): * [`get_stream_metadata()`](#get-stream-metadata) * [`set_stream_metadata()`](#set-stream-metadata) ## What is a Stream? A stream in KurrentDB is a sequence of recorded events, each with a unique integer position. Each stream has a unique name. The positions of events in a stream are zero-based and gapless. The first event in a stream has position `0`, the second event has position `1`, the third has position `2`, and so on. ## Events in KurrentDB KurrentDB organises events in streams within a global transaction log. Two sequence numbers are assigned to each recorded event: * **stream position** – the position of a recorded event within its stream * **commit position** – the position of a recorded event in the global transaction log These numbers are assigned when new events are recorded, and used when recorded events are read. ## New Events The `NewEvent` class is provided for specifying new events before calling an append method. | Field | Type | Description | Default | |----------------|---------|---------------------------|----------------------| | `type` | `str` | The type of the event | | | `data` | `bytes` | The content of the event | | | `metadata` | `bytes` | Event metadata | `b""` | | `content_type` | `str` | The format of the content | `"application/json"` | | `id` | `UUID` | A unique ID for the event | `uuid.uuid4()` | ### Event Type Each new event must be supplied with an event `type` string. ### Event Data The `data` field is a Python bytes object that carries the event payload. Usually the serialized state of a domain event object. If you serialize your domain events as JSON objects, you can take advantage of KurrentDB's other functionality, such as projections. But you can serialize events using whatever format suits your requirements. The data will be stored as encoded bytes. ### Event Metadata The `metadata` field is a Python bytes object that carries salient information about the event. It can be used for storing additional information alongside your event payload, such as correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### Event Content Type The `content_type` field indicates whether the event is stored as JSON or binary format. You can choose between `'application/json'` (default) and `'application/octet-stream'`. For example, if you are using Message Pack or Protobuf to serialise your domain events, or you are serialising with JSON but also using application-level compression or encryption, then you can use `'application/octet-stream'` as the content type. The default value is `'application/json'`. ### Event ID The `id` field is a `UUID` object that can uniquely identify the event. KurrentDB does not enforce unique event IDs, however they are used to activate [idempotent append behavior](#idempotent-append-behavior). If two events with the same `UUID` are appended to the same stream with the same optimistic concurrency control, KurrentDB will only append one of the events to the stream. The default value is a new version 4 UUID. ### Examples Here's an example where only the `type` string and binary `data` are provided. ```python:no-line-numbers from kurrentdbclient import NewEvent order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) ``` You may also specify `metadata`, `content_type` and an `id`. ```python:no-line-numbers from uuid import uuid4 order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', metadata=b'{"correlation_id": "56"}', content_type="application/json", id=uuid4(), ) ``` ## Append to Stream The Python client's `append_to_stream()` method appends new events to a named stream. This method is atomic and [idempotent](#idempotent-append-behavior). Provide a `stream_name` argument, an `events` argument, and a `current_version` argument. The `events` argument must be an iterable of [`NewEvent`](#new-events) objects. The `current_version` parameter specifies what [optimistic concurrency control](#optimistic-concurrency-control) you want KurrentDB to apply. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream to which the `events` will be appended. | | | `events` | The [NewEvent](#new-events) objects to be appended to the stream. | | | `current_version` | The [optimistic concurrency control](#optimistic-concurrency-control) for appending `events`. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `append_to_stream()` returns the commit position (`int`) of the last event it appended. This value represents that event’s position in the global transaction log, and can be used by applications to wait until eventually consistent views reflect newly recorded events. ### Optimistic Concurrency Control The `current_version` argument can be used to inform KurrentDB of the state you expect a stream to be in when appending events. There are several available options for the `current_version` argument: * `StreamState.ANY` - No concurrency check * `StreamState.EXISTS` - Stream should exist * `StreamState.NO_STREAM` - Stream should not exist * `int` value - Stream position of the last recorded event If the optimistic concurrency control fails, a `WrongCurrentVersionError` exception will be raised. Usually you will use `StreamState.NO_STREAM` when writing new events to a new stream, and then the correct stream position of the last recorded event in the stream when writing subsequent events. This will protect the stream from becoming inconsistent due to conflicting concurrent writers. Alternatively, you can specify `StreamState.EXISTS`, which requires only that the stream already has at least one event. Or, you can fully deactivate concurrency control by specifying `StreamState.ANY`. Let's see how to activate and deactivate optimistic concurrency control. ### Append to New Stream Here's an example appending the first event to stream `'order-123'`. The `current_version` argument `StreamState.NO_STREAM` requires that no events have been appended for this stream name. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, StreamState # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(connection_string) # Create a new stream with a new event client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, # <-- correct value events=[order_created], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, StreamState # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(connection_string) # Create a new stream with a new event await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, # <-- correct value events=[order_created], ) ``` ::: ### Append to Existing Stream Here's an example appending a second event to stream `'order-123'`. The `current_version` argument `0` is the position of the first event in the stream. ::: tabs @tab sync ```python:no-line-numbers payment_received = NewEvent( type="PaymentCompleted", data=b'{}', ) client.append_to_stream( stream_name="order-123", current_version=0, # <-- correct value events=[payment_received], ) ``` @tab async ```python:no-line-numbers payment_received = NewEvent( type="PaymentCompleted", data=b'{}', ) await client.append_to_stream( stream_name="order-123", current_version=0, # <-- correct value events=[payment_received], ) ``` ::: Here's an example that shows a third event can be successfully appended with `current_version` as the stream position of the second appended event, which is `1`. ::: tabs @tab sync ```python:no-line-numbers product_shipped = NewEvent( type="ProductShipped", data=b'{}', ) client.append_to_stream( stream_name="order-123", current_version=1, # <-- correct value events=[product_shipped], ) ``` @tab async ```python:no-line-numbers product_shipped = NewEvent( type="ProductShipped", data=b'{}', ) await client.append_to_stream( stream_name="order-123", current_version=1, # <-- correct value events=[product_shipped], ) ``` ::: ### Wrong Current Version Error Here's an example that shows optimistic concurrent control rejecting an append options. In this example,`StreamState.NO_STREAM` is specified as the value of `current_version`, however the stream already exists, and so the append operation fails. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError product_received = NewEvent( type="ProductReceived", data=b'{}', ) try: client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, # <-- wrong value events=[product_received], ) except WrongCurrentVersionError: print("Stream already exists!") else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError product_received = NewEvent( type="ProductReceived", data=b'{}', ) try: await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, # <-- wrong value events=[product_received], ) except WrongCurrentVersionError: print("Stream already exists!") else: raise Exception("Shouldn't get here") ``` ::: Similarly, the append operation in the example below fails because the value of `current_version` is `0`, however the stream position of the last recorded event in stream `order-123` is `2`. ::: tabs @tab sync ```python:no-line-numbers try: client.append_to_stream( stream_name="order-123", current_version=0, # <-- incorrect value events=[product_shipped], ) except WrongCurrentVersionError: print("Wrong current version!") else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers try: await client.append_to_stream( stream_name="order-123", current_version=0, # <-- incorrect value events=[product_shipped], ) except WrongCurrentVersionError: print("Wrong current version!") else: raise Exception("Shouldn't get here") ``` ::: ### Idempotent Append Behavior When [optimistic concurrency control](#optimistic-concurrency-control) is activated, retrying a successful append operation will return without failing due to the previous success. When optimistic concurrent control is [fully or partially disabled](#optimistic-concurrency-control), a successful append operation will return without appending duplicate events. Without KurrentDB's idempotent append behavior, a client would need to probe the database to determine whether an apparently failed request had actually succeeded. This behavior depends on events having unique event IDs, which is the default when constructing [`NewEvent`](#new-events) objects. Please note, KurrentDB does not enforce unique event IDs. The idempotent append behaviour does not protect against recording more than one event with the same ID, for example by appending an event with the same ID in a different stream, or in the same stream when specifying correctly the position of the last recorded event, or in the same stream at a much later time when disabling concurrency controls. Here are some examples showing previous operations succeeding idempotently. ::: tabs @tab sync ```python:no-line-numbers # Check the stream has exactly three events. assert len(client.get_stream("order-123")) == 3 # Retry order created - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Retry payment received - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=0, events=[payment_received], ) # Retry product shipped - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=1, events=[product_shipped], ) # Check the stream has exactly three events. assert len(client.get_stream("order-123")) == 3 ``` @tab async ```python:no-line-numbers # Check the stream has exactly two events. assert len(await client.get_stream("order-123")) == 3 # Retry appending first event - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Retry appending second event - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=0, events=[payment_received], ) # Retry appending third event - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=1, events=[product_shipped], ) # Check the stream has exactly three events. assert len(await client.get_stream("order-123")) == 3 ``` ::: Here are some examples showing idempotent append behavior when optimistic concurrency controls have been either fully or partially disabled. Duplicate events are not recorded: the steam still has exactly two events. ::: tabs @tab sync ```python:no-line-numbers # Fully disabled concurrency control - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[order_created], ) # Partially disabled concurrency control - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=StreamState.EXISTS, events=[payment_received], ) # Partially disabled concurrency control - succeeds idempotently. client.append_to_stream( stream_name="order-123", current_version=StreamState.EXISTS, events=[product_shipped], ) # Check the stream has exactly three events. assert len(client.get_stream("order-123")) == 3 ``` @tab async ```python:no-line-numbers # Fully disabled concurrency control - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[order_created], ) # Partially disabled concurrency control - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=StreamState.EXISTS, events=[payment_received], ) # Partially disabled concurrency control - succeeds idempotently. await client.append_to_stream( stream_name="order-123", current_version=StreamState.EXISTS, events=[product_shipped], ) # Check the stream has exactly three events. assert len(await client.get_stream("order-123")) == 3 ``` ::: ## Multi-Append to Stream ::: info Supported by KurrentDB 25.1 and later. ::: You can use the `multi_append_to_stream()` method to append new events to multiple streams. This method is atomic and [idempotent](#idempotent-append-behavior). Provide an `events` argument, an iterable of [`NewEvents`](#the-newevents-class) objects. Each specifies a stream name, a collection of [`NewEvent`](#new-events) objects to be appended to that stream, and an [optimistic concurrency control](#optimistic-concurrency-control) to be used when appending those events to that stream. | Parameter | Description | Default | |---------------|--------------------------------------------------------------------------------------------------------------|---------| | `events` | An iterable of [NewEvents](#the-newevents-class) objects. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `multi_append_to_stream()` returns the commit position (`int`) of the last event it appended. This value represents the event’s position in the global transaction log and can be used to ensure that eventually consistent views reflect the new events. ### The NewEvents Class Use the `NewEvents` dataclass when [appending events to multiple streams](#multi-append-to-stream). The fields of a `NewEvents` object specify a `stream_name`, the `events` to be appended to that stream, and a `current_version` value for [optimistic concurrency control](#optimistic-concurrency-control) of that stream. These fields have the same meaning as the corresponding parameters of [`append_to_stream()`](#append-to-stream). | Field | Type | Description | |-------------------|----------------------|------------------------------------------------------------------------| | `stream_name` | `str` | Stream to which new events will be appended. | | `events` | `Iterable[NewEvent]` | The [`NewEvent`](#new-events) objects to append to the stream. | | `current_version` | `int\|StreamState` | The [optimistic concurrency](#optimistic-concurrency-control) control | The fields of a `NewEvents` object are like the arguments of [`append_to_stream()`](#append-to-stream). Because [`multi_append_to_stream()`](#multi-append-to-stream) allows many such things in one call, many streams can be written to in one atomic operation. ### Metadata Restrictions When appending events with `multi_append_to_stream()`, the `metadata` field of each `NewEvent` must be either an empty `bytes` string or a `bytes` string containing a JSON object whose values are strings. The following metadata values are acceptable. | | Description | Examples | |---|--------------------------------|-----------------| | ✅ | Empty bytes | `b""` | | ✅ | JSON object with string values | `b'{"a": "1"}'` | The following metadata values are NOT acceptable and will cause a `ProgrammingError` exception. | | Description | Examples | |---|------------------------------------|-----------------------------------------------| | ❌ | Random bytes | `b'\xf5d\xc5W3^b\xb0(\xf9\x01D\x81\xa7Y\x98'` | | ❌ | JSON string | `b'"abcdef"'` | | ❌ | JSON object with non-string values | `b'{"a": 1}'` or `b'{"a": false}'` | | ❌ | Nested JSON objects | `b'{"a": {}}'` | ### Example The example below appends new events to two streams. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvents student_events = NewEvents( stream_name="student-123", events=[ NewEvent( type='StudentRegistered', data=b'{"name": "Joe"}' ), NewEvent( type='StudentJoinedCourse', data=b'{"course_id": "course-456"}' ), ], current_version=StreamState.NO_STREAM, ) course_events = NewEvents( stream_name="course-456", events=[ NewEvent( type='CourseCreated', data=b'{"name": "French"}' ), NewEvent( type='StudentJoinedCourse', data=b'{"student_id": "student-123"}' ), ], current_version=StreamState.NO_STREAM, ) client.multi_append_to_stream( events=[student_events, course_events], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvents student_events = NewEvents( stream_name="student-123", events=[ NewEvent( type='StudentRegistered', data=b'{"name": "Joe"}' ), NewEvent( type='StudentJoinedCourse', data=b'{"course_id": "course-456"}' ), ], current_version=StreamState.NO_STREAM, ) course_events = NewEvents( stream_name="course-456", events=[ NewEvent( type='CourseCreated', data=b'{"name": "French"}' ), NewEvent( type='StudentJoinedCourse', data=b'{"student_id": "student-123"}' ), ], current_version=StreamState.NO_STREAM, ) await client.multi_append_to_stream( events=[student_events, course_events], ) ``` ::: ## Get Stream Metadata You can use the `get_stream_metadata()` method to get [stream metadata](@server/features/streams.md#metadata-and-reserved-names). Provide a `stream_name` argument. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `stream_name` | Metadata for this stream will be returned. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `get_stream_metadata()` returns a Python `dict` of metadata keys and values for the named stream, along with the current version of the stream's metadata stream. If the named stream does not exist, the `dict` will be empty and the current version value will be `StreamState.NO_STREAM`. These two values can be used as arguments of `metadata` and `current_version` when calling [`set_stream_metadata()`](#set-stream-metadata). ### Example The example below gets metadata for stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers metadata, current_version = client.get_stream_metadata( stream_name="order-123", ) ``` @tab async ```python:no-line-numbers metadata, current_version = await client.get_stream_metadata( stream_name="order-123", ) ``` ::: ## Set Stream Metadata You can use the `set_stream_metadata()` method to set [stream metadata](@server/features/streams.md#metadata-and-reserved-names). Provide a `stream_name` argument, a Python `dict` of stream metadata keys and values, and optionally the current version of the stream's metadata stream. The named stream's metadata will be overwritten with the given `dict`. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------| | `stream_name` | Metadata for this stream will be updated. | | | `metadata` | A Python `dict` of stream metadata keys and values. | | | `current_version` | The [optimistic concurrency control](#optimistic-concurrency-control) for setting stream metadata. | `StreamState.ANY` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `set_stream_metadata()` returns `None`. If the named stream does not exist, the metadata will be set anyway. This allows streams to be configured before they are used. ### Example The example below sets metadata for stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers metadata["foo"] = "bar" client.set_stream_metadata( stream_name="order-123", metadata=metadata, current_version=current_version, ) metadata, _ = client.get_stream_metadata("order-123") assert metadata["foo"] == "bar" ``` @tab async ```python:no-line-numbers metadata["foo"] = "bar" await client.set_stream_metadata( stream_name="order-123", metadata=metadata, current_version=current_version, ) metadata, _ = await client.get_stream_metadata("order-123") assert metadata["foo"] == "bar" ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/connection-strings.md' --- # Connection Strings This guide explains the standardized connection string format used by all official KurrentDB clients. :::info For production services, ask your service provider for a valid connection string. ::: KurrentDB clients use a connection string to configure their connection to KurrentDB. ## Two Protocols KurrentDB connection strings support two protocols. * **`kurrentdb://`** for **connecting directly** to specific KurrentDB server endpoints. * **`kurrentdb+discover://`** for connecting using cluster discovery **via DNS A records**. With the `kurrentdb://` protocol you can specify one or many endpoints, separated by commas. If you specify only one endpoint, the client will **connect directly and remain with it**. If you specify many endpoints, the client will use them to query for cluster information and **pick an endpoint from the obtained cluster information** for continuing operations, according to the node preference specified by the connection string - see options below. This process will be repeated if the client detects that it needs to reconnect to the cluster. An "endpoint" can be a specified either as a host name or an IP address, with a port number. With the `kurrentdb+discover://` protocol you should specify a fully-qualified domain name of a KurrentDB cluster, with an optional port number. Using the cluster's **DNS A records**, the client will query for cluster information and pick an endpoint from the cluster information for continuing operations, according to the node preference specified by the connection string - see options below. This process will be repeated if the client detects that it needs to reconnect to the cluster. ## User Info Both the `kurrentdb://` and `kurrentdb+discover://` protocols support an optional user info string. If it exists, the user info string must be separated from the rest of the URI with the `"@"` character. The user info string must include a username and a password, separated with the `":"` character. The user info is sent by the client in a "basic auth" authorization header in each gRPC call to a "secure" server. This authorization header is used by the server to authenticate the client. The Python client does not allow call credentials to be transferred to "insecure" servers (option `tls=false`). ## Examples In the examples below, `user` is a username and `pass` is a password. For connecting directly to a single node: ```:no-line-numbers kurrentdb://user:pass@node1:2113 ``` For connecting to a cluster using specific endpoints to obtain cluster information: ```:no-line-numbers kurrentdb://user:pass@node1:2113,node2:2113,node3:2113 ``` For connecting to a cluster configured with DNS A records for the cluster endpoints: ```:no-line-numbers kurrentdb+discover://user:pass@cluster1:2113 ``` ## User Certificates To authenticate a client with an X.509 certificate, you need: * KurrentDB version 25.0+ [configured for user certificates](@server/security/user-authentication.html#user-x-509-certificates); and * A valid client certificate and private key. Then use the `userCertFile` and `userKeyFile` connection string options. Here's an example for connecting to KurrentDB with a client certificate. ```:no-line-numbers kurrentdb://node1:2113?userCertFile=user_cert.pem&userKeyFile=user_key.pem ``` ## Connection Options The table below describes optional query parameters that can be used in the connection string to configure the client. All option field names and values are case-insensitive. | Field name | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Set to `false` when connecting to KurrentDB running with "insecure" mode. | | `connectionName` | Any string | Random UUID | Connection name | | `maxDiscoverAttempts` | Integer | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Integer | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Integer | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | File system path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Integer | None | Maximum duration, in seconds, for completion of client operations. Can be overridden per operation using the `timeout` parameter of client methods. | | `keepAliveInterval` | Integer | None | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Integer | None | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | File system path | None | User certificate file for X.509 authentication. | | `userKeyFile` | File system path | None | Key file for the user certificate used for X.509 authentication. | --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/delete-stream.md' --- # Deleting Events This guide describes the Python client methods for deleting streams. ## Introduction In KurrentDB, you can delete events and streams either partially or completely. Stream [metadata settings](./appending-events.md#set-stream-metadata) like `$maxAge` and `$maxCount` help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. The Python clients have two methods for deleting streams: * `delete_stream()` – soft delete * `tombstone_stream()` – hard delete ## Delete Stream The `delete_stream()` method "soft deletes" a stream in KurrentDB. Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. While "soft delete" marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream to be "soft deleted". | | | `current_version` | The [optimistic concurrency control](./appending-events.md#optimistic-concurrency-control) for deleting a stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `delete_stream()` returns `None`. ### Example Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: Now let's "soft delete" the stream. ::: tabs @tab sync ```python:no-line-numbers # Get the current version of the stream current_version = client.get_current_version(stream_name="order-123") # Soft delete the stream client.delete_stream( stream_name="order-123", current_version=current_version ) ``` @tab async ```python:no-line-numbers # Get the current version of the stream current_version = await client.get_current_version(stream_name="order-123") # Soft delete the stream await client.delete_stream( stream_name="order-123", current_version=current_version ) ``` ::: ## Tombstone Stream The `tombstone_stream()` method "hard deletes" a stream in KurrentDB. Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream to be "hard deleted". | | | `current_version` | The [optimistic concurrency control](./appending-events.md#optimistic-concurrency-control) for deleting a stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `tombstone_stream()` returns `None`. ### Example ::: tabs @tab sync ```python:no-line-numbers # Get the current version of the stream current_version = client.get_current_version(stream_name="order-123") # Hard delete (tombstone) the stream client.tombstone_stream( stream_name="order-123", current_version=current_version ) print("Tombstoned stream: order-123") ``` @tab async ```python:no-line-numbers # Get the current version of the stream current_version = await client.get_current_version(stream_name="order-123") # Hard delete (tombstone) the stream await client.tombstone_stream( stream_name="order-123", current_version=current_version ) print("Tombstoned stream: order-123") ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/getting-started.md' --- # Getting Started This guide will help you get started with the Python clients for KurrentDB: * [Start KurrentDB locally](#running-kurrentdb-locally) * [Install the Python package](#installation) * [Client configuration](#client-configuration) * [Connect to KurrentDB](#connecting-to-kurrentdb) * [Create new events](#creating-new-events) * [Append events to streams](#appending-to-a-stream) * [Read streams](#reading-a-stream) ## Running KurrentDB Locally You can start KurrentDB with "insecure" mode in Docker by using the `--insecure` flag: ```bash:no-line-numbers docker run --name kurrentdb-node -it -p 2113:2113 \ docker.kurrent.io/kurrent-lts/kurrentdb:latest \ --insecure \ --run-projections=All \ --enable-atom-pub-over-http ``` Please read the server docs for more details about [KurrentDB installation](@server/quick-start/installation.html). ## Installation The `kurrentdbclient` Python package provides the official Python clients for KurrentDB. ### Install or Update Python Before installing the Python client for KurrentDB, ensure you’re using Python 3.10 or later. For information about how to get the latest version of Python, see the official [Python documentation](https://www.python.org/downloads/). ### Setup a Virtual Environment Once you have a supported version of Python installed, create a virtual environment and activate it: Create a virtual environment: ```bash:no-line-numbers python -m venv .venv ``` Activate the virtual environment: ```bash:no-line-numbers source .venv/bin/activate ``` ### Install the Package Install the [`kurrentdbclient`](https://pypi.org/project/kurrentdbclient/) Python package via pip: ```bash:no-line-numbers pip install "kurrentdbclient" ``` If your project requires a specific version, or has compatibility concerns with certain versions, you may provide constraints when installing: ```bash:no-line-numbers pip install "kurrentdbclient~=1.2" ``` ## Python Clients for KurrentDB The `kurrentdbclient` Python package provides sync and async clients for KurrentDB: * Sync client – **blocking** interface suitable for sequential code and multi-threaded apps * Async client – **asynchronous** interface suitable for high-concurrency applications These clients have been tested with KurrentDB versions 25.0, 25.1, 26.0, and 26.1, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS, in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12, 3.13, and 3.14. ## Client Configuration KurrentDB clients use a standardized [connection string](./connection-strings.md) to configure their connection to KurrentDB. When KurrentDB is [running locally](#running-kurrentdb-locally) with "insecure" mode, use a connection string with `tls=false`: ```python:no-line-numbers connection_string = "kurrentdb://127.0.0.1:2113?tls=false" ``` For production services, ask your service provider for a valid [connection string](./connection-strings.md). ## Connecting to KurrentDB To connect to KurrentDB, instantiate a [sync or async client](#python-clients-for-kurrentdb) with a [suitable connection string](#client-configuration). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient client = KurrentDBClient(connection_string) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient client = AsyncKurrentDBClient(connection_string) ``` ::: ## Creating New Events Use the [`NewEvent`](./appending-events.md#new-events) class to define new events with a `type` string and binary `data`. ```python:no-line-numbers from kurrentdbclient import NewEvent new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) ``` See the [`NewEvent`](./appending-events.md#new-events) documentation for more details. ## Appending to a Stream The Python client's [`append_to_stream()`](./appending-events.md#append-to-stream) method records new events in KurrentDB. When appending to a stream, specify a `stream_name`, the new [`events`](./appending-events.md#new-events) and a [`current_version`](./appending-events.md#optimistic-concurrency-control). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) client.append_to_stream( stream_name="order-123", events=[new_event], current_version=StreamState.NO_STREAM, ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) await client.append_to_stream( stream_name="order-123", events=[new_event], current_version=StreamState.NO_STREAM, ) ``` ::: See [Appending Events](./appending-events.md) for more information about writing to KurrentDB. ## Reading a Stream The Python client's [`get_stream()`](./reading-events.md#get-stream) method reads events from a named stream. ::: tabs @tab sync ```python:no-line-numbers for recorded_event in client.get_stream( stream_name="order-123" ): print("Stream name:", recorded_event.stream_name) print("Stream position:", recorded_event.stream_position) print("Commit position:", recorded_event.commit_position) print("Event type:", recorded_event.type) print("Event data:", recorded_event.data) print("Event ID:", recorded_event.id) ``` @tab async ```python:no-line-numbers for recorded_event in await client.get_stream( stream_name="order-123" ): print("Stream name:", recorded_event.stream_name) print("Stream position:", recorded_event.stream_position) print("Commit position:", recorded_event.commit_position) print("Event type:", recorded_event.type) print("Event data:", recorded_event.data) print("Event ID:", recorded_event.id) ``` ::: See [Reading Events](./reading-events.md) for more information about reading from KurrentDB. ## Overriding User Credentials You can use the `credentials` parameter of the Python client methods to override the [user info](./connection-strings.md#user-info) given in a client connection string. Use the `construct_call_credentials()` method to construct a `CallCredentials` object from a username and password. ::: tabs @tab sync ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[new_event], credentials=credentials, ) ``` @tab async ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[new_event], credentials=credentials, ) ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/observability.md' --- # Observability This guide explains how to instrument and export telemetry data from the Python clients. ## Introduction The Python client package provide [OpenTelemetry](https://opentelemetry.io) intrumentors. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support, for both the sync and async Python clients. ## Instrumenting a Client The Python client instrumentors depend on various OpenTelemetry Python packages, which you will need to install. ### Install Package To ensure verified version compatibility, install `kurrentdbclient` with the `opentelemetry` option. ```bash:no-line-numbers pip install kurrentdbclient[opentelemetry] ``` ### Activate Instrumentor You can then activate the client instrumentors within your application code. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.instrumentation.opentelemetry import ( KurrentDBClientInstrumentor, ) # Activate sync client instrumentation. KurrentDBClientInstrumentor().instrument() # Deactivate sync client instrumentation. KurrentDBClientInstrumentor().uninstrument() ``` @tab async ```python:no-line-numbers from kurrentdbclient.instrumentation.opentelemetry import ( AsyncKurrentDBClientInstrumentor, ) # Activate async client instrumentation. AsyncKurrentDBClientInstrumentor().instrument() # Deactivate async client instrumentation. AsyncKurrentDBClientInstrumentor().uninstrument() ``` ::: ## Exporting Telemetry Data In order to export telemetry data, you will need to initialise the global "tracer provider". ### Console Exporter For example, to export data to the console you will need to install the Python package `opentelemetry-sdk`, and use the class `TracerProvider`, `BatchSpanProcessor`, and `ConsoleSpanExporter` in the following way. ```python:no-line-numbers from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.trace import set_tracer_provider resource = Resource.create( attributes={ SERVICE_NAME: "kurrentdb", } ) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor( ConsoleSpanExporter() ) ) set_tracer_provider(provider) ``` ### OTLP Exporter To export data to an OpenTelemetry compatible data collector, such as [Jaeger](https://www.jaegertracing.io), you will need to install the Python package `opentelemetry-exporter-otlp-proto-http`, and then use the class `OTLPSpanExporter` from the `opentelemetry.exporter.otlp.proto.http.trace_exporter` module, with an appropriate `endpoint` argument for your collector. ```python:no-line-numbers from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.trace import set_tracer_provider resource = Resource.create( attributes={ SERVICE_NAME: "kurrentdb", } ) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces") ) ) set_tracer_provider(provider) ``` You can start Jaeger locally by running the following command. ```bash:no-line-numbers docker run --name jaeger -d -p 4318:4318 -p 16686:16686 \ jaegertracing/all-in-one:latest ``` Telemetry data from the client instrumentors can then be exported to `http://localhost:4318/v1/traces`. You can navigate to `http://localhost:16686` to access the Jaeger UI. You can find a list of available exporters for different platforms in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=python). For detailed configuration options, refer to the OpenTelemetry [Python documentation](https://opentelemetry.io/docs/languages/python/). ## Understanding Traces ### What Gets Traced At this time, the instrumented methods are [`append_to_stream()`](./appending-events.md#append-to-stream), [`multi_append_to_stream()`](./appending-events.md#multi-append-to-stream), [`subscribe_to_stream()`](./subscriptions.md#subscribe-to-stream), [`subscribe_to_all()`](./subscriptions.md#subscribe-to-all), [`read_subscription_to_stream()`](./persistent-subscriptions.md#read-subscription-to-stream), and [`read_subscription_to_all()`](./persistent-subscriptions.md#read-subscription-to-all). The append methods are instrumented by spanning the method call with a "producer" span. The subscription methods are instrumented by instrumenting the response iterators, creating a "consumer" span for each recorded event received. The producer spans add span context information to event metadata. The "consumer" spans extract this information from the recorded event metadata, and make each "consumer" span a child of a "producer" parent span. ### Producer Span Each span includes attributes to help with monitoring and debugging. Producer spans for [appending to a single stream](./appending-events.md#append-to-stream) have the following attributes: | Attribute | Description | Example | |------------------------------|----------------------------------------|---------------------| | db.operation | Type of operation performed | `"streams.append"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | db.kurrentdb.stream | Stream name or identifier | `"user-events-123"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | Producer spans for [appending to multiple streams](./appending-events.md#multi-append-to-stream) have the following attributes: | Attribute | Description | Example | |------------------------------|----------------------------------------|---------------------| | db.operation | Type of operation performed | `"streams.append"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | #### Example Here's an instrumentor span for a successful [`append_to_stream()`](./appending-events.md#append-to-stream) operation. ```json:no-line-numbers { "name": "streams.append", "context": { "trace_id": "0x82ac04990e711b6f35348556006fe4cf", "span_id": "0x9852ade35f00d350", "trace_state": "[]" }, "kind": "SpanKind.PRODUCER", "parent_id": null, "start_time": "2026-02-17T13:59:23.842871Z", "end_time": "2026-02-17T13:59:23.866696Z", "status": { "status_code": "OK" }, "attributes": { "db.operation": "streams.append", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.stream": "user-123", "server.address": "localhost", "server.port": "2113" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` ### Consumer Span Consumer spans have the following attributes. | Attribute | Description | Example | |------------------------------|----------------------------------------|------------------------------------------| | db.operation | Type of operation performed | `"streams.subscribe"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | db.kurrentdb.event.id | Event identifier | `"e7548b90-d79b-4474-b00f-631de4285acc"` | | db.kurrentdb.event.type | Event type identifier | `"AccountRegistered"` | | db.kurrentdb.stream | Stream name or identifier | `"user-123"` | | db.kurrentdb.subscription.id | Subscription identifier | `"user-123-subscription-1"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | #### Example Here's an instrumentor span from a [catch-up subscription](./subscriptions.md) operation. ```json:no-line-numbers { "name": "streams.subscribe", "context": { "trace_id": "0x5ad5e1bcff7f33cb44b93d470bd34554", "span_id": "0x446cf48b1bb9e574", "trace_state": "[]" }, "kind": "SpanKind.CONSUMER", "parent_id": "0x1496f8ba3507977b", "start_time": "2026-02-17T14:16:20.810515Z", "end_time": "2026-02-17T14:16:20.810605Z", "status": { "status_code": "OK" }, "attributes": { "db.operation": "streams.subscribe", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.event.id": "4ca26d3e-cbec-477e-9e59-d9248d8a3aef", "db.kurrentdb.event.type": "UserRegistered", "db.kurrentdb.stream": "user-123", "db.kurrentdb.subscription.id": "5da1a8c8-3dec-441e-8b6f-7514c797b1b4", "server.address": "localhost", "server.port": "2113" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` ### Span Errors Errors are traced by including a "span event" with the following attributes. | Attribute | Description | Example | |----------------------|---------------------------------------------------------|--------------------------------------------------------| | exception.type | Exception type if an error occurred | `"ServiceUnavailableError"` | | exception.message | Exception message if an error occurred | `"failed to connect to all addresses"` | | exception.stacktrace | Stack trace of the exception | `"Traceback (most recent call last):\n File..."` | | exception.escaped | Whether the exception is escaping the scope of the span | `"True"` | #### Example Here's an instrumentor span for an errorful [`append_to_stream()`](./appending-events.md#append-to-stream) operation. ```json:no-line-numbers { "name": "streams.append", "context": { "trace_id": "0xb99bba6da5c45dd2c72cca9f50064edd", "span_id": "0x31db5b46eac4a92e", "trace_state": "[]" }, "kind": "SpanKind.PRODUCER", "parent_id": null, "start_time": "2026-02-17T13:59:27.614387Z", "end_time": "2026-02-17T13:59:27.940788Z", "status": { "status_code": "ERROR", "description": "ServiceUnavailableError: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)" }, "attributes": { "db.operation": "streams.append", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.stream": "user-123", "server.address": "localhost", "server.port": "2113" }, "events": [ { "name": "exception", "timestamp": "2026-02-17T13:59:27.940712Z", "attributes": { "exception.type": "kurrentdbclient.exceptions.ServiceUnavailableError", "exception.message": "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)", "exception.stacktrace": "Traceback (most recent call last):\n File \"/venv/kurrentdbclient/client.py\", line 152, in retrygrpc_decorator\n return f(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/client.py\", line 140, in autoreconnect_decorator\n return f(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/client.py\", line 549, in append_to_stream\n return self.streams.batch_append(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/streams.py\", line 1331, in batch_append\n raise handle_rpc_error(e) from None\nkurrentdbclient.exceptions.ServiceUnavailableError: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)\n", "exception.escaped": "True" } } ], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/persistent-subscriptions.md' --- # Persistent Subscriptions This guide describes the Python client methods for persistent subscriptions. ## Introduction Persistent subscriptions are similar to [catch-up subscriptions](./subscriptions.md) with two key differences: * Persistent subscriptions are defined on the server and checkpoints are maintained by the server. This means that clients can reconnect to a persistent subscription and automatically receive unprocessed events. * It's possible to connect more than one consumer to the same persistent subscription. The server will send events to all connected consumers, according the choice of consumer strategy. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ### Creating Subscription Groups The first step is to create a new persistent subscription group. Admin permissions are required. The Python clients have two methods for creating a persistent subscription group: * [`create_subscription_to_stream()`](#create-subscription-to-stream) – create persistent subscription to a stream * [`create_subscription_to_all()`](#create-subscription-to-all) – create persistent subscription to global transaction log ### Consumer Strategies When creating a persistent subscription group, you can choose between a number of consumer strategies. #### DispatchToSingle (default) Distributes events to a single consumer until the buffer size is reached. After that, the next consumer is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. #### RoundRobin Distributes events to all consumers evenly. If the buffer size is reached, the consumer won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. #### Pinned For use with an indexing projection such as the system by-category projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual consumers. When a consumer connects, it is assigned some existing buckets. When a consumer disconnects, its buckets are assigned to other consumers. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### Consuming Events Consumers read from existing subscription groups. The server distributes events to consumers according to the subscription group's consumer strategy setting. The Python clients have two methods for reading from a subscription group: * [`read_subscription_to_stream()`](#read-subscription-to-stream) – start consuming events from a stream * [`read_subscription_to_all()`](#read-subscription-to-all) – start consuming events from the global transaction log These methods return a `PersistentSubscription` object. The `PersistenceSubscription` class is a Python iterable that returns [RecordedEvent](./reading-events.md#recorded-events) objects, and which has two methods, `ack()` and `nack()`, for acknowledging and negatively acknowledging received events. Consumers must use `ack()` or `nack()` to acknowledge or negatively acknowledge received events. ### Acknowledgements If processing is successful, a consumer should call `ack()` on its `PersistentSubscription` object, passing in the `RecordedEvent` object that was successfully consumed. This will pick the correct event ID to send to the server, letting the server know the message has been handled. | Parameter | Type | Description | |-----------|-------------------|------------------------------| | `item` | `RecordedEvent` | Successfully consumed events | #### Negative Acknowledgements If processing fails for some reason, the consumer should call `nack()` on its `PersistentSubscription` object, passing in both the `RecordedEvent` and a negative acknowledgement action. | Parameter | Type | Description | |-----------|-----------------|-------------------------------| | `item` | `RecordedEvent` | Unsuccessfully consumed event | | `action` | `str` | Name of action | The negative acknowledgement `action` describes what the server should do with the event. | Action | Description | |-----------|:----------------------------------------------------------------------| | `"park"` | Park the message and do not resend. Put it on poison queue. | | `"retry"` | Explicitly retry the message. | | `"skip"` | Skip this message do not resend and do not put in poison queue. | | `"stop"` | Stop the subscription. | ### Adjusting Group Settings You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. The Python clients have two methods for adjusting the settings of a persistent subscription group. * [`update_subscription_to_stream()`](#update-subscription-to-stream) – update settings for subscription to a stream * [`update_subscription_to_all()`](#update-subscription-to-all) – update settings for subscription to global transaction log ### Getting Subscription Info The Python clients have three methods for getting information about existing persistent subscriptions. * [`get_subscription_info()`](#get-subscription-info) – get information about a persistent subscription * [`list_subscriptions_to_stream()`](#list-subscriptions-to-stream) – get information about all persistent subscriptions to a stream * [`list_subscriptions()`](#list-subscriptions) – get information about all existing persistent subscriptions ### Deleting Subscription Groups Remove a subscription group with the delete operation. Like the creating and updating, you must have admin permissions to delete a persistent subscription group. The Python clients have one method for deleting a persistent subscription group. * [`delete_subscription()`](#delete-subscription) – delete subscription group ## Create Subscription to Stream Use `create_subscription_to_stream()` to create a group for consuming a stream. The persistent subscription can be created before the steam. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `from_end` | Whether to start the subscription from the end of the stream. | `False` | | `stream_position` | Position in stream from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `"DispatchToSingle"` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `30.0` | | `max_retry_count` | The maximum number of retries before a message will be parked. | `10` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `2.0` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `5` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to create a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(connection_string) # Create a persistent subscription to a specific stream client.create_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(connection_string) # Create a persistent subscription to a specific stream await client.create_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) ``` ::: ## Create Subscription to All Use `create_subscription_to_all()` to create a group for consuming the global transaction log. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| | `group_name` | Name of persistent subscription group. | | | `from_end` | Whether to start the subscription from the end of the stream. | `False` | | `commit_position` | Position in global transaction log from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name rather than event type. | `False` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `"DispatchToSingle"` | | `message_timeout` | The duration of time (in seconds) after which to consider a message as timed out and retried. | `30.0` | | `max_retry_count` | The maximum number of retries before a message will be parked. | `10` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `2.0` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `5` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to create a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.create_subscription_to_all( group_name="transaction-log-subscription", filter_include=["OrderCreated"], ) ``` @tab async ```python:no-line-numbers await client.create_subscription_to_all( group_name="transaction-log-subscription", filter_include=["OrderCreated"], ) ``` ::: ## Read Subscription to Stream Use `read_subscription_to_stream()` to start consuming events from a stream. | Parameter | Description | Default | |----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `event_buffer_size` | Number of events in consumer buffer. | `150` | | `max_ack_batch_size` | Number of acknowledgements before sending all to server. | `50` | | `max_ack_delay` | Amount of time (in seconds) before sending acknowledgements to server. | `0.2` | | `stopping_grace` | Amount of time (in seconds) to allow server to receive acknowledgements when consumer is stopping. | `0.2` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Returns a [`PersistentSubscription`](#consuming-events) object. #### Example Here's an example showing how to consume events from a subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState # Create a new stream with a new event order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Connect to a persistent subscription for a specific stream with client.read_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) as subscription: # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState # Create a new stream with a new event order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Connect to a persistent subscription for a specific stream async with await client.read_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) as subscription: # Process events and acknowledge them async for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing await subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") await subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` ::: ## Read Subscription to All Use `read_subscription_to_all()` to start consuming events from the global transaction log. | Parameter | Description | Default | |----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `event_buffer_size` | Number of events in consumer buffer. | `150` | | `max_ack_batch_size` | Number of acknowledgements before sending all to server. | `50` | | `max_ack_delay` | Amount of time (in seconds) before sending acknowledgements to server. | `0.2` | | `stopping_grace` | Amount of time (in seconds) to allow server to receive acknowledgements when consumer is stopping. | `0.2` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Returns a [`PersistentSubscription`](#consuming-events) object. #### Example Here's an example showing how to consume events from a subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Connect to a persistent subscription for all events with client.read_subscription_to_all( group_name="transaction-log-subscription" ) as subscription: # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Connect to a persistent subscription for all events async with await client.read_subscription_to_all( group_name="transaction-log-subscription" ) as subscription: # Process events and acknowledge them async for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing await subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") await subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` ::: ## Update Subscription to Stream Use `update_subscription_to_stream()` to adjust a group consuming from a stream. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `from_end` | Whether to start the subscription from the end of the stream. | `None` | | `stream_position` | Position in stream from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `None` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `None` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `None` | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `None` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `None` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `None` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `None` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `None` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `None` | | `read_batch_size` | The number of events read at a time when paging through history. | `None` | | `history_buffer_size` | The number of events to cache when paging through history. | `None` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to update a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.update_subscription_to_stream( group_name="stream-subscription", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) ``` @tab async ```python:no-line-numbers await client.update_subscription_to_stream( group_name="stream-subscription", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) ``` ::: ## Update Subscription to All Use `update_subscription_to_all()` to adjust a group consuming from the global transaction log. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `from_end` | Whether to start the subscription from the end of the stream. | `None` | | `commit_position` | Position in global transaction log from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `None` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `None` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `None` | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `None` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `None` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `None` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `None` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `None` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `None` | | `read_batch_size` | The number of events read at a time when paging through history. | `None` | | `history_buffer_size` | The number of events to cache when paging through history. | `None` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Please note, filter settings cannot be updated. #### Example Here's an example showing how to update a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.update_subscription_to_all( group_name="transaction-log-subscription", resolve_links=True, min_checkpoint_count=20 ) ``` @tab async ```python:no-line-numbers await client.update_subscription_to_all( group_name="transaction-log-subscription", resolve_links=True, min_checkpoint_count=20 ) ``` ::: ## Get Subscription Info Use `get_subscription_info()` to get a [`SubscriptionInfo`](#subscription-info) object for a persistent subscription group. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream (optional). | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Examples Here's an example showing how to get subscription information for a subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.get_subscription_info( group_name="stream-subscription", stream_name="order-123", ) ``` @tab async ```python:no-line-numbers await client.get_subscription_info( group_name="stream-subscription", stream_name="order-123", ) ``` ::: Here's an example showing how to get subscription information for a subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.get_subscription_info( group_name="transaction-log-subscription", ) ``` @tab async ```python:no-line-numbers await client.get_subscription_info( group_name="transaction-log-subscription", ) ``` ::: ## List Subscriptions to Stream Use `list_subscriptions_to_stream()` to return a list of [`SubscriptionInfo`](#subscription-info) objects describing persistent subscriptions to a named stream. | Parameter | Description | Default | |------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `stream_name` | Name of stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to get information for all persistent subscriptions to a stream. ::: tabs @tab sync ```python:no-line-numbers client.list_subscriptions_to_stream(stream_name="order-123") ``` @tab async ```python:no-line-numbers await client.list_subscriptions_to_stream(stream_name="order-123") ``` ::: ## List Subscriptions Use `list_subscriptions()` to return a list of [`SubscriptionInfo`](#subscription-info) objects describing all existing persistent subscriptions. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to get information for all existing persistent subscriptions. ::: tabs @tab sync ```python:no-line-numbers client.list_subscriptions() ``` @tab async ```python:no-line-numbers await client.list_subscriptions() ``` ::: ## Delete Subscription Use `delete_subscription()` to permanently delete a persistent subscription group. | Parameter | Description | Default | |----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream (optional). | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Examples Here's an example showing how to delete a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.delete_subscription( group_name="stream-subscription", stream_name="order-123" ) ``` @tab async ```python:no-line-numbers await client.delete_subscription( group_name="stream-subscription", stream_name="order-123" ) ``` ::: Here's an example showing how to delete a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.delete_subscription( group_name="transaction-log-subscription" ) ``` @tab async ```python:no-line-numbers await client.delete_subscription( group_name="transaction-log-subscription" ) ``` ::: ## Subscription Info The `SubscriptionInfo` objects returned by [`get_subscription_info()`](#get-subscription-info), [`list_subscriptions_to_stream()`](#list-subscriptions-to-stream), and [`list_subscriptions()`](#list-subscriptions) have the following fields. | Field | Type | |------------------------------------|------------------------------------------------------------------------------| | `event_source` | `str` | | `group_name` | `str` | | `status` | `str` | | `average_per_second` | `int` | | `total_items` | `int` | | `count_since_last_measurement` | `int` | | `last_checkpointed_event_position` | `str` | | `last_known_event_position` | `str` | | `resolve_links` | `bool` | | `start_from` | `str` | | `message_timeout` | `float` | | `extra_statistics` | `bool` | | `max_retry_count` | `int` | | `live_buffer_size` | `int` | | `history_buffer_size` | `int` | | `read_batch_size` | `int` | | `checkpoint_after` | `float` | | `min_checkpoint_count` | `int` | | `max_checkpoint_count` | `int` | | `read_buffer_count` | `int` | | `live_buffer_count` | `int` | | `retry_buffer_count` | `int` | | `total_in_flight_messages` | `int` | | `outstanding_messages_count` | `int` | | `consumer_strategy` | `Literal["DispatchToSingle", "RoundRobin", "Pinned", "PinnedByCorrelation"]` | | `max_subscriber_count` | `int` | | `parked_message_count` | `int` | | `connections` | `list[ConnectionInfo]` | The `ConnectionInfo` objects included in the `connections` field of `SubscriptionInfo` have the following fields. | Field | Type | |--------------------------------|---------------------| | `from_` | `str` | | `username` | `str` | | `average_items_per_second` | `int` | | `total_items` | `int` | | `count_since_last_measurement` | `int` | | `observed_measurements` | `list[Measurement]` | | `available_slots` | `int` | | `in_flight_messages` | `int` | | `connection_name` | `str` | The `Measurement` objects included in the `observed_measurements` field of `ConnectionInfo` have the following fields. | Field | Type | |-----------|---------| | `key` | `str` | | `value` | `int` | --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/projections.md' --- # Projections This guide describes the Python client methods for working with [projections](@server/features/projections.md) in KurrentDB. ::: tip Projections require [event data](./appending-events.md#new-events) to be JSON. ::: ## Introduction KurrentDB has a [projections subsystem](@server/features/projections.md) that lets you append new events or link existing events to streams in a reactive manner. The Python client has twelve methods for working with projections: * [`create_projection()`](#create-projection) * [`get_projection_state()`](#get-projection-state) * [`disable_projection()`](#disable-projection) * [`update_projection()`](#update-projection) * [`reset_projection()`](#reset-projection) * [`enable_projection()`](#enable-projection) * [`get_projection_statistics()`](#get-projection-statistics) * [`list_continuous_projection_statistics()`](#list-continuous-projection-statistics) * [`list_all_projection_statistics()`](#list-all-projection-statistics) * [`abort_projection()`](#abort-projection) * [`delete_projection()`](#delete-projection) * [`restart_projections_subsystem()`](#restart-projections-subsystem) Let's get started by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false&defaultDeadline=5" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false&defaultDeadline=5" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: ## Create Projection Use the `create_projection()` method to create a "continuous" projection with a Javascript "query". The `emit_enabled` argument must be `True` if the `query` code includes a call to `.emit()` otherwise the projection will not run. If `track_emitted_streams` is `True` then any emitted emitted streams can be optionally deleted when a projection is deleted. See [`delete_projection()`](#delete-projection) for more details. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `query` | Javascript projection code, defines what the projection will do. | | | `emit_enabled` | Whether a projection will be able to emit events. | `False` | | `track_emitted_streams` | Whether emitted streams are tracked. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns `None`. ### Example The Javascript code below will project the stream named `"order-123"`. It will: * initialise the projection's current state to a Javascript object with "count" and "list" values; and * for each appended `OrderCreated` event in the projected stream: * increment "count" value; and * and emit an `Emitted` event to stream `"emitted-order-123"`. ```python:no-line-numbers projection_query = """ fromStream("order-123") .when({ $init: function(){ return { count: 0, list: [null, "2.10", true] }; }, OrderCreated: function(s,e){ s.count += 1; emit("emitted-order-123", "Emitted", {}, {}); } }) .outputState() """ ``` Now let's create a projection that uses this query. ::: tabs @tab sync ```python:no-line-numbers client.create_projection( name="projection-order-123", query=projection_query, emit_enabled=True, track_emitted_streams=True, ) ``` @tab async ```python:no-line-numbers await client.create_projection( name="projection-order-123", query=projection_query, emit_enabled=True, track_emitted_streams=True, ) ``` ::: ## Get Projection State Use the `get_projection_state()` method to get a projection's current state. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `partition` | Projection partition (optional). | `""` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success this method returns a `ProjectionState` object with a `value` attribute that corresponds to the current state of the projection. In the example below, the current state of the projection is a Python `dict` that has a `"count"` value and a `"list"` value. These values correspond to the projection query and events from the [previous example](#create-projection). ::: tabs @tab sync ```python:no-line-numbers from time import sleep sleep(1) state = client.get_projection_state("projection-order-123") assert 1 == state.value["count"] assert [None, "2.10", True] == state.value["list"] ``` @tab async ```python:no-line-numbers from time import sleep sleep(1) state = await client.get_projection_state("projection-order-123") assert 1 == state.value["count"] assert [None, "2.10", True] == state.value["list"] ``` ::: ## Disable Projection Use the `disable_projection()` method to stop a projection processing new events. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. The example below stops the `"order-123""` projection. ::: tabs @tab sync ```python:no-line-numbers client.disable_projection(name="projection-order-123") ``` @tab async ```python:no-line-numbers await client.disable_projection(name="projection-order-123") ``` ::: ## Update Projection Use the `update_projection()` method to adjust the projection query. If `query` includes a call to `.emit()`, the `emit_enabled` argument must be `True`, otherwise the projection will not run. A projection must be disabled before it can be updated. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `query` | Javascript projection code, defines what the projection will do. | | | `emit_enabled` | Whether a projection will be able to emit events. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.update_projection( name="projection-order-123", query=projection_query, emit_enabled=True, ) ``` @tab async ```python:no-line-numbers await client.update_projection( name="projection-order-123", query=projection_query, emit_enabled=True, ) ``` ::: ## Reset Projection Use the `reset_projection()` method to reset the current state of a projection. A projection must be disabled before it can be reset. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.reset_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.reset_projection( name="projection-order-123", ) ``` ::: ## Enable Projection Use the `enable_projection()` method to start a projection that has been disabled. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.enable_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.enable_projection( name="projection-order-123", ) ``` ::: ## Get Projection Statistics Use the `get_projection_statistics()` method to get statistics for a projection. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a [`ProjectionStatistics`](#the-projectionstatistics-class) object. ::: tabs @tab sync ```python:no-line-numbers statistics = client.get_projection_statistics( name="projection-order-123", ) assert "Running" == statistics.status ``` @tab async ```python:no-line-numbers statistics = await client.get_projection_statistics( name="projection-order-123", ) assert "Running" == statistics.status ``` ::: ## List Continuous Projection Statistics Use the `list_continuous_projection_statistics()` method to get a list of statistics for all continuous projections. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a list of [`ProjectionStatistics`](#the-projectionstatistics-class) objects. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.projections import ProjectionStatistics statistics = client.list_continuous_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` @tab async ```python:no-line-numbers from kurrentdbclient.projections import ProjectionStatistics statistics = await client.list_continuous_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` ::: ## List All Projection Statistics Use the `list_all_projection_statistics()` method to get a list of statistics for all projections. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a list of [`ProjectionStatistics`](#the-projectionstatistics-class) objects. ::: tabs @tab sync ```python:no-line-numbers statistics = client.list_all_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` @tab async ```python:no-line-numbers statistics = await client.list_all_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` ::: ## Abort Projection Use the `abort_projection()` method to abort a projection. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.abort_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.abort_projection( name="projection-order-123", ) ``` ::: ## Delete Projection Use the `delete_projection()` method to delete a projection. A projection must be disabled before it can be deleted. Attempting to delete a projection that is running will raise an `OperationFailedError` exception. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.disable_projection(name="projection-order-123") client.delete_projection( "projection-order-123", delete_emitted_streams=True, delete_state_stream=True, delete_checkpoint_stream=True, ) ``` @tab async ```python:no-line-numbers await client.disable_projection(name="projection-order-123") await client.delete_projection( "projection-order-123", delete_emitted_streams=True, delete_state_stream=True, delete_checkpoint_stream=True, ) ``` ::: On success, this method will return `None`. ## Restart Projections Subsystem Use the `restart_projections_subsystem()` method to restart the projections subsystem. | Parameter | Description | Default | |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.restart_projections_subsystem() ``` @tab async ```python:no-line-numbers await client.restart_projections_subsystem() ``` ::: ## The ProjectionStatistics Class The `ProjectionStatistics` dataclass is defined with the following fields: | Field | Type | Description | |--------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `core_processing_time` | `int` | The total time, in ms, the projection took to handle events since the last restart. | | `version` | `int` | This is used internally, the version is increased when the projection is edited or reset. | | `epoch` | `int` | This is used internally, the epoch is increased when the projection is reset. | | `effective_name` | `str` | The name of the projection. | | `writes_in_progress` | `int` | The number of write requests to emitted streams currently in progress, these writes can be batches of events. | | `reads_in_progress` | `int` | The number of read requests currently in progress. | | `partitions_cached` | `int` | The number of cached projection partitions. | | `status` | `str` | A human readable string of the current statuses of the projection (see below). | | `state_reason` | `str` | A human readable string explaining the reason of the current projection state. | | `name` | `str` | The name of the projection. | | `mode` | `str` | `Continuous`, `OneTime` , `Transient` | | `position` | `str` | The position of the last processed event. | | `progress` | `float` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed. | | `last_checkpoint` | `str` | The position of the last checkpoint of this projection. | | `events_processed_after_restart` | `int` | The number of events processed since the last restart of this projection. | | `checkpoint_status` | `str` | A human readable string explaining the current operation performed on the checkpoint: `requested`, `writing`. | | `buffered_events` | `int` | The number of events in the projection read buffer. | | `write_pending_events_before_checkpoint` | `int` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written. | | `write_pending_events_after_checkpoint` | `int` | The number of events to be appended to emitted streams since the last checkpoint. | The `status` string is a combination of the following values. The first three are the most common one, as the other one are transient values while the projection is initialised or stopped. | Value | Description | |---------------------|------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events. | | Stopped | The projection is stopped and is no longer processing new events. | | Faulted | An error occurred in the projection, StateReason will give the fault details, the projection is not processing events. | | Initial | This is the initial state, before the projection is fully initialised. | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection. | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting. | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting. | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting. | | FaultedStopping | This happens before the projection is stopped due to an error in the projection. | | Stopping | The projection is being stopped. | | CompletingPhase | This happens while the projection is stopping. | | PhaseCompleted | This happens while the projection is stopping. | --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/reading-events.md' --- # Reading Events This guide describes the Python client methods for reading events from KurrentDB. ## Introduction Recorded events can be read from a [named stream](#read-stream), from the [global transaction log](#read-all), and from [secondary indexes](#read-index). The Python clients for KurrentDB have four methods for reading events: * [`get_stream()`](#get-stream) – returns a Python `tuple` of events from a named stream * [`read_stream()`](#read-stream) – returns a streaming iterable of events from a named stream * [`read_all()`](#read-all) – returns a streaming iterable of events from global transaction log * [`read_index()`](#read-index) – returns a streaming iterable of events from a secondary index ## Recorded Events The Python client for KurrentDB uses the `RecordedEvent` class when presenting recorded events. A `RecordedEvent` object specifies the type string, binary data, metadata, content type, and ID of a [new event](./appending-events.md#new-events) that has been recorded. Additionally, it specifies the event's stream name and stream position, the commit and prepare position, the recorded time, and possibly a link event and a persistent subscription consumer group retry count. | Field | Type | Description | |--------------------|-----------------------|---------------------------------------------------------------------------------------| | `type` | `str` | The type of the event | | `data` | `bytes` | The content of the event | | `metadata` | `bytes` | Event metadata | | `content_type` | `str` | The format of the content | | `id` | `UUID` | A unique ID for the event | | `stream_name` | `str` | A unique ID for the event | | `stream_position` | `int` | Position of the event in the stream | | `commit_position` | `int` | Position of the event in the global transaction log | | `recorded_at` | `datetime\|None` | Timestamp added by KurrentDB | | `link` | `RecordedEvent\|None` | Resolved link event | | `retry_count` | `int\|None` | Number of times this event has been sent to a persistence subscription consumer group | You will never need to construct a `RecordedEvent` object. However, all events returned from KurrentDB by the Python clients are presented as `RecordedEvent` objects, and so it is important to understand these fields. ## Get Stream Use the `get_stream()` method to get a `tuple` of events from a stream in KurrentDB. You can get all the events or a sample of the events from an individual stream, starting from any position in the stream, either forwards or backwards. This is a convenient alternative to [`read_stream()`](#read-stream) that returns a Python `tuple` collection. | Parameter | Description | Default | |-------------------|--------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `get_stream()` returns a `tuple` of [`RecordedEvent`](#recorded-events) objects. ### Examples Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: ### Reading Forwards The simplest way to get stream events is to supply a `stream_name` argument. This is a typical operation when retrieving events to construct a decision model in a command handler. Here's an example of getting all events from stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers for event in client.get_stream(stream_name="order-123"): print(f"Event: {event.type} at position {event.stream_position}") ``` @tab async ```python:no-line-numbers for event in await client.get_stream(stream_name="order-123"): print(f"Event: {event.type} at position {event.stream_position}") ``` ::: ### Reading Backwards Set the `backwards` parameter to `True` to get stream events in reverse order. ::: tabs @tab sync ```python:no-line-numbers # Get all events backwards from the end for event in client.get_stream( stream_name="order-123", backwards=True, ): assert event.stream_position == 2 break ``` @tab async ```python:no-line-numbers # Get all events backwards from the end for event in await client.get_stream( stream_name="order-123", backwards=True, ): assert event.stream_position == 2 break ``` ::: :::tip Get stream event backwards with a limit of `1` to find the last position in the stream. Alternatively, call the more convenient method `get_current_version()`. ::: ### Limited Number Passing in a `limit` argument allows you to restrict the number of events that are returned. In the example below, we read a maximum of two events from the stream: ::: tabs @tab sync ```python:no-line-numbers events = client.get_stream( stream_name="order-123", limit=2 ) assert len(events) == 2 ``` @tab async ```python:no-line-numbers events = await client.get_stream( stream_name="order-123", limit=2 ) assert len(events) == 2 ``` ::: ### From Stream Position Specifying a `stream_position` argument will get events from a specific position. This is useful, for example, when advancing a snapshot of an aggregate to the latest current state. Getting stream events from a specific position is inclusive, which means the event at that position will be returned by the response. ::: tabs @tab sync ```python:no-line-numbers # Get events from a specific stream position for event in client.get_stream( stream_name="order-123", stream_position=1, ): assert event.stream_position == 1 break ``` @tab async ```python:no-line-numbers # Get events from a specific stream position for event in await client.get_stream( stream_name="order-123", stream_position=1, ): assert event.stream_position == 1 break ``` ::: ### Resolving Link Events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers for event in client.get_stream( stream_name="$et-OrderCreated", resolve_links=True ): assert event.type == "OrderCreated" ``` @tab async ```python:no-line-numbers for event in await client.get_stream( stream_name="$et-OrderCreated", resolve_links=True ): assert event.type == "OrderCreated" ``` ::: ### Not Found Error Reading a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: await client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Read Stream Use the `read_stream()` method to read events from a stream in KurrentDB. You can read all the events or a sample of the events from an individual stream, starting from any position in the stream, and can read either forwards or backwards. Alternatively, use [`get_stream()`](#get-stream) to get a Python `tuple` collection of events, rather then a streaming iterable response. | Parameter | Description | Default | |-------------------|------------------------------------------------------------------------------------------|---------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `read_stream()` returns an iterable of `RecordedEvent` objects. Please note, a `NotFoundError` exception will be raised if the stream does not exist. ### Reading Forwards The simplest way to read a stream is to supply a `stream_name` argument and read every event already recorded in that stream. This is a typical operation when retrieving events to construct a decision model in a command handler. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream(stream_name="order-123") as events: for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` @tab async ```python:no-line-numbers async with await client.read_stream(stream_name="order-123") as events: async for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` ::: ### Reading Backwards Set `backwards=True` to read stream events in reverse order. ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end with client.read_stream( stream_name="order-123", backwards=True, ) as events: for event in events: assert event.stream_position == 2 break ``` @tab async ```python:no-line-numbers # Read all events backwards from the end async with await client.read_stream( stream_name="order-123", backwards=True, ) as events: async for event in events: assert event.stream_position == 2 break ``` ::: :::tip Read backwards with a limit of `1` to find the last position in the stream. Alternatively, call the convenience Python client method `get_current_version()`. ::: ### Limited Number Passing in a `limit` argument allows you to restrict the number of events that are returned. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream( stream_name="order-123", limit=2 ) as events: assert len(tuple(events)) == 2 ``` @tab async ```python:no-line-numbers async with await client.read_stream( stream_name="order-123", limit=2 ) as events: assert len([e async for e in events]) == 2 ``` ::: ### From Stream Position Specifying a `stream_position` argument will start reading from a specific position in the stream. This is useful, for example, when advancing a snapshot of an aggregate to the latest current state. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific stream position with client.read_stream( stream_name="order-123", stream_position=1, ) as events: for event in events: assert event.stream_position == 1 break ``` @tab async ```python:no-line-numbers # Read from a specific stream position async with await client.read_stream( stream_name="order-123", stream_position=1, ) as events: async for event in events: assert event.stream_position == 1 break ``` ::: Please note, reading a stream from a specific position is inclusive, which means the event at that position will be returned by the response. ### Resolving Link Events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream( stream_name="$et-OrderCreated", resolve_links=True ) as events: for event in events: assert event.type == "OrderCreated" ``` @tab async ```python:no-line-numbers async with await client.read_stream( stream_name="$et-OrderCreated", resolve_links=True ) as events: async for event in events: assert event.type == "OrderCreated" ``` ::: ### Not Found Error Reading a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: with client.read_stream( stream_name="not-a-stream" ) as events: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: async with await client.read_stream( stream_name="not-a-stream" ) as events: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Read All Use the `read_all()` method to read events from the global transaction log. No arguments are required when reading from the global transaction log. You can start from a particular commit position, read events backwards, and read a limited number of events. You can also filter events by type string or stream name. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| | `commit_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name rather than event type. | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `read_all()` returns an iterable of `RecordedEvent` objects. ### Server-Side Filtering KurrentDB supports server-side filtering of events while reading from, or subscribing to, the global transaction log, so that you can receive only the events you care about. You can filter by event type or stream name using regular expressions. The `filter_include` and `filter_exclude` parameters are designed to have exactly the opposite effect from each other, so that a sequence of strings given to `filter_include` will return exactly those events which would be excluded if the same argument value were used with `filter_exclude`. And vice versa, so that a sequence of strings given to `filter_exclude` will return exactly those events that would not be included if the same argument value were used with `filter_include`. The `filter_include` parameter takes precedence over `filter_exclude`. That is to say, if you pass arguments for both, the `filter_exclude` argument will be ignored. The `filter_include` and `filter_exclude` parameters are typed as `Sequence[str]` which means that you can either pass a single `str`, or a collection of `str`. The `str` value or values should be unanchored regular expression patterns. If you supply a collection of `str`, they will be concatenated together by the Python client as bracketed alternatives in a larger regular expression that is anchored to the start and end of the strings being matched. So there is no need to include the `'^'` and `'$'` anchor assertions. KurrentDB generates "system events" that all have a `type` that begins with `"$"`. By default, system events are excluded, along with `PersistentConfig` and `Result` events. If you want to also exclude other types of events, then use an argument for `filter_exclude` that adds to the default argument value `DEFAULT_EXCLUDE_FILTER`. If you especially want to include system events, then you can override the default filter by passing an empty sequence as the `filter_exclude` argument. If you want to select only for system events, then specify a suitable `filter_include` argument. You should use wildcards if you want to match substrings. For example, `"Order.*"` matches all strings that start with `"Order"`. Alternatively,`".*Snapshot"` matches all strings that end with `"Snapshot"`. Characters that are metacharacters with special meaning in regular expressions, such as `.` `*` `+` `?` `^` `$` `|` `(` `)` `[` `]` `{` `}` `\` must be escaped to be used literally when matching event types and stream names. Python's raw string literals can help to avoid doubling of escape backslashes. For example `r"\$.*"` can be used to match system event types that all start with the `$` character. ### Reading Forwards The simplest way to read events from the global transaction log is to call `read_all()` without arguments. ::: tabs @tab sync ```python:no-line-numbers # Read all events from the beginning with client.read_all() as events: # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` @tab async ```python:no-line-numbers # Read all events from the beginning async with await client.read_all() as events: # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` ::: ### Reading Backwards Set `backwards=True` to read the global transaction log backwards from the end. ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end with client.read_all(backwards=True) as events: ... # Read backwards from a specific commit position with client.read_all( commit_position=commit_position, backwards=True, ) as events: ... ``` @tab async ```python:no-line-numbers # Read all events backwards from the end async with await client.read_all(backwards=True) as events: ... # Read backwards from a specific commit position async with await client.read_all( commit_position=commit_position, backwards=True, ) as events: ... ``` ::: :::tip Read one event backwards to find the last position in the global transaction log. Alternatively, call the more convenient Python client method `get_commit_position()`. ::: ### Limited Number Passing in a `limit` allows you to restrict the number of events that are returned. In the example below, we read a maximum of 100 events: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( limit=100 ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( limit=100 ) as events: ... ``` ::: ### From Commit Position You can also start reading from a specific position in the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific commit position with client.read_all( commit_position=commit_position ) as events: ... ``` @tab async ```python:no-line-numbers # Read from a specific commit position async with await client.read_all( commit_position=commit_position ) as events: ... ``` ::: Please note, an `InvalidCommitPositionError` exception will be raised if the commit position does not exist. ### Resolving Link Events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.read_all( resolve_links=True ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( resolve_links=True ) as events: ... ``` ::: ### Filtering Examples You can read more selectively from the global transaction log with [server-side filtering](#server-side-filtering) by supplying an argument for either the `filter_include` or the `filter_exclude` parameters. By default, events will be filtered by `type`. Alternatively, you can filter events by `stream_name` name by setting the `filter_by_stream_name` parameter to `True`. Here's an example that reads all events that have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_include=["Order.*"] ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_include=["Order.*"] ) as events: ... ``` ::: Here's an example that selects all events that do not have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_exclude=["Order.*"] ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_exclude=["Order.*"] ) as events: ... ``` ::: Here's an example that selects all events that have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_include=["order.*"], filter_by_stream_name=True, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_include=["order.*"], filter_by_stream_name=True, ) as events: ... ``` ::: Here's an example that selects all events that do not have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_exclude=["order.*"], filter_by_stream_name=True, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_exclude=["order.*"], filter_by_stream_name=True, ) as events: ... ``` ::: ## Read Index ::: info Supported by KurrentDB 25.1 and later. ::: Use the `read_index()` method to read events from a secondary index in KurrentDB. You can read events from a secondary index starting from any commit position. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `index_name` | Name of secondary index (`"$idx-"` prefix is optional). | | | `commit_position` | Position from which to start reading events. | `None` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `read_index()` returns an iterable of `RecordedEvent` objects. ### Reading Forwards The simplest way to read from a secondary index is to call `read_index()` with the name of an index. Here's an example of reading all events with type string `"OrderCreated"`. ::: tabs @tab sync ```python:no-line-numbers # Read all OrderCreated events with client.read_index( index_name="et-OrderCreated" ) as events: # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` @tab async ```python:no-line-numbers # Read all OrderCreated events async with await client.read_index( index_name="et-OrderCreated" ) as events: # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` ::: ### From Commit Position You can also start reading a secondary index from a specific position in the global transaction log. Here's an example of reading a secondary index from a specific commit position. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific commit position with client.read_index( index_name="et-OrderCreated", commit_position=commit_position, ) as events: for event in events: break ``` @tab async ```python:no-line-numbers # Read from a specific commit position async with await client.read_index( index_name="et-OrderCreated", commit_position=commit_position, ) as events: async for event in events: break ``` ::: Please note, an `InvalidCommitPositionError` exception will be raised if the commit position does not exist. ### Limited Number Passing in a `limit` allows you to restrict the number of events that are returned. Here's an example of reading a maximum of 100 events from a secondary index. ::: tabs @tab sync ```python:no-line-numbers with client.read_index( index_name="et-OrderCreated", limit=100, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_index( index_name="et-OrderCreated", limit=100, ) as events: ... ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.2/subscriptions.md' --- # Catch-up Subscriptions This guide describes the Python client methods for catch-up subscriptions. ## Introduction Catchup-subscriptions are like the responses from the read methods, with one useful difference: they return [already-recorded events](./reading-events.md#recorded-events), and then continue as new events are subsequently recorded. You can subscribe to individual streams, to the global transaction log, and to a secondary indexes. The Python clients for KurrentDB have three methods for catch-up subscriptions. * [`subscribe_to_stream()`](#subscribe-to-stream) – returns a catch-up subscription to a stream * [`subscribe_to_all()`](#subscribe-to-all) - returns a catch-up subscription to global transaction log * [`subscribe_to_index()`](#subscribe-to-index) – returns a catch-up subscription to a secondary index ## Subscribe to Stream The `subscribe_to_stream()` method returns a catch-up subscription to a stream. The only required argument is the name of a stream. You can subscribe to all the events in a stream, or a sample of the events from the named stream, optionally starting after a specific stream position or the end of the stream. | Parameter | Description | Default | |-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position after which to start reading events. | `None` | | `from_end` | Read from the end of the stream (new events only). | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `include_caught_up` | Receive "caught up" messages when iterating the response. | `False` | | `include_fell_behind` | Receive "fell behind" messages when iterating the response. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `subscribe_to_stream()` returns an iterable of `RecordedEvent` objects. Please note, a `NotFoundError` exception will be raised if the stream does not exist. ### Examples Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the first event to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1], ) # Append second and third event to the same stream. client.append_to_stream( stream_name="order-123", current_version=0, events=[event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the first event to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1], ) # Append second and third event to the same stream. await client.append_to_stream( stream_name="order-123", current_version=0, events=[event2, event3], ) ``` ::: ### Basic Subscription The simplest way to subscribe to a stream is to supply a `stream_name` argument. ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in a stream (use context manager for auto-cleanup) with client.subscribe_to_stream(stream_name="order-123") as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: assert event.stream_position == 0 assert event.id == event1.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in a stream (use context manager for auto-cleanup) async with await client.subscribe_to_stream(stream_name="order-123") as subscription: # Iterate through the subscription with an 'async for' loop async for event in subscription: assert event.stream_position == 0 assert event.id == event1.id break # <-- so we can continue with the examples ``` ::: ### After Stream Position Specifying a `stream_position` argument will get events after that position. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific stream position with client.subscribe_to_stream( stream_name="order-123", stream_position=1, ) as subscription: for event in subscription: assert event.stream_position == 2 assert event.id == event3.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Get events after a specific stream position async with await client.subscribe_to_stream( stream_name="order-123", stream_position=1, ) as subscription: async for event in subscription: assert event.stream_position == 2 assert event.id == event3.id break # <-- so we can continue with the examples ``` ::: ### From End of Stream Here's an example of subscribing from the end of a stream for "live events" only. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_stream( stream_name="order-123", from_end=True, ) as subscription: ... ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_stream( stream_name="order-123", from_end=True, ) as subscription: ... ``` ::: ### Resolving Link Events When you subscribe to a stream with link events (e.g., category streams), set `resolve_links` to `True`. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_stream( stream_name="$et-OrderCreated", resolve_links=True ) as subscription: for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_stream( stream_name="$et-OrderCreated", resolve_links=True ) as subscription: async for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` ::: Link events point to events in other streams in KurrentDB. These are generally created by projections such as the by-event-type projection which links events of the same event type into the same stream. This makes it easy to look up all events of a specific type. However, it may be faster to use a [filtered subscription](#subscribe-to-all) for a specific type or stream name prefix than subscribing to the corresponding system projection. ### Stream Not Found Error Subscribing to a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: with client.subscribe_to_stream( stream_name="not-a-stream" ) as subscription: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: async with await client.subscribe_to_stream( stream_name="not-a-stream" ) as subscription: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Subscribe To All The `subscribe_to_all()` method returns a catch-up subscription to the global transaction log. A catch-up subscription to the global transaction log will return all events in chronological order. You can start after a particular commit position, or from the end of the log so that only new events are received. You can also filter events by type string or by stream name. See notes on [filtering the global transaction log](./reading-events.md#server-side-filtering). | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| | `commit_position` | Position after which to start reading events. | `None` | | `from_end` | Read from the end of the database (new events only). | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name (default is to filter by event type). | `False` | | `include_caught_up` | Receive "caught up" messages when iterating the response. | `False` | | `include_fell_behind` | Receive "fell behind" messages when iterating the response. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `subscribe_to_all()` returns an iterable of `RecordedEvent` objects. ### Examples Let's see how to use `subscribe_to_all()` by looking at some examples. ### Basic Subscription ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in global transaction log with client.subscribe_to_all() as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: print(f"Event: {event.type} at position {event.commit_position}") break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in global transaction log async with await client.subscribe_to_all() as subscription: # Iterate through the subscription with an async 'for' loop async for event in subscription: print(f"Event: {event.type} at position {event.commit_position}") break # <-- so we can continue with the examples ``` ::: ### After Commit Position Specifying a `commit_position` argument will get events after that position in the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific commit position with client.subscribe_to_all( commit_position=commit_position, ) as subscription: for event in subscription: assert event.id == event2.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Get events after a specific commit position async with await client.subscribe_to_all( commit_position=commit_position, ) as subscription: async for event in subscription: assert event.id == event2.id break # <-- so we can continue with the examples ``` ::: ### Live Events Only Here's an example of subscribing from the end of the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific stream position with client.subscribe_to_all(from_end=True) as subscription: ... ``` @tab async ```python:no-line-numbers # Get events after a specific stream position async with await client.subscribe_to_all(from_end=True) as subscription: ... ``` ::: ### Resolving Link Events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_all(resolve_links=True) as subscription: ... ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_all(resolve_links=True) as subscription: ... ``` ::: ### Filtering by Event Type Here's an example of filtering for certain event types. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_all( filter_include=["OrderCreated", "OrderUpdated"], ) as subscription: for event in subscription: assert event.type in ["OrderCreated", "OrderUpdated"] break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_all( filter_include=["OrderCreated", "OrderUpdated"], ) as subscription: async for event in subscription: assert event.type in ["OrderCreated", "OrderUpdated"] break # <-- so we can continue with the examples ``` ::: ### Filtering by Stream Name Here's an example of filtering for a stream category. ::: tabs @tab sync ```python:no-line-numbers # Filter by stream name prefix with client.subscribe_to_all( filter_include=["order-.*"], filter_by_stream_name=True ) as subscription: for event in subscription: assert event.stream_name.startswith("order") break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Filter by stream name prefix async with await client.subscribe_to_all( filter_include=['order-.*'], filter_by_stream_name=True ) as subscription: async for event in subscription: assert event.stream_name.startswith("order") break # <-- so we can continue with the examples ``` ::: ### Checkpointing When a catch-up subscription to the global transaction log is used to process events, you can checkpoint progress by recording the commit position of the last processed event. If you record commit positions in the same atomic database transaction as the results of processing an event, and with a uniqueness constraint, and you resume using the last recorded position, the processing of events from the global transaction log will immediately have "exactly once" semantics. If you are filtering the subscription, you can set `include_checkpoints=True` to cause KurrentDB occasionally to send the commit positions of events that have been excluded from the catch-up subscription, so that progress across large gaps can also be checkpointed. These emerge from the catch-up subscription as `Checkpoint` objects. Please note, occasionally KurrentDB will send a checkpoint with the same commit position as a recorded event, which means you must check first to see if an item is a `RecordedEvent` before recording the commit position of a `Checkpoint` object. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import Checkpoint, RecordedEvent def process_events_with_checkpointing(client, projection): # Get the last checkpoint last_commit_position = projection.get_last_checkpoint() # Subscribe using the last checkpoint with client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) as subscription: for item in subscription: if type(item) is RecordedEvent: # Regular event processing new_state = {"key": "value"} # Record commit position with new state projection.update_state(new_state, item.commit_position) elif type(item) is Checkpoint: # Record commit position projection.save_checkpoint(item.commit_position) break # <-- so we can continue with the examples class Projection: def __init__(self): self._last_checkpoint = None self._current_state = {} def get_last_checkpoint(self): return self._last_checkpoint def save_checkpoint(self, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is None or checkpoint > self._last_checkpoint ): self._last_checkpoint = checkpoint def update_state(self, state, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is not None and checkpoint <= self._last_checkpoint ): msg = f"Checkpoint conflict: {checkpoint} <= {self._last_checkpoint}" raise ValueError(msg) self._last_checkpoint = checkpoint self._current_state = state process_events_with_checkpointing(client, Projection()) ``` @tab async ```python:no-line-numbers from kurrentdbclient import Checkpoint, RecordedEvent async def process_events_with_checkpointing(client, projection): # Get the last checkpoint last_commit_position = await projection.get_last_checkpoint() # Subscribe using the last checkpoint async with await client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) as subscription: async for item in subscription: if type(item) is RecordedEvent: # Regular event processing new_state = {"key": "value"} # Record commit position with new state await projection.update_state(new_state, item.commit_position) elif type(item) is Checkpoint: # Record commit position await projection.save_checkpoint(item.commit_position) break # <-- so we can continue with the examples class Projection: def __init__(self): self._last_checkpoint = None self._current_state = {} async def get_last_checkpoint(self): return self._last_checkpoint async def save_checkpoint(self, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is None or checkpoint > self._last_checkpoint ): self._last_checkpoint = checkpoint async def update_state(self, state, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is not None and checkpoint <= self._last_checkpoint ): msg = f"Checkpoint conflict: {checkpoint} <= {self._last_checkpoint}" raise ValueError(msg) self._last_checkpoint = checkpoint self._current_state = state await process_events_with_checkpointing(client, Projection()) ``` ::: The same principles can be applied when processing events from a stream or a secondary index. ## Subscribe to Index ::: info Supported by KurrentDB 25.1 and later. ::: The `subscribe_to_index()` method returns a catch-up subscription to a secondary index. You can subscribe to all the events in a secondary index, optionally starting after a commit position. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `index_name` | Name of secondary index (`"$idx-"` prefix is optional). | | | `commit_position` | Position after which to start reading events. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, `subscribe_to_index()` returns an iterable of `RecordedEvent` objects. ### Examples Let's see how to use `subscribe_to_index()` by looking at some examples. ### Basic Subscription ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in a secondary index with client.subscribe_to_index(index_name="et-OrderCreated") as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in a secondary index async with await client.subscribe_to_index(index_name="et-OrderCreated") as subscription: # Iterate through the subscription with an 'async for' loop async for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` ::: ### After Commit Position ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in a secondary index with client.subscribe_to_index( index_name="et-OrderUpdated", commit_position=commit_position, ) as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: assert event.type == "OrderUpdated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in a secondary index async with await client.subscribe_to_index( index_name="et-OrderUpdated", commit_position=commit_position, ) as subscription: # Iterate through the subscription with an 'async for' loop async for event in subscription: assert event.type == "OrderUpdated" break # <-- so we can continue with the examples ``` ::: ## Handling Dropped Subscriptions An application which hosts the subscription can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. You should implement retry logic to handle such cases. ::: tabs @tab sync ```python:no-line-numbers import time from kurrentdbclient.exceptions import ConsumerTooSlowError, GrpcError projection = Projection() retries = 5 while True: try: process_events_with_checkpointing(client, projection) break # <-- so we can continue with the examples except (ConsumerTooSlowError, GrpcError): if retries <= 0: raise retries -= 1 time.sleep(5) continue ``` @tab async ```python:no-line-numbers import time from kurrentdbclient.exceptions import ConsumerTooSlowError, GrpcError projection = Projection() retries = 5 while True: try: await process_events_with_checkpointing(client, projection) break # <-- so we can continue with the examples except (ConsumerTooSlowError, GrpcError): if retries <= 0: raise retries -= 1 time.sleep(5) continue ``` ::: ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up". You can detect this transition by using the `include_caught_up` parameter of the catch-up subscription methods. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import CaughtUp # Subscribe with caught-up notifications with client.subscribe_to_stream( stream_name="order-123", include_caught_up=True, ) as subscription: for item in subscription: if type(item) is CaughtUp: print("Subscription has caught up to live events") break # <-- so we can continue with the examples else: # Regular event processing print(f"Processing event: {item.type}") ``` @tab async ```python:no-line-numbers from kurrentdbclient import CaughtUp # Subscribe with caught-up notifications async with await client.subscribe_to_stream( stream_name="order-123", include_caught_up=True, ) as subscription: async for item in subscription: if type(item) is CaughtUp: print("Subscription has caught up to live events") break # <-- so we can continue with the examples else: # Regular event processing print(f"Processing event: {item.type}") ``` ::: ::: tip The caught-up notification is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this notification will be sent right away. ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/appending-events.md' --- # Appending events This guide describes Python client methods for writing new events and records to KurrentDB. ## Overview The [Python clients for KurrentDB](getting-started.md#python-clients-for-kurrentdb) have three methods for writing new events: * [`append_to_stream()`](#append-to-stream) – write a collection of events to a single stream * [`multi_append_to_stream()`](#multi-append-to-stream) – write collections of events to multiple streams * [`append_records()`](#append-records) – write events to one or more streams in any order These methods are atomic and support [idempotent retries](#idempotent-behavior). The clients also provide methods for managing [stream metadata](@server/features/streams.md#metadata-and-reserved-names): * [`get_stream_metadata()`](#get-stream-metadata) * [`set_stream_metadata()`](#set-stream-metadata) ## New event records KurrentDB organises records into streams within a global transaction log. The Python clients provide two dataclasses for creating new records: * Use [`NewEvent`](#the-newevent-class) with [`append_to_stream()`](#append-to-stream) and [`multi_append_to_stream()`](#multi-append-to-stream). * Use [`NewRecord`](#the-newrecord-class) with the [`append_records()`](#append-records) method. ::: info [`NewRecord`](#the-newrecord-class) reflects the evolution of the KurrentDB API. Unlike [`NewEvent`](#the-newevent-class), it has a `stream_name` field. The name "record" reflects the fact that users may store more than just events in KurrentDB. ::: The `data` field of [`NewEvent`](#the-newevent-class) and [`NewRecord`](#the-newrecord-class) is a Python `bytes` object that contains the record payload. If you serialize your events as JSON, you can take advantage of KurrentDB features such as projections. However, you are free to use any serialization format that suits your requirements. The `content_type` field indicates whether `data` contains JSON or another binary format. You can choose between `"application/json"` and `"application/octet-stream"`. For example, if you use Message Pack or Protobuf, or if you compress or encrypt JSON data before writing it, use `"application/octet-stream"`. The default value is `"application/json"`. The `metadata` field is a Python `bytes` object that contains additional information about the record, such as correlation IDs, timestamps, access information, or other application-specific values. Metadata is stored separately from the payload. See [metadata restrictions](#metadata-restrictions) when using [`multi_append_to_stream()`](#multi-append-to-stream) and [`append_records()`](#append-records). The `id` field is a `UUID` that can uniquely identify the record. KurrentDB does not enforce uniqueness of record IDs, but they are used to support [idempotent append behavior](#idempotent-behavior). By default, a new version 4 UUID is generated. When a record is written, KurrentDB assigns two sequence numbers: * **Commit position** – The position of the record in the global transaction log. * **Stream position** – The position of the record within its stream. Each stream has a unique name. Stream positions are zero-based and gapless. The position in a stream is position `0`, the second is position `1`, the third is position `2`, and so on. Positions in KurrentDB's global transaction log are not gapless. ## Consistency checks When writing to a stream, you can use consistency checks to ensure that the stream is in the expected state before events are appended. Consistency checks are specified using: * the `current_version` argument of [`append_to_stream()`](#the-newevent-class), * the `current_version` field of the [`NewEvents`](#the-newevents-class) dataclass in [`multi_append_to_stream()`](#multi-append-to-stream), or * the `expected_state` field of [`StreamStateCheck`](#the-streamstatecheck-class) in [`append_records()`](#append-records). The following consistency checks are available: * `int` - the stream's current version must match the specified value, * `StreamState.NO_STREAM` - stream must not exist, * `StreamState.EXISTS` - stream must exist, and * `StreamState.ANY` - no consistency check is performed. To prevent concurrent writers from producing unexpected results, use `StreamState.NO_STREAM` when creating a new stream and the stream's current version when writing subsequent events. Alternatively, use `StreamState.EXISTS` to require that the stream already exists. Consistency checks can be diabled by using `StreamState.ANY`. If a consistency check fails, [`append_to_stream()`](#append-to-stream) and [`multi_append_to_stream()`](#multi-append-to-stream) will raise a `WrongCurrentVersionError`. The [`append_records()`](#append-records) method raises a `ConsistencyChecksFailedError`. ## Idempotent behavior KurrentDB append operations are idempotent. If a successful append operation is retried with the same [consistency checks](#consistency-checks) and the same event IDs, the operation will succeed without appending duplicate events. This allows clients to safely retry append operations when the outcome is uncertain, for example when a network failure occurs after the operation has been committed but before the response has been received. KurrentDB does not enforce globally unique event IDs. Event IDs are used only to detect retries of the same append operation. ## Append to stream The `append_to_stream()` method appends new event records to a named stream. | Parameter | Type | Description | |-------------------|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `stream_name` | `str` | Stream to which the `events` will be appended. | | `events` | `Iterable[NewEvent]` | The [NewEvent](#the-newevent-class) objects to be appended to the stream. | | `current_version` | int | StreamState | [Consistency check](#consistency-checks) for appending the given `events`. | | `timeout` | float | None | Maximum duration of operation (in seconds). | | `credentials` | grpc.CallCredentials | None | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `append_to_stream()` method returns the commit position (`int`) of the last event. This value can be used by applications to wait until eventually consistent views reflect newly recorded events. If the consistency check fails, the `append_to_stream()` method will raise will raise a `WrongCurrentVersionError` exception. This method is atomic and [idempotent](#idempotent-behavior). ::: info Requires leader Events can only be written to the "leader" node of a KurrentDB cluster. ::: ### The NewEvent class Use the `NewEvent` dataclass with the [`append_to_stream()`](#append-to-stream) and [`multi_append_to_stream()`](#multi-append-to-stream) methods. | Field | Type | Description | Default | |----------------|---------|----------------------------|----------------------| | `type` | `str` | The type of the event. | | | `data` | `bytes` | The content of the event. | | | `metadata` | `bytes` | Event metadata. | `b""` | | `content_type` | `str` | The format of the content. | `"application/json"` | | `id` | `UUID` | A unique ID for the event. | `uuid.uuid4()` | ### Examples #### Append to new stream The example below appends an event to a new stream `"student-1"`. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import ( AsyncKurrentDBClient, NewEvent, StreamState, ) # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(connection_string) # Create a new stream with a new event client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentRegistered", data=b'{"name": "Greg"}', ), ], current_version=StreamState.NO_STREAM, ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import ( AsyncKurrentDBClient, NewEvent, StreamState, ) # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(connection_string) # Create a new stream with a new event await client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentRegistered", data=b'{"name": "Greg"}', ), ], current_version=StreamState.NO_STREAM, ) ``` ::: The argument `current_version=StreamState.NO_STREAM` checks that no previous events have been appended. #### Append to existing stream The example below appends a second event to stream `"student-1"`. ::: tabs @tab sync ```python:no-line-numbers client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentNameChanged", data=b'{"name": "Gregory"}', ), ], current_version=0, ) ``` @tab async ```python:no-line-numbers await client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentNameChanged", data=b'{"name": "Gregory"}', ), ], current_version=0, ) ``` ::: The argument `current_version=0` checks that exactly one event has been appended to the stream. #### Wrong current version error The example below shows consistency checks failing an append operation. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError try: client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentRegistered", data=b'{"name": "Greg"}', ), ], current_version=StreamState.NO_STREAM, ) except WrongCurrentVersionError as e: assert e.stream_name == "student-1" assert e.expected_version == StreamState.NO_STREAM assert e.actual_version == 1 else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import WrongCurrentVersionError try: await client.append_to_stream( stream_name="student-1", events=[ NewEvent( type="StudentRegistered", data=b'{"name": "Greg"}', ), ], current_version=StreamState.NO_STREAM, ) except WrongCurrentVersionError as e: assert e.stream_name == "student-1" assert e.expected_version == StreamState.NO_STREAM assert e.actual_version == 1 else: raise Exception("Shouldn't get here") ``` ::: The `StreamState.NO_STREAM` value is wrong because the stream already has two events. The append operation fails by raising a `WrongCurrentVersionError` exception. ## Multi-append to stream The `multi_append_to_stream()` method appends groups of new events to different streams. | Parameter | Type | Description | |---------------|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `events` | `Iterable[NewEvents]` | An iterable of [NewEvents](#the-newevents-class) objects. | | `timeout` | float | None | Maximum duration of operation (in seconds). | | `credentials` | grpc.CallCredentials | None | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | The `multi_append_to_stream()` method returns the commit position (`int`) of the last event. This value can be used by applications to wait until eventually consistent views reflect newly recorded events. If more than one `NewEvents` object mentions the same `stream_name`, the `multi_append_to_stream()` method raises a `MultiAppendToSameStreamError` exception. If any of the consistency checks fail, the `multi_append_to_stream()` method raises a `WrongCurrentVersionError` exception. This method is atomic and [idempotent](#idempotent-behavior). ::: info Requires leader Events can only be written to the "leader" node of a KurrentDB cluster. ::: ::: info KurrentDB 25.1+ The `multi_append_to_stream()` method is supported by KurrentDB 25.1 and later. ::: ### The NewEvents class Use the `NewEvents` dataclass with the [`multi_append_to_stream()`](#multi-append-to-stream) method. | Field | Type | Description | |-------------------|----------------------------------------------|------------------------------------------------------------------------| | `stream_name` | `str` | Stream to which new events will be appended. | | `events` | `Iterable[NewEvent]` | The [`NewEvent`](#the-newevent-class) objects to append to the stream. | | `current_version` | int | StreamState | [Consistency check](#consistency-checks). | ### Examples The example below appends events to two streams. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvents client.multi_append_to_stream( events=[ NewEvents( stream_name="course-1", events=[ NewEvent( type='CourseCreated', data=b'{"name": "French"}' ), ], current_version=StreamState.NO_STREAM, ), NewEvents( stream_name="course-2", events=[ NewEvent( type='CourseCreated', data=b'{"name": "German"}' ), ], current_version=StreamState.NO_STREAM, ), ], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvents await client.multi_append_to_stream( events=[ NewEvents( stream_name="course-1", events=[ NewEvent( type='CourseCreated', data=b'{"name": "French"}' ), ], current_version=StreamState.NO_STREAM, ), NewEvents( stream_name="course-2", events=[ NewEvent( type='CourseCreated', data=b'{"name": "German"}' ), ], current_version=StreamState.NO_STREAM, ), ], ) ``` ::: The `StreamState.NO_STREAM` values check that no previous events have been appended. #### Append to existing streams The example below appends events to two existing streams. ::: tabs @tab sync ```python:no-line-numbers client.multi_append_to_stream( events=[ NewEvents( stream_name="student-1", events=[ NewEvent( type='StudentJoinedCourse', data=b'{"course_id": "course-1"}' ), ], current_version=1, ), NewEvents( stream_name="course-1", events=[ NewEvent( type='StudentJoinedCourse', data=b'{"student_id": "student-1"}' ), ], current_version=0, ), ], ) ``` @tab async ```python:no-line-numbers await client.multi_append_to_stream( events=[ NewEvents( stream_name="student-1", events=[ NewEvent( type='StudentJoinedCourse', data=b'{"course_id": "course-1"}' ), ], current_version=1, ), NewEvents( stream_name="course-1", events=[ NewEvent( type='StudentJoinedCourse', data=b'{"student_id": "student-1"}' ), ], current_version=0, ), ], ) ``` ::: The `current_version=1` value checks exactly two events have been appended to `"student-1"`. The `current_version=0` value checks exactly one event has been appended to `"course-1"` ## Append records The `append_records()` method appends new event records to multiple streams in any order. | Parameter | Type | Description | |---------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `records` | `Iterable[NewRecord]` | An iterable of [NewRecord](#the-newrecord-class) objects. | | `checks` | Iterable\[StreamStateCheck] | None | An iterable of [StreamStateCheck](#the-streamstatecheck-class) objects. | | `timeout` | float | None | Maximum duration of operation (in seconds). | | `credentials` | grpc.CallCredentials | None | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | The `append_records()` method returns the commit position (`int`) of the last event. This value can be used by applications to wait until eventually consistent views reflect newly recorded events. If one or more of the consistency checks fail, the `append_records()` method raises a `ConsistencyChecksFailedError` that details all of the consistency check failures. This method is atomic and [idempotent](#idempotent-behavior). ::: info Requires leader Events can only be written to the "leader" node of a KurrentDB cluster. ::: ::: info KurrentDB 26.1+ The `append_records()` method is supported by KurrentDB 26.1 and later. ::: ### The NewRecord class Use the `NewRecord` dataclass with the [`append_records()`](#append-records) method. | Field | Type | Description | Default | |----------------|---------|--------------------------------------------------------------|----------------------| | `stream_name` | `str` | Stream to which this record will be appended. | | | `type` | `str` | The type of the event. | | | `data` | `bytes` | The content of the event. | | | `metadata` | `bytes` | Event metadata (has [restrictions](#metadata-restrictions)). | `b""` | | `content_type` | `str` | The format of the content. | `"application/json"` | | `id` | `UUID` | A unique ID for the event. | `uuid.uuid4()` | ### The StreamStateCheck class Use the `StreamStateCheck` dataclass with the [`append_records()`](#append-records) method. | Field | Type | Description | Default | |------------------|----------------------------------------------|------------------------------------------------------------------------------------------------|----------------------| | `stream_name` | `str` | Stream to which this record will be appended. | | | `expected_state` | StreamState | int | [Consistency check](#consistency-checks) for setting stream metadata. | | ### Example The example below appends events to streams `"student-2"` and `"course-2"`. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewRecord, StreamStateCheck client.append_records( records=[ NewRecord( stream_name="student-2", type='StudentRegistered', data=b'{"name": "Joe"}' ), NewRecord( stream_name="course-2", type='StudentJoinedCourse', data=b'{"student_id": "student-2"}' ), NewRecord( stream_name="student-2", type='StudentJoinedCourse', data=b'{"course_id": "course-2"}' ), ], checks=[ StreamStateCheck( stream_name="student-2", expected_state=StreamState.NO_STREAM, ), StreamStateCheck( stream_name="course-2", expected_state=0, ), ], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewRecord, StreamStateCheck await client.append_records( records=[ NewRecord( stream_name="student-2", type='StudentRegistered', data=b'{"name": "Joe"}' ), NewRecord( stream_name="course-2", type='StudentJoinedCourse', data=b'{"student_id": "student-2"}' ), NewRecord( stream_name="student-2", type='StudentJoinedCourse', data=b'{"course_id": "course-2"}' ), ], checks=[ StreamStateCheck( stream_name="student-2", expected_state=StreamState.NO_STREAM, ), StreamStateCheck( stream_name="course-2", expected_state=0, ), ], ) ``` ::: The `expected_state=StreamState.NO_STREAM` value checks `"student-2"` has no events. The `expected_state=0` value checks exactly one event has been appended to `"course-2"` #### Consistency checks failed error The example below shows consistency checks failing an append operation. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import ( ConsistencyChecksFailedError, ConsistencyCheckFailure, StreamStateCheckFailure, ) try: client.append_records( records=[ NewRecord( stream_name="student-2", type='StudentRegistered', data=b'{"name": "Joe"}' ), ], checks=[ StreamStateCheck( stream_name="student-2", expected_state=StreamState.NO_STREAM, ), ], ) except ConsistencyChecksFailedError as e: assert len(e.failures) == 1 failure = e.failures[0] assert isinstance(failure, ConsistencyCheckFailure) assert failure.check_index == 0 assert failure.stream_state_failure is not None assert failure.stream_state_failure.stream_name == "student-2" assert failure.stream_state_failure.expected_state == -1 assert failure.stream_state_failure.actual_state == 1 else: raise Exception("Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import ( ConsistencyChecksFailedError, ConsistencyCheckFailure, StreamStateCheckFailure, ) try: await client.append_records( records=[ NewRecord( stream_name="student-2", type='StudentRegistered', data=b'{"name": "Joe"}' ), ], checks=[ StreamStateCheck( stream_name="student-2", expected_state=StreamState.NO_STREAM, ), ], ) except ConsistencyChecksFailedError as e: assert len(e.failures) == 1 failure = e.failures[0] assert isinstance(failure, ConsistencyCheckFailure) assert failure.check_index == 0 assert failure.stream_state_failure is not None assert failure.stream_state_failure.stream_name == "student-2" assert failure.stream_state_failure.expected_state == -1 assert failure.stream_state_failure.actual_state == 1 else: raise Exception("Shouldn't get here") ``` ::: The value `current_version=StreamState.NO_STREAM` is wrong because the stream already exists. The append operation fails by raising a `ConsistencyChecksFailedError` exception which details the failure. ## Metadata restrictions When appending events with [`multi_append_to_stream()`](#multi-append-to-stream) and [`append_records()`](#append-records), the `metadata` field of `NewEvent` or `NewRecord` must be either an empty `bytes` string or a `bytes` string containing a JSON object whose values are strings. ### Examples The following metadata values are acceptable. | | Description | Example | |---|--------------------------------|-----------------| | ✅ | Empty bytes | `b""` | | ✅ | JSON object with string values | `b'{"a": "1"}'` | The following metadata values are NOT acceptable and will cause a `ProgrammingError` exception. | | Description | Example | |---|-------------------------------------|-----------------------------------------------| | ❌ | Random bytes | `b'\xf5d\xc5W3^b\xb0(\xf9\x01D\x81\xa7Y\x98'` | | ❌ | JSON string | `b'"abcdef"'` | | ❌ | JSON object with non-string values | `b'{"a": 1}'` or `b'{"a": false}'` | | ❌ | Nested JSON objects | `b'{"a": {}}'` | ## Get stream metadata The `get_stream_metadata()` method gets [stream metadata](@server/features/streams.md#metadata-and-reserved-names) for a particular stream. | Parameter | Type | Description | |---------------|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `stream_name` | `str` | The name of a stream the metadata applies to (don't use `$$` prefix here). | | `timeout` | float | None | Maximum duration of operation (in seconds). | | `credentials` | grpc.CallCredentials | None | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | The `get_stream_metadata()` method returns a Python `dict` of metadata keys and values for the named stream, along with the current version of the stream's metadata stream. If the named stream does not exist, the `dict` will be empty and the current version value will be `StreamState.NO_STREAM`. These two values can be used as arguments of `metadata` and `current_version` when calling [`set_stream_metadata()`](#set-stream-metadata). ### Example The example below gets metadata for stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers metadata, current_version = client.get_stream_metadata( stream_name="order-123", ) ``` @tab async ```python:no-line-numbers metadata, current_version = await client.get_stream_metadata( stream_name="order-123", ) ``` ::: ## Set stream metadata The `set_stream_metadata()` method sets [stream metadata](@server/features/streams.md#metadata-and-reserved-names) for a particular stream. If the named stream does not exist, the metadata will be set anyway. This allows streams to be configured before they are used. | Parameter | Type | Description | |-------------------|---------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | `stream_name` | `str` | The name of a stream the metadata applies to (don't use `$$` prefix here). | | `metadata` | `dict[str, Any]` | A Python `dict` of stream metadata keys and values. Needs to be serializable by `json.dumps()`. | | `current_version` | int | StreamState | [Consistency check](#consistency-checks) for setting stream metadata. | | `timeout` | float | None | Maximum duration of operation (in seconds). | | `credentials` | grpc.CallCredentials | None | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | The `set_stream_metadata()` method returns `None`. ### Example The example below sets metadata for stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers metadata["foo"] = "bar" client.set_stream_metadata( stream_name="order-123", metadata=metadata, current_version=current_version, ) metadata, _ = client.get_stream_metadata("order-123") assert metadata["foo"] == "bar" ``` @tab async ```python:no-line-numbers metadata["foo"] = "bar" await client.set_stream_metadata( stream_name="order-123", metadata=metadata, current_version=current_version, ) metadata, _ = await client.get_stream_metadata("order-123") assert metadata["foo"] == "bar" ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/connection-strings.md' --- # Connection strings This guide explains the standardized connection string format used by all official KurrentDB clients. :::info For production services, ask your service provider for a valid connection string. ::: KurrentDB clients use a connection string to configure their connection to KurrentDB. ## Two protocols KurrentDB connection strings support two protocols. * **`kurrentdb://`** for **connecting directly** to specific KurrentDB server endpoints. * **`kurrentdb+discover://`** for connecting using cluster discovery **via DNS records**. With the `kurrentdb://` protocol, you can specify one or many endpoints, separated by commas. If a single endpoint is specified, the client **connects directly** to that endpoint and continues to use it for subsequent operations. If multiple endpoints are specified, the client uses them to query cluster information and selects an endpoint **from the discovered cluster members** according to the node preference specified in the connection string (see options below). If the client needs to reconnect, this discovery process is repeated. An endpoint may be specified as either a host name or an IP address, together with a port number. With the `kurrentdb+discover://` protocol, you should specify the fully-qualified domain name of a KurrentDB cluster, with an optional port number. The client **resolves the cluster domain name** to discover cluster nodes, queries the cluster for membership information, and then selects an endpoint according to the node preference specified in the connection string (see options below). If the client needs to reconnect, the discovery process is repeated. ## User info Both the `kurrentdb://` and `kurrentdb+discover://` protocols support an optional user info string. If it exists, the user info string must be separated from the rest of the URI with the `"@"` character. The user info string must include a username and a password, separated with the `":"` character. The user info is sent by the client in a "basic auth" authorization header in each gRPC call to a "secure" server. This authorization header is used by the server to authenticate the client. The Python clients do not allow call credentials to be transferred to "insecure" servers (option `tls=false`). ## Examples In the examples below, `user` is a username and `pass` is a password. For connecting directly to a single node: ```:no-line-numbers kurrentdb://user:pass@node1:2113 ``` For connecting to a cluster using specific endpoints to obtain cluster information: ```:no-line-numbers kurrentdb://user:pass@node1:2113,node2:2113,node3:2113 ``` For connecting to a cluster configured with DNS A records for the cluster endpoints: ```:no-line-numbers kurrentdb+discover://user:pass@cluster1:2113 ``` ## User certificates To authenticate a client with an X.509 certificate, you need: * KurrentDB version 25.0+ [configured for user certificates](@server/security/user-authentication.html#user-x-509-certificates); and * A valid client certificate and private key. Then use the `userCertFile` and `userKeyFile` connection string options. Here's an example for connecting to KurrentDB with a client certificate. ```:no-line-numbers kurrentdb://node1:2113?userCertFile=user_cert.pem&userKeyFile=user_key.pem ``` ::: info A licence is required to authenticate clients with user certificates. ::: ## Connection options The table below describes optional query parameters that can be used in the connection string to configure the client. All option field names and values are case-insensitive. | Field name | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Set to `false` when connecting to KurrentDB running with "insecure" mode. | | `connectionName` | Any string | Random UUID | Connection name | | `maxDiscoverAttempts` | Integer | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Integer | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Integer | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsCaFile` | File system path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Integer | None | Maximum duration, in seconds, for completion of client operations. Can be overridden per operation using the `timeout` parameter of client methods. | | `keepAliveInterval` | Integer | None | Interval between keep-alive ping calls, in milliseconds. | | `keepAliveTimeout` | Integer | None | Keep-alive ping call timeout, in milliseconds. | | `userCertFile` | File system path | None | User certificate file for X.509 authentication. | | `userKeyFile` | File system path | None | Key file for the user certificate used for X.509 authentication. | --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/delete-stream.md' --- # Deleting events This guide describes Python client methods for deleting streams in KurrentDB. ## Overview In KurrentDB, you can delete events and streams either partially or completely. Stream [metadata settings](./appending-events.md#set-stream-metadata) like `$maxAge` and `$maxCount` help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. The Python clients have two methods for deleting streams: * `delete_stream()` – soft delete * `tombstone_stream()` – hard delete ## Delete stream The `delete_stream()` method "soft deletes" a stream in KurrentDB. Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. While "soft delete" marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream to be "soft deleted". | | | `current_version` | The [optimistic concurrency control](./appending-events.md#consistency-checks) for deleting a stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `delete_stream()` returns `None`. ### Example Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: Now let's "soft delete" the stream. ::: tabs @tab sync ```python:no-line-numbers # Get the current version of the stream current_version = client.get_current_version(stream_name="order-123") # Soft delete the stream client.delete_stream( stream_name="order-123", current_version=current_version ) ``` @tab async ```python:no-line-numbers # Get the current version of the stream current_version = await client.get_current_version(stream_name="order-123") # Soft delete the stream await client.delete_stream( stream_name="order-123", current_version=current_version ) ``` ::: ## Tombstone stream The `tombstone_stream()` method "hard deletes" a stream in KurrentDB. Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream to be "hard deleted". | | | `current_version` | The [optimistic concurrency control](./appending-events.md#consistency-checks) for deleting a stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | If successful, `tombstone_stream()` returns `None`. ### Example ::: tabs @tab sync ```python:no-line-numbers # Get the current version of the stream current_version = client.get_current_version(stream_name="order-123") # Hard delete (tombstone) the stream client.tombstone_stream( stream_name="order-123", current_version=current_version ) print("Tombstoned stream: order-123") ``` @tab async ```python:no-line-numbers # Get the current version of the stream current_version = await client.get_current_version(stream_name="order-123") # Hard delete (tombstone) the stream await client.tombstone_stream( stream_name="order-123", current_version=current_version ) print("Tombstoned stream: order-123") ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/getting-started.md' --- # Getting started This guide will help you get started with the Python clients for KurrentDB: * [Start KurrentDB locally](#running-kurrentdb-locally) * [Install the Python package](#installation) * [Client configuration](#client-configuration) * [Connect to KurrentDB](#connecting-to-kurrentdb) * [Create new events](#creating-new-events) * [Append events to streams](#appending-to-a-stream) * [Read streams](#reading-a-stream) ## Running KurrentDB locally You can start KurrentDB with "insecure" mode in Docker by using the `--insecure` flag: ```bash:no-line-numbers docker run --name kurrentdb-node -it -p 2113:2113 \ docker.kurrent.io/kurrent-lts/kurrentdb:latest \ --insecure \ --run-projections=All \ --enable-atom-pub-over-http ``` Please read the server docs for more details about [KurrentDB installation](@server/quick-start/installation.html). ## Installation The [`kurrentdbclient`](https://pypi.org/project/kurrentdbclient/) package provides the official sync and async Python clients for KurrentDB. ### Install or update Python For information about how to get the latest version of Python, see the official [Python documentation](https://www.python.org/downloads/). Before installing the Python clients for KurrentDB, ensure you’re using Python 3.10 or later. ### Install the Python clients If you use `uv`: ```bash:no-line-numbers uv add "kurrentdbclient~=1.3" ``` If you use `poetry`: ```bash:no-line-numbers poetry add "kurrentdbclient~=1.3" ``` If you prefer a manual setup with `pip`: ```bash:no-line-numbers python -m venv .venv source .venv/bin/activate pip install "kurrentdbclient~=1.3" ``` ## Python clients for KurrentDB The `kurrentdbclient` Python package provides both sync and async clients for KurrentDB. The sync and async clients have exactly the same methods and behaviors as each other. * Sync client – **blocking** interface suitable for sequential code and multi-threaded apps * Async client – **asynchronous** interface suitable for high-concurrency applications This documentation provides examples for both sync and async clients in tabbed boxes (see below). The official sync and async Python clients have been tested with KurrentDB versions 25.0, 25.1, 26.0, and 26.1, and EventStoreDB versions 23.10 and 24.10, with and without SSL/TLS, in both single-server and cluster modes, across Python versions 3.10, 3.11, 3.12, 3.13, and 3.14. ## Client configuration All KurrentDB clients use a standardized [connection string](./connection-strings.md) to configure their connection to KurrentDB. When KurrentDB is [running locally](#running-kurrentdb-locally) with "insecure" mode, use a connection string with `tls=false`: ```python:no-line-numbers connection_string = "kurrentdb://127.0.0.1:2113?tls=false" ``` For production services, ask your service provider for a valid [connection string](./connection-strings.md). ## Connecting to KurrentDB To connect to KurrentDB from Python, instantiate a [client](#python-clients-for-kurrentdb) with a [connection string](#client-configuration). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient client = KurrentDBClient(connection_string) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient client = AsyncKurrentDBClient(connection_string) ``` ::: ## Creating new events Use the [`NewEvent`](./appending-events.md#the-newevent-class) class to define new events with a `type` string and binary `data`. ```python:no-line-numbers from kurrentdbclient import NewEvent new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) ``` See the [`NewEvent`](./appending-events.md#the-newevent-class) documentation for more details. ## Appending to a stream The client [`append_to_stream()`](./appending-events.md#append-to-stream) method records new events in KurrentDB. When appending to a stream, specify a `stream_name`, the new [`events`](./appending-events.md#the-newevent-class) and a [`current_version`](./appending-events.md#consistency-checks). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) client.append_to_stream( stream_name="order-123", events=[new_event], current_version=StreamState.NO_STREAM, ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState new_event = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) await client.append_to_stream( stream_name="order-123", events=[new_event], current_version=StreamState.NO_STREAM, ) ``` ::: See [Appending events](./appending-events.md) for more information about writing to KurrentDB. ## Reading a stream The client [`get_stream()`](./reading-events.md#get-stream) method reads events from a named stream. ::: tabs @tab sync ```python:no-line-numbers for recorded_event in client.get_stream( stream_name="order-123" ): print("Stream name:", recorded_event.stream_name) print("Stream position:", recorded_event.stream_position) print("Commit position:", recorded_event.commit_position) print("Event type:", recorded_event.type) print("Event data:", recorded_event.data) print("Event ID:", recorded_event.id) ``` @tab async ```python:no-line-numbers for recorded_event in await client.get_stream( stream_name="order-123" ): print("Stream name:", recorded_event.stream_name) print("Stream position:", recorded_event.stream_position) print("Commit position:", recorded_event.commit_position) print("Event type:", recorded_event.type) print("Event data:", recorded_event.data) print("Event ID:", recorded_event.id) ``` ::: See [Reading events](./reading-events.md) for more information about reading from KurrentDB. ## Overriding user credentials You can use the `credentials` parameter of the Python client methods to override the [user info](./connection-strings.md#user-info) given in a client connection string. Use the `construct_call_credentials()` method to construct a `CallCredentials` object from a username and password. ::: tabs @tab sync ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[new_event], credentials=credentials, ) ``` @tab async ```python:no-line-numbers # Construct call credentials credentials = client.construct_call_credentials( username="admin", password="changeit", ) # Use credentials for this specific operation commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.ANY, events=[new_event], credentials=credentials, ) ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/observability.md' --- # Observability This guide explains how to instrument and export telemetry data from the Python clients. ## Overview The Python client package provide [OpenTelemetry](https://opentelemetry.io) intrumentors. This enables you to monitor, trace, and troubleshoot your event store operations with distributed tracing support, for both the sync and async Python clients. ## Instrumenting a client The Python client instrumentors depend on various OpenTelemetry Python packages, which you will need to install. ### Install package To ensure verified version compatibility, install `kurrentdbclient` with the `opentelemetry` option. ```bash:no-line-numbers pip install kurrentdbclient[opentelemetry] ``` ### Activate instrumentor You can then activate the client instrumentors within your application code. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.instrumentation.opentelemetry import ( KurrentDBClientInstrumentor, ) # Activate sync client instrumentation. KurrentDBClientInstrumentor().instrument() # Deactivate sync client instrumentation. KurrentDBClientInstrumentor().uninstrument() ``` @tab async ```python:no-line-numbers from kurrentdbclient.instrumentation.opentelemetry import ( AsyncKurrentDBClientInstrumentor, ) # Activate async client instrumentation. AsyncKurrentDBClientInstrumentor().instrument() # Deactivate async client instrumentation. AsyncKurrentDBClientInstrumentor().uninstrument() ``` ::: ## Exporting telemetry data In order to export telemetry data, you will need to initialise the global "tracer provider". ### Console exporter For example, to export data to the console you will need to install the Python package `opentelemetry-sdk`, and use the class `TracerProvider`, `BatchSpanProcessor`, and `ConsoleSpanExporter` in the following way. ```python:no-line-numbers from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.trace import set_tracer_provider resource = Resource.create( attributes={ SERVICE_NAME: "kurrentdb", } ) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor( ConsoleSpanExporter() ) ) set_tracer_provider(provider) ``` ### OTLP exporter To export data to an OpenTelemetry compatible data collector, such as [Jaeger](https://www.jaegertracing.io), you will need to install the Python package `opentelemetry-exporter-otlp-proto-http`, and then use the class `OTLPSpanExporter` from the `opentelemetry.exporter.otlp.proto.http.trace_exporter` module, with an appropriate `endpoint` argument for your collector. ```python:no-line-numbers from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.trace import set_tracer_provider resource = Resource.create( attributes={ SERVICE_NAME: "kurrentdb", } ) provider = TracerProvider(resource=resource) provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces") ) ) set_tracer_provider(provider) ``` You can start Jaeger locally by running the following command. ```bash:no-line-numbers docker run --name jaeger -d -p 4318:4318 -p 16686:16686 \ jaegertracing/all-in-one:latest ``` Telemetry data from the client instrumentors can then be exported to `http://localhost:4318/v1/traces`. You can navigate to `http://localhost:16686` to access the Jaeger UI. You can find a list of available exporters for different platforms in the [OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?component=exporter\&language=python). For detailed configuration options, refer to the OpenTelemetry [Python documentation](https://opentelemetry.io/docs/languages/python/). ## Understanding traces ### What gets traced At this time, the instrumented methods are [`append_to_stream()`](./appending-events.md#append-to-stream), [`multi_append_to_stream()`](./appending-events.md#multi-append-to-stream), [`subscribe_to_stream()`](./subscriptions.md#subscribe-to-stream), [`subscribe_to_all()`](./subscriptions.md#subscribe-to-all), [`read_subscription_to_stream()`](./persistent-subscriptions.md#read-subscription-to-stream), and [`read_subscription_to_all()`](./persistent-subscriptions.md#read-subscription-to-all). The append methods are instrumented by spanning the method call with a "producer" span. The subscription methods are instrumented by instrumenting the response iterators, creating a "consumer" span for each recorded event received. The producer spans add span context information to event metadata. The "consumer" spans extract this information from the recorded event metadata, and make each "consumer" span a child of a "producer" parent span. ### Producer span Each span includes attributes to help with monitoring and debugging. Producer spans for [appending to a single stream](./appending-events.md#append-to-stream) have the following attributes: | Attribute | Description | Example | |------------------------------|----------------------------------------|---------------------| | db.operation | Type of operation performed | `"streams.append"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | db.kurrentdb.stream | Stream name or identifier | `"user-events-123"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | Producer spans for [appending to multiple streams](./appending-events.md#multi-append-to-stream) have the following attributes: | Attribute | Description | Example | |------------------------------|----------------------------------------|---------------------| | db.operation | Type of operation performed | `"streams.append"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | #### Example Here's an instrumentor span for a successful [`append_to_stream()`](./appending-events.md#append-to-stream) operation. ```json:no-line-numbers { "name": "streams.append", "context": { "trace_id": "0x82ac04990e711b6f35348556006fe4cf", "span_id": "0x9852ade35f00d350", "trace_state": "[]" }, "kind": "SpanKind.PRODUCER", "parent_id": null, "start_time": "2026-02-17T13:59:23.842871Z", "end_time": "2026-02-17T13:59:23.866696Z", "status": { "status_code": "OK" }, "attributes": { "db.operation": "streams.append", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.stream": "user-123", "server.address": "localhost", "server.port": "2113" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` ### Consumer span Consumer spans have the following attributes. | Attribute | Description | Example | |------------------------------|----------------------------------------|------------------------------------------| | db.operation | Type of operation performed | `"streams.subscribe"` | | db.system | Database system identifier | `"kurrentdb"` | | db.user | Database user name | `"admin"` | | db.kurrentdb.event.id | Event identifier | `"e7548b90-d79b-4474-b00f-631de4285acc"` | | db.kurrentdb.event.type | Event type identifier | `"AccountRegistered"` | | db.kurrentdb.stream | Stream name or identifier | `"user-123"` | | db.kurrentdb.subscription.id | Subscription identifier | `"user-123-subscription-1"` | | server.address | KurrentDB server address | `"localhost"` | | server.port | KurrentDB server port | `"2113"` | #### Example Here's an instrumentor span from a [catch-up subscription](./subscriptions.md) operation. ```json:no-line-numbers { "name": "streams.subscribe", "context": { "trace_id": "0x5ad5e1bcff7f33cb44b93d470bd34554", "span_id": "0x446cf48b1bb9e574", "trace_state": "[]" }, "kind": "SpanKind.CONSUMER", "parent_id": "0x1496f8ba3507977b", "start_time": "2026-02-17T14:16:20.810515Z", "end_time": "2026-02-17T14:16:20.810605Z", "status": { "status_code": "OK" }, "attributes": { "db.operation": "streams.subscribe", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.event.id": "4ca26d3e-cbec-477e-9e59-d9248d8a3aef", "db.kurrentdb.event.type": "UserRegistered", "db.kurrentdb.stream": "user-123", "db.kurrentdb.subscription.id": "5da1a8c8-3dec-441e-8b6f-7514c797b1b4", "server.address": "localhost", "server.port": "2113" }, "events": [], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` ### Span errors Errors are traced by including a "span event" with the following attributes. | Attribute | Description | Example | |----------------------|---------------------------------------------------------|--------------------------------------------------------| | exception.type | Exception type if an error occurred | `"ServiceUnavailableError"` | | exception.message | Exception message if an error occurred | `"failed to connect to all addresses"` | | exception.stacktrace | Stack trace of the exception | `"Traceback (most recent call last):\n File..."` | | exception.escaped | Whether the exception is escaping the scope of the span | `"True"` | #### Example Here's an instrumentor span for an errorful [`append_to_stream()`](./appending-events.md#append-to-stream) operation. ```json:no-line-numbers { "name": "streams.append", "context": { "trace_id": "0xb99bba6da5c45dd2c72cca9f50064edd", "span_id": "0x31db5b46eac4a92e", "trace_state": "[]" }, "kind": "SpanKind.PRODUCER", "parent_id": null, "start_time": "2026-02-17T13:59:27.614387Z", "end_time": "2026-02-17T13:59:27.940788Z", "status": { "status_code": "ERROR", "description": "ServiceUnavailableError: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)" }, "attributes": { "db.operation": "streams.append", "db.system": "kurrentdb", "db.user": "admin", "db.kurrentdb.stream": "user-123", "server.address": "localhost", "server.port": "2113" }, "events": [ { "name": "exception", "timestamp": "2026-02-17T13:59:27.940712Z", "attributes": { "exception.type": "kurrentdbclient.exceptions.ServiceUnavailableError", "exception.message": "failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)", "exception.stacktrace": "Traceback (most recent call last):\n File \"/venv/kurrentdbclient/client.py\", line 152, in retrygrpc_decorator\n return f(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/client.py\", line 140, in autoreconnect_decorator\n return f(*args, **kwargs)\n ^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/client.py\", line 549, in append_to_stream\n return self.streams.batch_append(\n ^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/venv/kurrentdbclient/streams.py\", line 1331, in batch_append\n raise handle_rpc_error(e) from None\nkurrentdbclient.exceptions.ServiceUnavailableError: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:1000: Failed to connect to remote host: connect: Connection refused (61)\n", "exception.escaped": "True" } } ], "links": [], "resource": { "attributes": { "telemetry.sdk.language": "python", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "1.39.1", "service.name": "kurrentdb" }, "schema_url": "" } } ``` --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/persistent-subscriptions.md' --- # Persistent subscriptions This guide describes methods for persistent subscriptions. ## Overview Persistent subscriptions are similar to [catch-up subscriptions](./subscriptions.md) with two key differences: * Persistent subscriptions are defined on the server and checkpoints are maintained by the server. This means that clients can reconnect to a persistent subscription and automatically receive unprocessed events. * It's possible to connect more than one consumer to the same persistent subscription. The server will send events to all connected consumers, according the choice of consumer strategy. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ### Creating subscription groups The first step is to create a new persistent subscription group. Admin permissions are required. The Python clients have two methods for creating a persistent subscription group: * [`create_subscription_to_stream()`](#create-subscription-to-stream) – create persistent subscription to a stream * [`create_subscription_to_all()`](#create-subscription-to-all) – create persistent subscription to global transaction log ### Consumer strategies When creating a persistent subscription group, you can choose between a number of consumer strategies. #### DispatchToSingle (default) Distributes events to a single consumer until the buffer size is reached. After that, the next consumer is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. #### RoundRobin Distributes events to all consumers evenly. If the buffer size is reached, the consumer won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. #### Pinned For use with an indexing projection such as the system by-category projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual consumers. When a consumer connects, it is assigned some existing buckets. When a consumer disconnects, its buckets are assigned to other consumers. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### Consuming events Consumers read from existing subscription groups. The server distributes events to consumers according to the subscription group's consumer strategy setting. The Python clients have two methods for reading from a subscription group: * [`read_subscription_to_stream()`](#read-subscription-to-stream) – start consuming events from a stream * [`read_subscription_to_all()`](#read-subscription-to-all) – start consuming events from the global transaction log These methods return a `PersistentSubscription` object. The `PersistenceSubscription` class is a Python iterable that returns [RecordedEvent](./reading-events.md#recorded-events) objects, and which has two methods, `ack()` and `nack()`, for acknowledging and negatively acknowledging received events. Consumers must use `ack()` or `nack()` to acknowledge or negatively acknowledge received events. ### Acknowledgements If processing is successful, a consumer should call `ack()` on its `PersistentSubscription` object, passing in the `RecordedEvent` object that was successfully consumed. This will pick the correct event ID to send to the server, letting the server know the message has been handled. | Parameter | Type | Description | |-----------|-------------------|------------------------------| | `item` | `RecordedEvent` | Successfully consumed events | #### Negative acknowledgements If processing fails for some reason, the consumer should call `nack()` on its `PersistentSubscription` object, passing in both the `RecordedEvent` and a negative acknowledgement action. | Parameter | Type | Description | |-----------|-----------------|-------------------------------| | `item` | `RecordedEvent` | Unsuccessfully consumed event | | `action` | `str` | Name of action | The negative acknowledgement `action` describes what the server should do with the event. | Action | Description | |-----------|:----------------------------------------------------------------------| | `"park"` | Park the message and do not resend. Put it on poison queue. | | `"retry"` | Explicitly retry the message. | | `"skip"` | Skip this message do not resend and do not put in poison queue. | | `"stop"` | Stop the subscription. | ### Adjusting group settings You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. The Python clients have two methods for adjusting the settings of a persistent subscription group. * [`update_subscription_to_stream()`](#update-subscription-to-stream) – update settings for subscription to a stream * [`update_subscription_to_all()`](#update-subscription-to-all) – update settings for subscription to global transaction log ### Getting subscription info The Python clients have three methods for getting information about existing persistent subscriptions. * [`get_subscription_info()`](#get-subscription-info) – get information about a persistent subscription * [`list_subscriptions_to_stream()`](#list-subscriptions-to-stream) – get information about all persistent subscriptions to a stream * [`list_subscriptions()`](#list-subscriptions) – get information about all existing persistent subscriptions ### Deleting subscription groups Remove a subscription group with the delete operation. Like the creating and updating, you must have admin permissions to delete a persistent subscription group. The Python clients have one method for deleting a persistent subscription group. * [`delete_subscription()`](#delete-subscription) – delete subscription group ## Create subscription to stream Use `create_subscription_to_stream()` to create a group for consuming a stream. The persistent subscription can be created before the steam. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `from_end` | Whether to start the subscription from the end of the stream. | `False` | | `stream_position` | Position in stream from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `"DispatchToSingle"` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `30.0` | | `max_retry_count` | The maximum number of retries before a message will be parked. | `10` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `2.0` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `5` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to create a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(connection_string) # Create a persistent subscription to a specific stream client.create_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient # Connect to KurrentDB connection_string = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(connection_string) # Create a persistent subscription to a specific stream await client.create_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) ``` ::: ## Create subscription to all Use `create_subscription_to_all()` to create a group for consuming the global transaction log. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| | `group_name` | Name of persistent subscription group. | | | `from_end` | Whether to start the subscription from the end of the stream. | `False` | | `commit_position` | Position in global transaction log from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name rather than event type. | `False` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `"DispatchToSingle"` | | `message_timeout` | The duration of time (in seconds) after which to consider a message as timed out and retried. | `30.0` | | `max_retry_count` | The maximum number of retries before a message will be parked. | `10` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `10` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `2.0` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `5` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `read_batch_size` | The number of events read at a time when paging through history. | `200` | | `history_buffer_size` | The number of events to cache when paging through history. | `500` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to create a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.create_subscription_to_all( group_name="transaction-log-subscription", filter_include=["OrderCreated"], ) ``` @tab async ```python:no-line-numbers await client.create_subscription_to_all( group_name="transaction-log-subscription", filter_include=["OrderCreated"], ) ``` ::: ## Read subscription to stream Use `read_subscription_to_stream()` to start consuming events from a stream. | Parameter | Description | Default | |----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `event_buffer_size` | Number of events in consumer buffer. | `150` | | `max_ack_batch_size` | Number of acknowledgements before sending all to server. | `50` | | `max_ack_delay` | Amount of time (in seconds) before sending acknowledgements to server. | `0.2` | | `stopping_grace` | Amount of time (in seconds) to allow server to receive acknowledgements when consumer is stopping. | `0.2` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Returns a [`PersistentSubscription`](#consuming-events) object. #### Example Here's an example showing how to consume events from a subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState # Create a new stream with a new event order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Connect to a persistent subscription for a specific stream with client.read_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) as subscription: # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` @tab async ```python:no-line-numbers from kurrentdbclient import NewEvent, StreamState # Create a new stream with a new event order_created = NewEvent( type="OrderCreated", data=b'{"name": "Greg"}', ) await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[order_created], ) # Connect to a persistent subscription for a specific stream async with await client.read_subscription_to_stream( group_name="stream-subscription", stream_name="order-123" ) as subscription: # Process events and acknowledge them async for event in subscription: try: # Process the event print(f"Processing event: {event.type}") # Acknowledge successful processing await subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") await subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` ::: ## Read subscription to all Use `read_subscription_to_all()` to start consuming events from the global transaction log. | Parameter | Description | Default | |----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `event_buffer_size` | Number of events in consumer buffer. | `150` | | `max_ack_batch_size` | Number of acknowledgements before sending all to server. | `50` | | `max_ack_delay` | Amount of time (in seconds) before sending acknowledgements to server. | `0.2` | | `stopping_grace` | Amount of time (in seconds) to allow server to receive acknowledgements when consumer is stopping. | `0.2` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Returns a [`PersistentSubscription`](#consuming-events) object. #### Example Here's an example showing how to consume events from a subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Connect to a persistent subscription for all events with client.read_subscription_to_all( group_name="transaction-log-subscription" ) as subscription: # Process events and acknowledge them for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Connect to a persistent subscription for all events async with await client.read_subscription_to_all( group_name="transaction-log-subscription" ) as subscription: # Process events and acknowledge them async for event in subscription: try: # Process the event print(f"Processing event: {event.type} from stream {event.stream_name}") # Acknowledge successful processing await subscription.ack(event) except Exception as e: # Handle processing errors print(f"Error processing event: {e}") await subscription.nack(event, action="retry") break # <- so we can continue with the examples ``` ::: ## Update subscription to stream Use `update_subscription_to_stream()` to adjust a group consuming from a stream. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream from which to consume events. | | | `from_end` | Whether to start the subscription from the end of the stream. | `None` | | `stream_position` | Position in stream from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `None` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `None` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `None` | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `None` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `None` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `None` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `None` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `None` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `None` | | `read_batch_size` | The number of events read at a time when paging through history. | `None` | | `history_buffer_size` | The number of events to cache when paging through history. | `None` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to update a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.update_subscription_to_stream( group_name="stream-subscription", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) ``` @tab async ```python:no-line-numbers await client.update_subscription_to_stream( group_name="stream-subscription", stream_name="order-123", resolve_links=True, min_checkpoint_count=20 ) ``` ::: ## Update subscription to all Use `update_subscription_to_all()` to adjust a group consuming from the global transaction log. | Parameter | Description | Default | |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `from_end` | Whether to start the subscription from the end of the stream. | `None` | | `commit_position` | Position in global transaction log from which to consume events (inclusive). | `None` | | `resolve_links` | Whether the subscription should resolve link events to their linked events. | `None` | | `consumer_strategy` | The [consumer strategy](#consumer-strategies) to use for distributing events to client consumers. | `None` | | `message_timeout` | The amount of time (in seconds) after which to consider a message as timed out and retried. | `None` | | `max_retry_count` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `None` | | `min_checkpoint_count` | The minimum number of messages to process before a checkpoint may be written. | `None` | | `max_checkpoint_count` | The maximum number of messages not checkpoint before forcing a checkpoint. | `None` | | `checkpoint_after` | The maximum duration of time (in seconds) before forcing a checkpoint. | `None` | | `max_subscriber_count` | The maximum number of subscribers allowed (`0` is unbounded). | `None` | | `live_buffer_size` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `None` | | `read_batch_size` | The number of events read at a time when paging through history. | `None` | | `history_buffer_size` | The number of events to cache when paging through history. | `None` | | `extra_statistics` | Whether to track latency statistics on this subscription. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | Please note, filter settings cannot be updated. #### Example Here's an example showing how to update a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.update_subscription_to_all( group_name="transaction-log-subscription", resolve_links=True, min_checkpoint_count=20 ) ``` @tab async ```python:no-line-numbers await client.update_subscription_to_all( group_name="transaction-log-subscription", resolve_links=True, min_checkpoint_count=20 ) ``` ::: ## Get subscription info Use `get_subscription_info()` to get a [`SubscriptionInfo`](#subscription-info) object for a persistent subscription group. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream (optional). | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Examples Here's an example showing how to get subscription information for a subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.get_subscription_info( group_name="stream-subscription", stream_name="order-123", ) ``` @tab async ```python:no-line-numbers await client.get_subscription_info( group_name="stream-subscription", stream_name="order-123", ) ``` ::: Here's an example showing how to get subscription information for a subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.get_subscription_info( group_name="transaction-log-subscription", ) ``` @tab async ```python:no-line-numbers await client.get_subscription_info( group_name="transaction-log-subscription", ) ``` ::: ## List subscriptions to stream Use `list_subscriptions_to_stream()` to return a list of [`SubscriptionInfo`](#subscription-info) objects describing persistent subscriptions to a named stream. | Parameter | Description | Default | |------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `stream_name` | Name of stream. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to get information for all persistent subscriptions to a stream. ::: tabs @tab sync ```python:no-line-numbers client.list_subscriptions_to_stream(stream_name="order-123") ``` @tab async ```python:no-line-numbers await client.list_subscriptions_to_stream(stream_name="order-123") ``` ::: ## List subscriptions Use `list_subscriptions()` to return a list of [`SubscriptionInfo`](#subscription-info) objects describing all existing persistent subscriptions. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Example Here's an example showing how to get information for all existing persistent subscriptions. ::: tabs @tab sync ```python:no-line-numbers client.list_subscriptions() ``` @tab async ```python:no-line-numbers await client.list_subscriptions() ``` ::: ## Delete subscription Use `delete_subscription()` to permanently delete a persistent subscription group. | Parameter | Description | Default | |----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `group_name` | Name of persistent subscription group. | | | `stream_name` | Name of stream (optional). | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | #### Examples Here's an example showing how to delete a persistent subscription to a stream. ::: tabs @tab sync ```python:no-line-numbers client.delete_subscription( group_name="stream-subscription", stream_name="order-123" ) ``` @tab async ```python:no-line-numbers await client.delete_subscription( group_name="stream-subscription", stream_name="order-123" ) ``` ::: Here's an example showing how to delete a persistent subscription to the global transaction log. ::: tabs @tab sync ```python:no-line-numbers client.delete_subscription( group_name="transaction-log-subscription" ) ``` @tab async ```python:no-line-numbers await client.delete_subscription( group_name="transaction-log-subscription" ) ``` ::: ## Subscription info The `SubscriptionInfo` objects returned by [`get_subscription_info()`](#get-subscription-info), [`list_subscriptions_to_stream()`](#list-subscriptions-to-stream), and [`list_subscriptions()`](#list-subscriptions) have the following fields. | Field | Type | |------------------------------------|------------------------------------------------------------------------------| | `event_source` | `str` | | `group_name` | `str` | | `status` | `str` | | `average_per_second` | `int` | | `total_items` | `int` | | `count_since_last_measurement` | `int` | | `last_checkpointed_event_position` | `str` | | `last_known_event_position` | `str` | | `resolve_links` | `bool` | | `start_from` | `str` | | `message_timeout` | `float` | | `extra_statistics` | `bool` | | `max_retry_count` | `int` | | `live_buffer_size` | `int` | | `history_buffer_size` | `int` | | `read_batch_size` | `int` | | `checkpoint_after` | `float` | | `min_checkpoint_count` | `int` | | `max_checkpoint_count` | `int` | | `read_buffer_count` | `int` | | `live_buffer_count` | `int` | | `retry_buffer_count` | `int` | | `total_in_flight_messages` | `int` | | `outstanding_messages_count` | `int` | | `consumer_strategy` | `Literal["DispatchToSingle", "RoundRobin", "Pinned", "PinnedByCorrelation"]` | | `max_subscriber_count` | `int` | | `parked_message_count` | `int` | | `connections` | `list[ConnectionInfo]` | The `ConnectionInfo` objects included in the `connections` field of `SubscriptionInfo` have the following fields. | Field | Type | |--------------------------------|---------------------| | `from_` | `str` | | `username` | `str` | | `average_items_per_second` | `int` | | `total_items` | `int` | | `count_since_last_measurement` | `int` | | `observed_measurements` | `list[Measurement]` | | `available_slots` | `int` | | `in_flight_messages` | `int` | | `connection_name` | `str` | The `Measurement` objects included in the `observed_measurements` field of `ConnectionInfo` have the following fields. | Field | Type | |-----------|---------| | `key` | `str` | | `value` | `int` | --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/projections.md' --- # Projections This guide describes Python client methods for working with [projections](@server/features/projections.md) in KurrentDB. ::: tip Projections require [event data](./appending-events.md#the-newevent-class) to be JSON. ::: ## Overview KurrentDB has a [projections subsystem](@server/features/projections.md) that lets you append new events or link existing events to streams in a reactive manner. The Python client has twelve methods for working with projections: * [`create_projection()`](#create-projection) * [`get_projection_state()`](#get-projection-state) * [`disable_projection()`](#disable-projection) * [`update_projection()`](#update-projection) * [`reset_projection()`](#reset-projection) * [`enable_projection()`](#enable-projection) * [`get_projection_statistics()`](#get-projection-statistics) * [`list_continuous_projection_statistics()`](#list-continuous-projection-statistics) * [`list_all_projection_statistics()`](#list-all-projection-statistics) * [`abort_projection()`](#abort-projection) * [`delete_projection()`](#delete-projection) * [`restart_projections_subsystem()`](#restart-projections-subsystem) Let's get started by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false&defaultDeadline=5" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false&defaultDeadline=5" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: ## Create projection Use the `create_projection()` method to create a "continuous" projection with a Javascript "query". The `emit_enabled` argument must be `True` if the `query` code includes a call to `.emit()` otherwise the projection will not run. If `track_emitted_streams` is `True` then any emitted emitted streams can be optionally deleted when a projection is deleted. See [`delete_projection()`](#delete-projection) for more details. If the `engine` is `"v2"`, the projection will run on the version 2 projections engine, which is more reliable. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `query` | Javascript projection code, defines what the projection will do. | | | `engine` | Projection engine version on which to run this projection. Acceptable values are `"v1"` and `"v2"`. | `"v1"` | | `emit_enabled` | Whether a projection will be able to emit events. | `False` | | `track_emitted_streams` | Whether emitted streams are tracked. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns `None`. ### Example The Javascript code below will project the stream named `"order-123"`. It will: * initialise the projection's current state to a Javascript object with "count" and "list" values; and * for each appended `OrderCreated` event in the projected stream: * increment "count" value; and * and emit an `Emitted` event to stream `"emitted-order-123"`. ```python:no-line-numbers projection_query = """ fromStream("order-123") .when({ $init: function(){ return { count: 0, list: [null, "2.10", true] }; }, OrderCreated: function(s,e){ s.count += 1; emit("emitted-order-123", "Emitted", {}, {}); } }) .outputState() """ ``` Now let's create a projection that uses this query. ::: tabs @tab sync ```python:no-line-numbers client.create_projection( name="projection-order-123", query=projection_query, emit_enabled=True, track_emitted_streams=True, ) ``` @tab async ```python:no-line-numbers await client.create_projection( name="projection-order-123", query=projection_query, emit_enabled=True, track_emitted_streams=True, ) ``` ::: ## Get projection state Use the `get_projection_state()` method to get a projection's current state. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `partition` | Projection partition (optional). | `""` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success this method returns a `ProjectionState` object with a `value` attribute that corresponds to the current state of the projection. In the example below, the current state of the projection is a Python `dict` that has a `"count"` value and a `"list"` value. These values correspond to the projection query and events from the [previous example](#create-projection). ::: tabs @tab sync ```python:no-line-numbers from time import sleep sleep(1) state = client.get_projection_state("projection-order-123") assert 1 == state.value["count"] assert [None, "2.10", True] == state.value["list"] ``` @tab async ```python:no-line-numbers from time import sleep sleep(1) state = await client.get_projection_state("projection-order-123") assert 1 == state.value["count"] assert [None, "2.10", True] == state.value["list"] ``` ::: ## Disable projection Use the `disable_projection()` method to stop a projection processing new events. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. The example below stops the `"order-123""` projection. ::: tabs @tab sync ```python:no-line-numbers client.disable_projection(name="projection-order-123") ``` @tab async ```python:no-line-numbers await client.disable_projection(name="projection-order-123") ``` ::: ## Update projection Use the `update_projection()` method to adjust the projection query. If `query` includes a call to `.emit()`, the `emit_enabled` argument must be `True`, otherwise the projection will not run. A projection must be disabled before it can be updated. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `query` | Javascript projection code, defines what the projection will do. | | | `emit_enabled` | Whether a projection will be able to emit events. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.update_projection( name="projection-order-123", query=projection_query, emit_enabled=True, ) ``` @tab async ```python:no-line-numbers await client.update_projection( name="projection-order-123", query=projection_query, emit_enabled=True, ) ``` ::: ## Reset projection Use the `reset_projection()` method to reset the current state of a projection. A projection must be disabled before it can be reset. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.reset_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.reset_projection( name="projection-order-123", ) ``` ::: ## Enable projection Use the `enable_projection()` method to start a projection that has been disabled. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.enable_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.enable_projection( name="projection-order-123", ) ``` ::: ## Get projection statistics Use the `get_projection_statistics()` method to get statistics for a projection. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a [`ProjectionStatistics`](#the-projectionstatistics-class) object. ::: tabs @tab sync ```python:no-line-numbers statistics = client.get_projection_statistics( name="projection-order-123", ) assert "Running" == statistics.status ``` @tab async ```python:no-line-numbers statistics = await client.get_projection_statistics( name="projection-order-123", ) assert "Running" == statistics.status ``` ::: ## List continuous projection statistics Use the `list_continuous_projection_statistics()` method to get a list of statistics for all continuous projections. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a list of [`ProjectionStatistics`](#the-projectionstatistics-class) objects. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.projections import ProjectionStatistics statistics = client.list_continuous_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` @tab async ```python:no-line-numbers from kurrentdbclient.projections import ProjectionStatistics statistics = await client.list_continuous_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` ::: ## List all projection statistics Use the `list_all_projection_statistics()` method to get a list of statistics for all projections. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method returns a list of [`ProjectionStatistics`](#the-projectionstatistics-class) objects. ::: tabs @tab sync ```python:no-line-numbers statistics = client.list_all_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` @tab async ```python:no-line-numbers statistics = await client.list_all_projection_statistics() assert isinstance(statistics, list) assert 0 < len(statistics) assert isinstance(statistics[0], ProjectionStatistics) ``` ::: ## Abort projection Use the `abort_projection()` method to abort a projection. | Parameter | Description | Default | |---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.abort_projection( name="projection-order-123", ) ``` @tab async ```python:no-line-numbers await client.abort_projection( name="projection-order-123", ) ``` ::: ## Delete projection Use the `delete_projection()` method to delete a projection. A projection must be disabled before it can be deleted. Attempting to delete a projection that is running will raise an `OperationFailedError` exception. | Parameter | Description | Default | |----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `name` | Name of the projection. | | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.disable_projection(name="projection-order-123") client.delete_projection( "projection-order-123", delete_emitted_streams=True, delete_state_stream=True, delete_checkpoint_stream=True, ) ``` @tab async ```python:no-line-numbers await client.disable_projection(name="projection-order-123") await client.delete_projection( "projection-order-123", delete_emitted_streams=True, delete_state_stream=True, delete_checkpoint_stream=True, ) ``` ::: On success, this method will return `None`. ## Restart projections subsystem Use the `restart_projections_subsystem()` method to restart the projections subsystem. | Parameter | Description | Default | |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | On success, this method will return `None`. ::: tabs @tab sync ```python:no-line-numbers client.restart_projections_subsystem() ``` @tab async ```python:no-line-numbers await client.restart_projections_subsystem() ``` ::: ## The ProjectionStatistics class The `ProjectionStatistics` dataclass is defined with the following fields: | Field | Type | Description | |--------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `core_processing_time` | `int` | The total time, in ms, the projection took to handle events since the last restart. | | `version` | `int` | This is used internally, the version is increased when the projection is edited or reset. | | `epoch` | `int` | This is used internally, the epoch is increased when the projection is reset. | | `effective_name` | `str` | The name of the projection. | | `writes_in_progress` | `int` | The number of write requests to emitted streams currently in progress, these writes can be batches of events. | | `reads_in_progress` | `int` | The number of read requests currently in progress. | | `partitions_cached` | `int` | The number of cached projection partitions. | | `status` | `str` | A human readable string of the current statuses of the projection (see below). | | `state_reason` | `str` | A human readable string explaining the reason of the current projection state. | | `name` | `str` | The name of the projection. | | `mode` | `str` | `Continuous`, `OneTime` , `Transient` | | `position` | `str` | The position of the last processed event. | | `progress` | `float` | The progress, in %, indicates how far this projection has processed event, in case of a restart this could be -1% or some number. It will be updated as soon as a new event is appended and processed. | | `last_checkpoint` | `str` | The position of the last checkpoint of this projection. | | `events_processed_after_restart` | `int` | The number of events processed since the last restart of this projection. | | `checkpoint_status` | `str` | A human readable string explaining the current operation performed on the checkpoint: `requested`, `writing`. | | `buffered_events` | `int` | The number of events in the projection read buffer. | | `write_pending_events_before_checkpoint` | `int` | The number of events waiting to be appended to emitted streams before the pending checkpoint can be written. | | `write_pending_events_after_checkpoint` | `int` | The number of events to be appended to emitted streams since the last checkpoint. | The `status` string is a combination of the following values. The first three are the most common one, as the other one are transient values while the projection is initialised or stopped. | Value | Description | |---------------------|------------------------------------------------------------------------------------------------------------------------| | Running | The projection is running and processing events. | | Stopped | The projection is stopped and is no longer processing new events. | | Faulted | An error occurred in the projection, StateReason will give the fault details, the projection is not processing events. | | Initial | This is the initial state, before the projection is fully initialised. | | Suspended | The projection is suspended and will not process events, this happens while stopping the projection. | | LoadStateRequested | The state of the projection is being retrieved, this happens while the projection is starting. | | StateLoaded | The state of the projection is loaded, this happens while the projection is starting. | | Subscribed | The projection has successfully subscribed to its readers, this happens while the projection is starting. | | FaultedStopping | This happens before the projection is stopped due to an error in the projection. | | Stopping | The projection is being stopped. | | CompletingPhase | This happens while the projection is stopping. | | PhaseCompleted | This happens while the projection is stopping. | --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/reading-events.md' --- # Reading events This guide describes Python client methods for reading events from KurrentDB. ## Overview The [Python clients for KurrentDB](getting-started.md#python-clients-for-kurrentdb) have four methods for reading events: * [`get_stream()`](#get-stream) – returns a Python `tuple` of events from a **named stream** * [`read_stream()`](#read-stream) – returns a streaming iterable of events from a named stream * [`read_all()`](#read-all) – returns a streaming iterable of events from **global transaction log** * [`read_index()`](#read-index) – returns a streaming iterable of events from a **secondary index** ## Recorded events Recorded events are presented as `RecordedEvent` objects. A `RecordedEvent` object specifies the type string, binary data, metadata, content type, and ID of a [new event](./appending-events.md#the-newevent-class) that has been recorded. Additionally, it specifies the event's stream name and stream position, the commit and prepare position, the recorded time, and possibly a link event and a persistent subscription consumer group retry count. | Field | Type | Description | |--------------------|-----------------------|---------------------------------------------------------------------------------------| | `type` | `str` | The type of the event | | `data` | `bytes` | The content of the event | | `metadata` | `bytes` | Event metadata | | `content_type` | `str` | The format of the content | | `id` | `UUID` | A unique ID for the event | | `stream_name` | `str` | A unique ID for the event | | `stream_position` | `int` | Position of the event in the stream | | `commit_position` | `int` | Position of the event in the global transaction log | | `recorded_at` | `datetime\|None` | Timestamp added by KurrentDB | | `link` | `RecordedEvent\|None` | Resolved link event | | `retry_count` | `int\|None` | Number of times this event has been sent to a persistence subscription consumer group | You will never need to construct a `RecordedEvent` object. However, all events returned from KurrentDB by the Python clients are presented as `RecordedEvent` objects, and so it is important to understand these fields. ## Get stream The `get_stream()` method returns event records from a stream in KurrentDB. This is a convenient alternative to [`read_stream()`](#read-stream), that converts a streaming iterable into a Python `tuple`. | Parameter | Description | Default | |-------------------|--------------------------------------------------------------------------------------------------------------------------------------|----------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `get_stream()` method returns a `tuple` of [`RecordedEvent`](#recorded-events) objects. ### Examples Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the events to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1, event2, event3], ) ``` ::: ### Reading forwards The simplest way to get stream events is to supply a `stream_name` argument. This is a typical operation when retrieving events to construct a decision model in a command handler. Here's an example of getting all events from stream `"order-123"`. ::: tabs @tab sync ```python:no-line-numbers for event in client.get_stream(stream_name="order-123"): print(f"Event: {event.type} at position {event.stream_position}") ``` @tab async ```python:no-line-numbers for event in await client.get_stream(stream_name="order-123"): print(f"Event: {event.type} at position {event.stream_position}") ``` ::: ### Reading backwards Set the `backwards` parameter to `True` to get stream events in reverse order. ::: tabs @tab sync ```python:no-line-numbers # Get all events backwards from the end for event in client.get_stream( stream_name="order-123", backwards=True, ): assert event.stream_position == 2 break ``` @tab async ```python:no-line-numbers # Get all events backwards from the end for event in await client.get_stream( stream_name="order-123", backwards=True, ): assert event.stream_position == 2 break ``` ::: :::tip Get stream event backwards with a limit of `1` to find the last position in the stream. Alternatively, call the more convenient method `get_current_version()`. ::: ### Limited number Passing in a `limit` argument allows you to restrict the number of events that are returned. In the example below, we read a maximum of two events from the stream: ::: tabs @tab sync ```python:no-line-numbers events = client.get_stream( stream_name="order-123", limit=2 ) assert len(events) == 2 ``` @tab async ```python:no-line-numbers events = await client.get_stream( stream_name="order-123", limit=2 ) assert len(events) == 2 ``` ::: ### From stream position Specifying a `stream_position` argument will get events from a specific position. This is useful, for example, when advancing a snapshot of an aggregate to the latest current state. Getting stream events from a specific position is inclusive, which means the event at that position will be returned by the response. ::: tabs @tab sync ```python:no-line-numbers # Get events from a specific stream position for event in client.get_stream( stream_name="order-123", stream_position=1, ): assert event.stream_position == 1 break ``` @tab async ```python:no-line-numbers # Get events from a specific stream position for event in await client.get_stream( stream_name="order-123", stream_position=1, ): assert event.stream_position == 1 break ``` ::: ### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers for event in client.get_stream( stream_name="$et-OrderCreated", resolve_links=True ): assert event.type == "OrderCreated" ``` @tab async ```python:no-line-numbers for event in await client.get_stream( stream_name="$et-OrderCreated", resolve_links=True ): assert event.type == "OrderCreated" ``` ::: ### Not found error Reading a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: await client.get_stream(stream_name="not-a-stream") except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Read stream The `read_stream()` method returns event records from a stream in KurrentDB. You can read all the events from an individual stream, starting from any position in the stream, and can read either forwards or backwards, and request a limited number of events. | Parameter | Description | Default | |-------------------|------------------------------------------------------------------------------------------|---------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `read_stream()` method returns a streaming iterable of [`RecordedEvent`](#recorded-events) objects. If the stream does not exist, the `read_stream()` method raises a `NotFoundError` exception. Alternatively, use [`get_stream()`](#get-stream) to get a Python `tuple`. ### Reading forwards The simplest way to read a stream is to supply a `stream_name` argument and read every event already recorded in that stream. This is a typical operation when retrieving events to construct a decision model in a command handler. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream(stream_name="order-123") as events: for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` @tab async ```python:no-line-numbers async with await client.read_stream(stream_name="order-123") as events: async for event in events: print(f"Event: {event.type} at position {event.stream_position}") ``` ::: ### Reading backwards Set `backwards=True` to read stream events in reverse order. ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end with client.read_stream( stream_name="order-123", backwards=True, ) as events: for event in events: assert event.stream_position == 2 break ``` @tab async ```python:no-line-numbers # Read all events backwards from the end async with await client.read_stream( stream_name="order-123", backwards=True, ) as events: async for event in events: assert event.stream_position == 2 break ``` ::: :::tip Read backwards with a limit of `1` to find the last position in the stream. Alternatively, call the convenience Python client method `get_current_version()`. ::: ### Limited number Passing in a `limit` argument allows you to restrict the number of events that are returned. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream( stream_name="order-123", limit=2 ) as events: assert len(tuple(events)) == 2 ``` @tab async ```python:no-line-numbers async with await client.read_stream( stream_name="order-123", limit=2 ) as events: assert len([e async for e in events]) == 2 ``` ::: ### From stream position Specifying a `stream_position` argument will start reading from a specific position in the stream. This is useful, for example, when advancing a snapshot of an aggregate to the latest current state. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific stream position with client.read_stream( stream_name="order-123", stream_position=1, ) as events: for event in events: assert event.stream_position == 1 break ``` @tab async ```python:no-line-numbers # Read from a specific stream position async with await client.read_stream( stream_name="order-123", stream_position=1, ) as events: async for event in events: assert event.stream_position == 1 break ``` ::: Please note, reading a stream from a specific position is inclusive, which means the event at that position will be returned by the response. ### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.read_stream( stream_name="$et-OrderCreated", resolve_links=True ) as events: for event in events: assert event.type == "OrderCreated" ``` @tab async ```python:no-line-numbers async with await client.read_stream( stream_name="$et-OrderCreated", resolve_links=True ) as events: async for event in events: assert event.type == "OrderCreated" ``` ::: ### Not found error Reading a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: with client.read_stream( stream_name="not-a-stream" ) as events: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: async with await client.read_stream( stream_name="not-a-stream" ) as events: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Read all The `read_all()` method read events from the global transaction log. No arguments are required when reading from the global transaction log. You can start from a particular commit position, read events backwards, and read a limited number of events. You can also filter events by type string or stream name. | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| | `commit_position` | Position from which to start reading events. | `None` | | `backwards` | Activate reading of events in reverse order. | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name rather than event type. | `False` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `read_all()` method returns a streaming iterable of [`RecordedEvent`](#recorded-events) objects. ### Server-side filtering KurrentDB supports server-side filtering of events while reading from, or subscribing to, the global transaction log, so that you can receive only the events you care about. You can filter by event type or stream name using regular expressions. The `filter_include` and `filter_exclude` parameters are designed to have exactly the opposite effect from each other, so that a sequence of strings given to `filter_include` will return exactly those events which would be excluded if the same argument value were used with `filter_exclude`. And vice versa, so that a sequence of strings given to `filter_exclude` will return exactly those events that would not be included if the same argument value were used with `filter_include`. The `filter_include` parameter takes precedence over `filter_exclude`. That is to say, if you pass arguments for both, the `filter_exclude` argument will be ignored. The `filter_include` and `filter_exclude` parameters are typed as `Sequence[str]` which means that you can either pass a single `str`, or a collection of `str`. The `str` value or values should be unanchored regular expression patterns. If you supply a collection of `str`, they will be concatenated together by the Python client as bracketed alternatives in a larger regular expression that is anchored to the start and end of the strings being matched. So there is no need to include the `'^'` and `'$'` anchor assertions. KurrentDB generates "system events" that all have a `type` that begins with `"$"`. By default, system events are excluded, along with `PersistentConfig` and `Result` events. If you want to also exclude other types of events, then use an argument for `filter_exclude` that adds to the default argument value `DEFAULT_EXCLUDE_FILTER`. If you especially want to include system events, then you can override the default filter by passing an empty sequence as the `filter_exclude` argument. If you want to select only for system events, then specify a suitable `filter_include` argument. You should use wildcards if you want to match substrings. For example, `"Order.*"` matches all strings that start with `"Order"`. Alternatively,`".*Snapshot"` matches all strings that end with `"Snapshot"`. Characters that are metacharacters with special meaning in regular expressions, such as `.` `*` `+` `?` `^` `$` `|` `(` `)` `[` `]` `{` `}` `\` must be escaped to be used literally when matching event types and stream names. Python's raw string literals can help to avoid doubling of escape backslashes. For example `r"\$.*"` can be used to match system event types that all start with the `$` character. ### Reading forwards The simplest way to read events from the global transaction log is to call `read_all()` without arguments. ::: tabs @tab sync ```python:no-line-numbers # Read all events from the beginning with client.read_all() as events: # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` @tab async ```python:no-line-numbers # Read all events from the beginning async with await client.read_all() as events: # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` ::: ### Reading backwards Set `backwards=True` to read the global transaction log backwards from the end. ::: tabs @tab sync ```python:no-line-numbers # Read all events backwards from the end with client.read_all(backwards=True) as events: ... # Read backwards from a specific commit position with client.read_all( commit_position=commit_position, backwards=True, ) as events: ... ``` @tab async ```python:no-line-numbers # Read all events backwards from the end async with await client.read_all(backwards=True) as events: ... # Read backwards from a specific commit position async with await client.read_all( commit_position=commit_position, backwards=True, ) as events: ... ``` ::: :::tip Read one event backwards to find the last position in the global transaction log. Alternatively, call the more convenient Python client method `get_commit_position()`. ::: ### Limited number Passing in a `limit` allows you to restrict the number of events that are returned. In the example below, we read a maximum of 100 events: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( limit=100 ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( limit=100 ) as events: ... ``` ::: ### From commit position You can also start reading from a specific position in the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific commit position with client.read_all( commit_position=commit_position ) as events: ... ``` @tab async ```python:no-line-numbers # Read from a specific commit position async with await client.read_all( commit_position=commit_position ) as events: ... ``` ::: Please note, an `InvalidCommitPositionError` exception will be raised if the commit position does not exist. ### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.read_all( resolve_links=True ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( resolve_links=True ) as events: ... ``` ::: ### Filtering examples You can read more selectively from the global transaction log with [server-side filtering](#server-side-filtering) by supplying an argument for either the `filter_include` or the `filter_exclude` parameters. By default, events will be filtered by `type`. Alternatively, you can filter events by `stream_name` name by setting the `filter_by_stream_name` parameter to `True`. Here's an example that reads all events that have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_include=["Order.*"] ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_include=["Order.*"] ) as events: ... ``` ::: Here's an example that selects all events that do not have a `type` starting with `"Order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_exclude=["Order.*"] ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_exclude=["Order.*"] ) as events: ... ``` ::: Here's an example that selects all events that have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_include=["order.*"], filter_by_stream_name=True, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_include=["order.*"], filter_by_stream_name=True, ) as events: ... ``` ::: Here's an example that selects all events that do not have a `stream_name` starting with `"order"`: ::: tabs @tab sync ```python:no-line-numbers with client.read_all( filter_exclude=["order.*"], filter_by_stream_name=True, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_all( filter_exclude=["order.*"], filter_by_stream_name=True, ) as events: ... ``` ::: ## Read index ::: info Supported by KurrentDB 25.1 and later. ::: The `read_index()` method reads events from a secondary index in KurrentDB. You can read events from a secondary index starting from any commit position. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `index_name` | Name of secondary index (`"$idx-"` prefix is optional). | | | `commit_position` | Position from which to start reading events. | `None` | | `limit` | Maximum number of events to return. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `read_index()` method returns a streaming iterable of [`RecordedEvent`](#recorded-events) objects. ### Reading forwards The simplest way to read from a secondary index is to call `read_index()` with the name of an index. Here's an example of reading all events with type string `"OrderCreated"`. ::: tabs @tab sync ```python:no-line-numbers # Read all OrderCreated events with client.read_index( index_name="et-OrderCreated" ) as events: # Iterate through the sync streaming response with a 'for' loop for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` @tab async ```python:no-line-numbers # Read all OrderCreated events async with await client.read_index( index_name="et-OrderCreated" ) as events: # Iterate through the async streaming response with an 'async for' loop async for event in events: print(f"Event: {event.type} from stream {event.stream_name}") ``` ::: ### From commit position You can also start reading a secondary index from a specific position in the global transaction log. Here's an example of reading a secondary index from a specific commit position. ::: tabs @tab sync ```python:no-line-numbers # Read from a specific commit position with client.read_index( index_name="et-OrderCreated", commit_position=commit_position, ) as events: for event in events: break ``` @tab async ```python:no-line-numbers # Read from a specific commit position async with await client.read_index( index_name="et-OrderCreated", commit_position=commit_position, ) as events: async for event in events: break ``` ::: Please note, an `InvalidCommitPositionError` exception will be raised if the commit position does not exist. ### Limited number Passing in a `limit` allows you to restrict the number of events that are returned. Here's an example of reading a maximum of 100 events from a secondary index. ::: tabs @tab sync ```python:no-line-numbers with client.read_index( index_name="et-OrderCreated", limit=100, ) as events: ... ``` @tab async ```python:no-line-numbers async with await client.read_index( index_name="et-OrderCreated", limit=100, ) as events: ... ``` ::: --- --- url: 'https://docs.kurrent.io/clients/python/v1.3/subscriptions.md' --- # Catch-up subscriptions This guide describes Python client methods for catch-up subscriptions to KurrentDB. Catch-up subscription methods are like the [read methods](./reading-events.md). The difference is that the streaming iterator responses from read methods stop when all recorded events have been received, whereas catch-up subscriptions block and then continue as new events are recorded. ## Overview You can subscribe to individual streams, to the global transaction log, and to a secondary indexes. The Python clients for KurrentDB have three methods for catch-up subscriptions. * [`subscribe_to_stream()`](#subscribe-to-stream) – returns a catch-up subscription to a stream * [`subscribe_to_all()`](#subscribe-to-all) - returns a catch-up subscription to global transaction log * [`subscribe_to_index()`](#subscribe-to-index) – returns a catch-up subscription to a secondary index ## Subscribe to stream The `subscribe_to_stream()` method returns a catch-up subscription to a stream. The only required argument is the name of a stream. You can subscribe to all the events in a stream, or a sample of the events from the named stream, optionally starting after a specific stream position or the end of the stream. | Parameter | Description | Default | |-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `stream_name` | Stream from which events will be read. | | | `stream_position` | Position after which to start reading events. | `None` | | `from_end` | Read from the end of the stream (new events only). | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `include_caught_up` | Receive "caught up" messages when iterating the response. | `False` | | `include_fell_behind` | Receive "fell behind" messages when iterating the response. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `subscribe_to_stream()` method returns an iterable of `RecordedEvent` objects. If the stream does not exist, a `NotFoundError` exception will be raised . ### Examples Let's set up the examples by [connecting to KurrentDB](./getting-started.md#connecting-to-kurrentdb) and [appending new events](./appending-events.md). ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import KurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = KurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the first event to a new stream commit_position = client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1], ) # Append second and third event to the same stream. client.append_to_stream( stream_name="order-123", current_version=0, events=[event2, event3], ) ``` @tab async ```python:no-line-numbers from kurrentdbclient import AsyncKurrentDBClient, NewEvent, StreamState # Connect to KurrentDB uri = "kurrentdb://127.0.0.1:2113?tls=false" client = AsyncKurrentDBClient(uri) # Construct new event objects event1 = NewEvent( type="OrderCreated", data=b'{"order_id": "order-123"}', ) event2 = NewEvent( type="OrderUpdated", data=b'{"status": "processing"}', ) event3 = NewEvent( type="OrderUpdated", data=b'{"status": "shipped"}', ) # Append the first event to a new stream commit_position = await client.append_to_stream( stream_name="order-123", current_version=StreamState.NO_STREAM, events=[event1], ) # Append second and third event to the same stream. await client.append_to_stream( stream_name="order-123", current_version=0, events=[event2, event3], ) ``` ::: ### Basic subscription The simplest way to subscribe to a stream is to supply a `stream_name` argument. ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in a stream (use context manager for auto-cleanup) with client.subscribe_to_stream(stream_name="order-123") as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: assert event.stream_position == 0 assert event.id == event1.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in a stream (use context manager for auto-cleanup) async with await client.subscribe_to_stream(stream_name="order-123") as subscription: # Iterate through the subscription with an 'async for' loop async for event in subscription: assert event.stream_position == 0 assert event.id == event1.id break # <-- so we can continue with the examples ``` ::: ### After stream position Specifying a `stream_position` argument will get events after that position. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific stream position with client.subscribe_to_stream( stream_name="order-123", stream_position=1, ) as subscription: for event in subscription: assert event.stream_position == 2 assert event.id == event3.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Get events after a specific stream position async with await client.subscribe_to_stream( stream_name="order-123", stream_position=1, ) as subscription: async for event in subscription: assert event.stream_position == 2 assert event.id == event3.id break # <-- so we can continue with the examples ``` ::: ### From end of stream Here's an example of subscribing from the end of a stream for "live events" only. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_stream( stream_name="order-123", from_end=True, ) as subscription: ... ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_stream( stream_name="order-123", from_end=True, ) as subscription: ... ``` ::: ### Resolving link events When you subscribe to a stream with link events (e.g., category streams), set `resolve_links` to `True`. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_stream( stream_name="$et-OrderCreated", resolve_links=True ) as subscription: for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_stream( stream_name="$et-OrderCreated", resolve_links=True ) as subscription: async for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` ::: Link events point to events in other streams in KurrentDB. These are generally created by projections such as the by-event-type projection which links events of the same event type into the same stream. This makes it easy to look up all events of a specific type. However, it may be faster to use a [filtered subscription](#subscribe-to-all) for a specific type or stream name prefix than subscribing to the corresponding system projection. ### Stream not found error Subscribing to a stream that doesn't exist will raise a `NotFoundError` exception. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: with client.subscribe_to_stream( stream_name="not-a-stream" ) as subscription: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` @tab async ```python:no-line-numbers from kurrentdbclient.exceptions import NotFoundError try: async with await client.subscribe_to_stream( stream_name="not-a-stream" ) as subscription: ... except NotFoundError: print("Success: Stream does not exist") except Exception as e: print(f"Shouldn't get here") ``` ::: ## Subscribe To all The `subscribe_to_all()` method returns a catch-up subscription to the global transaction log. A catch-up subscription to the global transaction log will return all events in chronological order. You can start after a particular commit position, or from the end of the log so that only new events are received. You can also filter events by type string or by stream name. See notes on [filtering the global transaction log](./reading-events.md#server-side-filtering). | Parameter | Description | Default | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| | `commit_position` | Position after which to start reading events. | `None` | | `from_end` | Read from the end of the database (new events only). | `False` | | `resolve_links` | Activate resolution of "link events". | `False` | | `filter_exclude` | [Patterns](./reading-events.md#server-side-filtering) for excluding events. | System events | | `filter_include` | [Patterns](./reading-events.md#server-side-filtering) for including events (if set, only matching events will be returned). | `()` | | `filter_by_stream_name` | Filter by stream name (default is to filter by event type). | `False` | | `include_caught_up` | Receive "caught up" messages when iterating the response. | `False` | | `include_fell_behind` | Receive "fell behind" messages when iterating the response. | `False` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `subscribe_to_all()` method returns an iterable of `RecordedEvent` objects. ### Examples Let's see how to use `subscribe_to_all()` by looking at some examples. ### Basic subscription ::: tabs @tab sync ```python:no-line-numbers # Subscribe to all events in global transaction log with client.subscribe_to_all() as subscription: # Iterate through the subscription with a 'for' loop for event in subscription: print(f"Event: {event.type} at position {event.commit_position}") break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Subscribe to all events in global transaction log async with await client.subscribe_to_all() as subscription: # Iterate through the subscription with an async 'for' loop async for event in subscription: print(f"Event: {event.type} at position {event.commit_position}") break # <-- so we can continue with the examples ``` ::: ### After commit position Specifying a `commit_position` argument will get events after that position in the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific commit position with client.subscribe_to_all( commit_position=commit_position, ) as subscription: for event in subscription: assert event.id == event2.id break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Get events after a specific commit position async with await client.subscribe_to_all( commit_position=commit_position, ) as subscription: async for event in subscription: assert event.id == event2.id break # <-- so we can continue with the examples ``` ::: ### Live events only Here's an example of subscribing from the end of the global transaction log. ::: tabs @tab sync ```python:no-line-numbers # Get events after a specific stream position with client.subscribe_to_all(from_end=True) as subscription: ... ``` @tab async ```python:no-line-numbers # Get events after a specific stream position async with await client.subscribe_to_all(from_end=True) as subscription: ... ``` ::: ### Resolving link events KurrentDB projections can create "link events" that are pointers to events you have appended to a stream. Set `resolve_links=True` so that KurrentDB will resolve the "link events" and return the linked events. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_all(resolve_links=True) as subscription: ... ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_all(resolve_links=True) as subscription: ... ``` ::: ### Filtering by event type Here's an example of filtering for certain event types. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_all( filter_include=["OrderCreated", "OrderUpdated"], ) as subscription: for event in subscription: assert event.type in ["OrderCreated", "OrderUpdated"] break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_all( filter_include=["OrderCreated", "OrderUpdated"], ) as subscription: async for event in subscription: assert event.type in ["OrderCreated", "OrderUpdated"] break # <-- so we can continue with the examples ``` ::: ### Filtering by stream name Here's an example of filtering for a stream category. ::: tabs @tab sync ```python:no-line-numbers # Filter by stream name prefix with client.subscribe_to_all( filter_include=["order-.*"], filter_by_stream_name=True ) as subscription: for event in subscription: assert event.stream_name.startswith("order") break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers # Filter by stream name prefix async with await client.subscribe_to_all( filter_include=['order-.*'], filter_by_stream_name=True ) as subscription: async for event in subscription: assert event.stream_name.startswith("order") break # <-- so we can continue with the examples ``` ::: ### Checkpointing When a catch-up subscription to the global transaction log is used to process events, you can checkpoint progress by recording the commit position of the last processed event. If you record commit positions in the same atomic database transaction as the results of processing an event, and with a uniqueness constraint, and you resume using the last recorded position, the processing of events from the global transaction log will immediately have "exactly once" semantics. If you are filtering the subscription, you can set `include_checkpoints=True` to cause KurrentDB occasionally to send the commit positions of events that have been excluded from the catch-up subscription, so that progress across large gaps can also be checkpointed. These emerge from the catch-up subscription as `Checkpoint` objects. Please note, occasionally KurrentDB will send a checkpoint with the same commit position as a recorded event, which means you must check first to see if an item is a `RecordedEvent` before recording the commit position of a `Checkpoint` object. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import Checkpoint, RecordedEvent def process_events_with_checkpointing(client, projection): # Get the last checkpoint last_commit_position = projection.get_last_checkpoint() # Subscribe using the last checkpoint with client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) as subscription: for item in subscription: if type(item) is RecordedEvent: # Regular event processing new_state = {"key": "value"} # Record commit position with new state projection.update_state(new_state, item.commit_position) elif type(item) is Checkpoint: # Record commit position projection.save_checkpoint(item.commit_position) break # <-- so we can continue with the examples class Projection: def __init__(self): self._last_checkpoint = None self._current_state = {} def get_last_checkpoint(self): return self._last_checkpoint def save_checkpoint(self, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is None or checkpoint > self._last_checkpoint ): self._last_checkpoint = checkpoint def update_state(self, state, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is not None and checkpoint <= self._last_checkpoint ): msg = f"Checkpoint conflict: {checkpoint} <= {self._last_checkpoint}" raise ValueError(msg) self._last_checkpoint = checkpoint self._current_state = state process_events_with_checkpointing(client, Projection()) ``` @tab async ```python:no-line-numbers from kurrentdbclient import Checkpoint, RecordedEvent async def process_events_with_checkpointing(client, projection): # Get the last checkpoint last_commit_position = await projection.get_last_checkpoint() # Subscribe using the last checkpoint async with await client.subscribe_to_all( commit_position=last_commit_position, include_checkpoints=True ) as subscription: async for item in subscription: if type(item) is RecordedEvent: # Regular event processing new_state = {"key": "value"} # Record commit position with new state await projection.update_state(new_state, item.commit_position) elif type(item) is Checkpoint: # Record commit position await projection.save_checkpoint(item.commit_position) break # <-- so we can continue with the examples class Projection: def __init__(self): self._last_checkpoint = None self._current_state = {} async def get_last_checkpoint(self): return self._last_checkpoint async def save_checkpoint(self, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is None or checkpoint > self._last_checkpoint ): self._last_checkpoint = checkpoint async def update_state(self, state, checkpoint): # Only update if checkpoint is greater than last recorded. if ( self._last_checkpoint is not None and checkpoint <= self._last_checkpoint ): msg = f"Checkpoint conflict: {checkpoint} <= {self._last_checkpoint}" raise ValueError(msg) self._last_checkpoint = checkpoint self._current_state = state await process_events_with_checkpointing(client, Projection()) ``` ::: The same principles can be applied when processing events from a stream or a secondary index. ## Subscribe to Index The `subscribe_to_index()` method returns a catch-up subscription to a secondary index. You can subscribe to all the events in a secondary index, optionally starting after a commit position. | Parameter | Description | Default | |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | `index_name` | Name of secondary index (`"$idx-"` prefix is optional). | | | `commit_position` | Position after which to start reading events. | `None` | | `timeout` | Maximum duration of operation (in seconds). | `None` | | `credentials` | [Override credentials](./getting-started.md#overriding-user-credentials) derived from [client configuration](./getting-started.md#client-configuration). | `None` | The `subscribe_to_index()` method returns an iterable of `RecordedEvent` objects. ::: info Supported by KurrentDB 25.1 and later. ::: ### Examples Let's see how to use `subscribe_to_index()` by looking at some examples. ### Basic subscription The example below subscribes to all events in the `"et-OrderUpdated"` index. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_index( index_name="et-OrderCreated" ) as subscription: for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_index( index_name="et-OrderCreated" ) as subscription: async for event in subscription: assert event.type == "OrderCreated" break # <-- so we can continue with the examples ``` ::: ### After commit position The example below subscribes to the secondardy index after a specific commit position. ::: tabs @tab sync ```python:no-line-numbers with client.subscribe_to_index( index_name="et-OrderUpdated", commit_position=commit_position, ) as subscription: for event in subscription: assert event.type == "OrderUpdated" break # <-- so we can continue with the examples ``` @tab async ```python:no-line-numbers async with await client.subscribe_to_index( index_name="et-OrderUpdated", commit_position=commit_position, ) as subscription: async for event in subscription: assert event.type == "OrderUpdated" break # <-- so we can continue with the examples ``` ::: ## Handling dropped subscriptions An application which hosts the subscription can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. You should implement retry logic to handle such cases. ::: tabs @tab sync ```python:no-line-numbers import time from kurrentdbclient.exceptions import ConsumerTooSlowError, GrpcError projection = Projection() retries = 5 while True: try: process_events_with_checkpointing(client, projection) break # <-- so we can continue with the examples except (ConsumerTooSlowError, GrpcError): if retries <= 0: raise retries -= 1 time.sleep(5) continue ``` @tab async ```python:no-line-numbers import time from kurrentdbclient.exceptions import ConsumerTooSlowError, GrpcError projection = Projection() retries = 5 while True: try: await process_events_with_checkpointing(client, projection) break # <-- so we can continue with the examples except (ConsumerTooSlowError, GrpcError): if retries <= 0: raise retries -= 1 time.sleep(5) continue ``` ::: ## Handling subscription state changes When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up". You can detect this transition by using the `include_caught_up` parameter of the catch-up subscription methods. ::: tabs @tab sync ```python:no-line-numbers from kurrentdbclient import CaughtUp # Subscribe with caught-up notifications with client.subscribe_to_stream( stream_name="order-123", include_caught_up=True, ) as subscription: for item in subscription: if type(item) is CaughtUp: print("Subscription has caught up to live events") break # <-- so we can continue with the examples else: # Regular event processing print(f"Processing event: {item.type}") ``` @tab async ```python:no-line-numbers from kurrentdbclient import CaughtUp # Subscribe with caught-up notifications async with await client.subscribe_to_stream( stream_name="order-123", include_caught_up=True, ) as subscription: async for item in subscription: if type(item) is CaughtUp: print("Subscription has caught up to live events") break # <-- so we can continue with the examples else: # Regular event processing print(f"Processing event: {item.type}") ``` ::: ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: ::: tip The caught-up notification is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this notification will be sent right away. ::: --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/appending-events.md' --- # Appending events When you start working with EventStoreDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to EventStoreDB is to create an `EventData` object and call `append_to_stream` method. ```rs{7-11} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().expected_revision(ExpectedRevision::NoStream); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` `append_to_stream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to EventStoreDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, EventStoreDB will only append one of the events to the stream. For example, the following code will only append a single event: ```rs{12-15} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default(); let _ = client .append_to_stream("order-123", &options, event.clone()) .await?; // attempt to append the same event again let _ = client .append_to_stream("order-123", &options, event) .await?; ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of EventStoreDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. EventStoreDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `esdb.ContentTypeJson` and `esdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform EventStoreDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```rs let data = OrderCreated { order_id: "1" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().expected_revision(ExpectedRevision::NoStream); let _ = client .append_to_stream("order-456", &options, event) .await?; let data = OrderCreated { order_id: "2" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-456", &options, event) .await?; ``` There are several available expected revision options: * `Any` - No concurrency check * `NoStream` - Stream should not exist * `StreamExists` - Stream should exist * `Exact` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from EventStoreDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```rs{8-13,22,36-38} struct OrderUpdated { pub order_id: String, pub status: String, } let options = ReadStreamOptions::default().position(StreamPosition::End); let last_event = client .read_stream("order-789", &options) .await? .next() .await? .expect("the stream to at least exist."); let data = OrderUpdated { order_id: "1".to_string(), status: "processing".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().expected_revision(ExpectedRevision::StreamRevision( last_event.get_original_event().revision, )); let _ = client .append_to_stream("order-789", &options, event) .await?; let data = OrderUpdated { order_id: "2".to_string(), status: "shipped".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-789", &options, event) .await?; ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```rs let options = AppendToStreamOptions::default().authenticated(Credentials::new("admin", "changeit")); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs let settings = "esdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}".parse()?; let client = Client::new(settings)?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/delete-stream.md' --- # Deleting Events In EventStoreDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, EventStoreDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in EventStoreDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```rs let options = DeleteStreamOptions::default(); client .delete_stream(stream_name, &options) .await?; ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in EventStoreDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```rs options := esdb.TombstoneStreamOptions{ let options = TombstoneStreamOptions::default(); client .tombstone_stream(stream_name, &options) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/getting-started.md' --- # Getting started This guide will help you get started with EventStoreDB in your Java application. It covers the basic steps to connect to EventStoreDB, create events, append them to streams, and read them back. ## Required packages Add the following crates to your project by including them in the dependencies list located in your project's `Cargo.toml` file: ```toml [dependencies] serde = "1.0.188" futures = "0.3.28" tokio = {version = "1.32.0", features = ["full"]} [dependencies.eventstore] version = "4.0.0" ``` ## Connecting to EventStoreDB To connect your application to EventStoreDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to EventStoreDB is using secure mode (which is the default). However, if your EventStoreDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: EventStoreDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`esdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`esdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `esdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `esdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, esdb supports gossip on single-node deployments, so `esdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` esdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` esdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` esdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `esdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```rs let settings = "esdb://localhost:2113?tls=false&tlsVerifyCert=false".parse()?; let client = Client::new(settings)?; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to EventStoreDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```rs let event = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event_data = EventData::json("OrderCreated", &event)?.id(Uuid::new_v4()); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```rs client .append_to_stream("order-123", &Default::default(), event_data) .await?; ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the stream. ```rs let options = ReadStreamOptions::default().max_count(10); let mut stream = client.read_stream("order-123", &options).await?; while let Some(event) = stream.next().await? { // Handle the event } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```rs client .create_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` ### Subscribing to `$all` ```rs let options = PersistentSubscriptionToAllOptions::default() .filter(SubscriptionFilter::on_stream_name().add_prefix("test")); client .create_persistent_subscription_to_all("subscription-group", &options) .await?; ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```rs let mut sub = client .subscribe_to_persistent_subscription_to_all("subscription-group", &Default::default()) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```rs let options = PersistentSubscriptionOptions::default() .resolve_link_tos(true) .checkpoint_lower_bound(20); client .update_persistent_subscription("order-123", "subscription-group", &options) .await?; ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```rs client .delete_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/reading-events.md' --- # Reading Events EventStoreDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in EventStoreDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in EventStoreDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `StreamPosition::Start` to begin from the very beginning of the stream * Use `StreamPosition::End` to begin from the current end of the stream * Use `StreamPosition::Position` with a specific revision number (64-bit signed integer) ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .forwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can also start reading from a specific revision in the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .authenticated(Credentials::new("admin", "changeit")); let stream = client.read_stream("some-stream", &options).await; ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::End) .backwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `next()` method will return a `crate::Error::ResourceNotFound` error. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```rs{13-15} let options = ReadStreamOptions::default().position(StreamPosition::Position(10)); let mut stream = client.read_stream("order-0", &options).await?; match stream.next().await { Ok(Some(event)) => { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } Ok(None) => { println!("End of stream reached"); } Err(crate::Error::ResourceNotFound) => { println!("Stream does not exist"); } Err(e) => { return Err(e); } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```rs let options = ReadAllOptions::default() .position(StreamPosition::Start) .forwards(); ``` You can also start reading from a specific position in the transaction log: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(Position { commit: 1_110, prepare: 1_110, })); ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { println!("Event> {:?}", event.get_original_event()); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell EventStoreDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadAllOptions::default() .authenticated(Credentials::new("admin", "changeit")) ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `esdb.Backwards`: ```rs let options = ReadAllOptions::default().position(StreamPosition::End); let mut stream = client.read_all(&options).await?; ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events EventStoreDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `event_type` property. ```rs let mut stream = client.read_all(&Default::default()).await?; while let Some(event) = stream.next().await? { if event.get_original_event().event_type.starts_with("$") { continue; } println!("Event> {:?}", event.get_original_event()); } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/legacy/v4.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```rs let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` **`$all` subscription:** ```rs let mut sub = client.subscribe_to_all(&Default::default()).await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolve_link_tos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`StreamPosition::Start`, `StreamPosition::End` or a 64-bit signed integer representing the revision number): ```rs let options = SubscribeToStreamOptions::default().start_from(StreamPosition::Position(20)); client.subscribe_to_stream("order-123", &options).await; ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```rs let options = SubscribeToAllOptions::default().position(StreamPosition::Position(Position { commit: 1_056, prepare: 1_056, })); client.subscribe_to_all(&options).await; ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```rs // Stream let options = SubscribeToStreamOptions::default().start_from(StreamPosition::End); // $all let options = SubscribeToAllOptions::default().position(StreamPosition::End); ``` ## Resolving link-to events Link-to events point to events in other streams in EventStoreDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolve_link_tos` parameter to `true`: ```rs let options = SubscribeToStreamOptions::default() .start_from(StreamPosition::Start) .resolve_link_tos(); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. The retry logic is built into the SDK to handle such cases and will automatically reconnect the subscription when the application is back online. ```rs let retry = RetryOptions::default().retry_forever(); let options = SubscribeToStreamOptions::default().retry_options(retry); let mut stream = client.subscribe_to_stream("some-stream", &options).await; loop { let event = stream.next().await?; } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```rs{15-17} let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { // Handle the transition to live mode } _ => {} } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```rs let options = SubscribeToAllOptions::default().authenticated(Credentials::new("admin", "changeit")); ``` ## Server-side Filtering EventStoreDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```rs let filter = SubscriptionFilter::on_stream_name() .add_prefix("test-") .add_prefix("other-"); let options = SubscribeToAllOptions::default().filter(filter); client.subscribe_to_all(&options).await; ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```rs let filter = SubscriptionFilter::on_event_type().exclude_system_events(); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by event type **By prefix:** ```rs let filter = SubscriptionFilter::on_event_type().add_prefix("customer-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("^user|^company"); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by stream name **By prefix:** ```rs let filter = SubscriptionFilter::on_stream_name().add_prefix("user-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```rs loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; println!("Received event {}@{}", revision, stream_id); } SubscriptionEvent::Checkpoint(position) => { // Save commit position to a persistent store as a checkpoint println!("checkpoint taken at {}", position.commit); } _ => {} } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); let mut sub = client.subscribe_to_all(&options).await; ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `append_to_stream` method. ```rs{7-11} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` `append_to_stream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```rs{12-15} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default(); let _ = client .append_to_stream("order-123", &options, event.clone()) .await?; // attempt to append the same event again let _ = client .append_to_stream("order-123", &options, event) .await?; ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `kurrentdb.ContentTypeJson` and `kurrentdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```rs let data = OrderCreated { order_id: "1" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-456", &options, event) .await?; let data = OrderCreated { order_id: "2" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-456", &options, event) .await?; ``` There are several available expected revision options: * `Any` - No concurrency check * `NoStream` - Stream should not exist * `StreamExists` - Stream should exist * `Exact` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```rs{8-13,22,36-38} struct OrderUpdated { pub order_id: String, pub status: String, } let options = ReadStreamOptions::default().position(StreamPosition::End); let last_event = client .read_stream("order-789", &options) .await? .next() .await? .expect("the stream to at least exist."); let data = OrderUpdated { order_id: "1".to_string(), status: "processing".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::StreamRevision( last_event.get_original_event().revision, )); let _ = client .append_to_stream("order-789", &options, event) .await?; let data = OrderUpdated { order_id: "2".to_string(), status: "shipped".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-789", &options, event) .await?; ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```rs let options = AppendToStreamOptions::default().authenticated(Credentials::new("admin", "changeit")); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs let settings = "kurrentdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}".parse()?; let client = Client::new(settings)?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```rs let options = DeleteStreamOptions::default(); client .delete_stream(stream_name, &options) .await?; ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```rs options := kurrentdb.TombstoneStreamOptions{ let options = TombstoneStreamOptions::default(); client .tombstone_stream(stream_name, &options) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Rust application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following crates to your project by including them in the dependencies list located in your project's `Cargo.toml` file: ```toml [dependencies] serde = "1.0.188" futures = "0.3.28" tokio = {version = "1.32.0", features = ["full"]} [dependencies.eventstore] version = "4.0.0" ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```rs let settings = "kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false".parse()?; let client = Client::new(settings)?; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```rs let event = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event_data = EventData::json("OrderCreated", &event)?.id(Uuid::new_v4()); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```rs client .append_to_stream("order-123", &Default::default(), event_data) .await?; ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the stream. ```rs let options = ReadStreamOptions::default().max_count(10); let mut stream = client.read_stream("order-123", &options).await?; while let Some(event) = stream.next().await? { // Handle the event } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```rs client .create_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` ### Subscribing to `$all` ```rs let options = PersistentSubscriptionToAllOptions::default() .filter(SubscriptionFilter::on_stream_name().add_prefix("test")); client .create_persistent_subscription_to_all("subscription-group", &options) .await?; ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```rs let mut sub = client .subscribe_to_persistent_subscription_to_all("subscription-group", &Default::default()) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```rs let options = PersistentSubscriptionOptions::default() .resolve_link_tos(true) .checkpoint_lower_bound(20); client .update_persistent_subscription("order-123", "subscription-group", &options) .await?; ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```rs client .delete_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `StreamPosition::Start` to begin from the very beginning of the stream * Use `StreamPosition::End` to begin from the current end of the stream * Use `StreamPosition::Position` with a specific revision number (64-bit signed integer) ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .forwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can also start reading from a specific revision in the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .authenticated(Credentials::new("admin", "changeit")); let stream = client.read_stream("some-stream", &options).await; ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::End) .backwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `next()` method will return a `crate::Error::ResourceNotFound` error. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```rs{13-15} let options = ReadStreamOptions::default().position(StreamPosition::Position(10)); let mut stream = client.read_stream("order-0", &options).await?; match stream.next().await { Ok(Some(event)) => { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } Ok(None) => { println!("End of stream reached"); } Err(crate::Error::ResourceNotFound) => { println!("Stream does not exist"); } Err(e) => { return Err(e); } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```rs let options = ReadAllOptions::default() .position(StreamPosition::Start) .forwards(); ``` You can also start reading from a specific position in the transaction log: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(Position { commit: 1_110, prepare: 1_110, })); ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { println!("Event> {:?}", event.get_original_event()); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadAllOptions::default() .authenticated(Credentials::new("admin", "changeit")) ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `kurrentdb.Backwards`: ```rs let options = ReadAllOptions::default().position(StreamPosition::End); let mut stream = client.read_all(&options).await?; ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `event_type` property. ```rs let mut stream = client.read_all(&Default::default()).await?; while let Some(event) = stream.next().await? { if event.get_original_event().event_type.starts_with("$") { continue; } println!("Event> {:?}", event.get_original_event()); } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.0/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```rs let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` **`$all` subscription:** ```rs let mut sub = client.subscribe_to_all(&Default::default()).await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolve_link_tos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`StreamPosition::Start`, `StreamPosition::End` or a 64-bit signed integer representing the revision number): ```rs let options = SubscribeToStreamOptions::default().start_from(StreamPosition::Position(20)); client.subscribe_to_stream("order-123", &options).await; ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```rs let options = SubscribeToAllOptions::default().position(StreamPosition::Position(Position { commit: 1_056, prepare: 1_056, })); client.subscribe_to_all(&options).await; ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```rs // Stream let options = SubscribeToStreamOptions::default().start_from(StreamPosition::End); // $all let options = SubscribeToAllOptions::default().position(StreamPosition::End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolve_link_tos` parameter to `true`: ```rs let options = SubscribeToStreamOptions::default() .start_from(StreamPosition::Start) .resolve_link_tos(); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. The retry logic is built into the SDK to handle such cases and will automatically reconnect the subscription when the application is back online. ```rs let retry = RetryOptions::default().retry_forever(); let options = SubscribeToStreamOptions::default().retry_options(retry); let mut stream = client.subscribe_to_stream("some-stream", &options).await; loop { let event = stream.next().await?; } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```rs{15-17} let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { // Handle the transition to live mode } _ => {} } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```rs let options = SubscribeToAllOptions::default().authenticated(Credentials::new("admin", "changeit")); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```rs let filter = SubscriptionFilter::on_stream_name() .add_prefix("test-") .add_prefix("other-"); let options = SubscribeToAllOptions::default().filter(filter); client.subscribe_to_all(&options).await; ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```rs let filter = SubscriptionFilter::on_event_type().exclude_system_events(); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by event type **By prefix:** ```rs let filter = SubscriptionFilter::on_event_type().add_prefix("customer-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("^user|^company"); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by stream name **By prefix:** ```rs let filter = SubscriptionFilter::on_stream_name().add_prefix("user-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```rs loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; println!("Received event {}@{}", revision, stream_id); } SubscriptionEvent::Checkpoint(position) => { // Save commit position to a persistent store as a checkpoint println!("checkpoint taken at {}", position.commit); } _ => {} } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); let mut sub = client.subscribe_to_all(&options).await; ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `append_to_stream` method. ```rs{7-11} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` `append_to_stream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```rs{12-15} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default(); let _ = client .append_to_stream("order-123", &options, event.clone()) .await?; // attempt to append the same event again let _ = client .append_to_stream("order-123", &options, event) .await?; ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `kurrentdb.ContentTypeJson` and `kurrentdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```rs let data = OrderCreated { order_id: "1" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-456", &options, event) .await?; let data = OrderCreated { order_id: "2" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-456", &options, event) .await?; ``` There are several available expected revision options: * `Any` - No concurrency check * `NoStream` - Stream should not exist * `StreamExists` - Stream should exist * `Exact` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```rs{8-13,22,36-38} struct OrderUpdated { pub order_id: String, pub status: String, } let options = ReadStreamOptions::default().position(StreamPosition::End); let last_event = client .read_stream("order-789", &options) .await? .next() .await? .expect("the stream to at least exist."); let data = OrderUpdated { order_id: "1".to_string(), status: "processing".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::StreamRevision( last_event.get_original_event().revision, )); let _ = client .append_to_stream("order-789", &options, event) .await?; let data = OrderUpdated { order_id: "2".to_string(), status: "shipped".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-789", &options, event) .await?; ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```rs let options = AppendToStreamOptions::default().authenticated(Credentials::new("admin", "changeit")); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs let settings = "kurrentdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}".parse()?; let client = Client::new(settings)?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```rs let options = DeleteStreamOptions::default(); client .delete_stream(stream_name, &options) .await?; ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```rs options := kurrentdb.TombstoneStreamOptions{ let options = TombstoneStreamOptions::default(); client .tombstone_stream(stream_name, &options) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Rust application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following crates to your project by including them in the dependencies list located in your project's `Cargo.toml` file: ```toml [dependencies] serde = "1.0.188" futures = "0.3.28" tokio = {version = "1.32.0", features = ["full"]} [dependencies.eventstore] version = "4.0.0" ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```rs let settings = "kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false".parse()?; let client = Client::new(settings)?; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```rs let event = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event_data = EventData::json("OrderCreated", &event)?.id(Uuid::new_v4()); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```rs client .append_to_stream("order-123", &Default::default(), event_data) .await?; ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the stream. ```rs let options = ReadStreamOptions::default().max_count(10); let mut stream = client.read_stream("order-123", &options).await?; while let Some(event) = stream.next().await? { // Handle the event } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```rs client .create_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` ### Subscribing to `$all` ```rs let options = PersistentSubscriptionToAllOptions::default() .filter(SubscriptionFilter::on_stream_name().add_prefix("test")); client .create_persistent_subscription_to_all("subscription-group", &options) .await?; ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```rs let mut sub = client .subscribe_to_persistent_subscription_to_all("subscription-group", &Default::default()) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```rs let options = PersistentSubscriptionOptions::default() .resolve_link_tos(true) .checkpoint_lower_bound(20); client .update_persistent_subscription("order-123", "subscription-group", &options) .await?; ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```rs client .delete_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `StreamPosition::Start` to begin from the very beginning of the stream * Use `StreamPosition::End` to begin from the current end of the stream * Use `StreamPosition::Position` with a specific revision number (64-bit signed integer) ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .forwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can also start reading from a specific revision in the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .authenticated(Credentials::new("admin", "changeit")); let stream = client.read_stream("some-stream", &options).await; ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::End) .backwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `next()` method will return a `crate::Error::ResourceNotFound` error. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```rs{13-15} let options = ReadStreamOptions::default().position(StreamPosition::Position(10)); let mut stream = client.read_stream("order-0", &options).await?; match stream.next().await { Ok(Some(event)) => { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } Ok(None) => { println!("End of stream reached"); } Err(crate::Error::ResourceNotFound) => { println!("Stream does not exist"); } Err(e) => { return Err(e); } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```rs let options = ReadAllOptions::default() .position(StreamPosition::Start) .forwards(); ``` You can also start reading from a specific position in the transaction log: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(Position { commit: 1_110, prepare: 1_110, })); ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { println!("Event> {:?}", event.get_original_event()); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadAllOptions::default() .authenticated(Credentials::new("admin", "changeit")) ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `kurrentdb.Backwards`: ```rs let options = ReadAllOptions::default().position(StreamPosition::End); let mut stream = client.read_all(&options).await?; ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `event_type` property. ```rs let mut stream = client.read_all(&Default::default()).await?; while let Some(event) = stream.next().await? { if event.get_original_event().event_type.starts_with("$") { continue; } println!("Event> {:?}", event.get_original_event()); } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.1/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```rs let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` **`$all` subscription:** ```rs let mut sub = client.subscribe_to_all(&Default::default()).await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolve_link_tos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`StreamPosition::Start`, `StreamPosition::End` or a 64-bit signed integer representing the revision number): ```rs let options = SubscribeToStreamOptions::default().start_from(StreamPosition::Position(20)); client.subscribe_to_stream("order-123", &options).await; ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```rs let options = SubscribeToAllOptions::default().position(StreamPosition::Position(Position { commit: 1_056, prepare: 1_056, })); client.subscribe_to_all(&options).await; ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```rs // Stream let options = SubscribeToStreamOptions::default().start_from(StreamPosition::End); // $all let options = SubscribeToAllOptions::default().position(StreamPosition::End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolve_link_tos` parameter to `true`: ```rs let options = SubscribeToStreamOptions::default() .start_from(StreamPosition::Start) .resolve_link_tos(); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. The retry logic is built into the SDK to handle such cases and will automatically reconnect the subscription when the application is back online. ```rs let retry = RetryOptions::default().retry_forever(); let options = SubscribeToStreamOptions::default().retry_options(retry); let mut stream = client.subscribe_to_stream("some-stream", &options).await; loop { let event = stream.next().await?; } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```rs{15-17} let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { // Handle the transition to live mode } _ => {} } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```rs let options = SubscribeToAllOptions::default().authenticated(Credentials::new("admin", "changeit")); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```rs let filter = SubscriptionFilter::on_stream_name() .add_prefix("test-") .add_prefix("other-"); let options = SubscribeToAllOptions::default().filter(filter); client.subscribe_to_all(&options).await; ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```rs let filter = SubscriptionFilter::on_event_type().exclude_system_events(); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by event type **By prefix:** ```rs let filter = SubscriptionFilter::on_event_type().add_prefix("customer-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("^user|^company"); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by stream name **By prefix:** ```rs let filter = SubscriptionFilter::on_stream_name().add_prefix("user-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```rs loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; println!("Received event {}@{}", revision, stream_id); } SubscriptionEvent::Checkpoint(position) => { // Save commit position to a persistent store as a checkpoint println!("checkpoint taken at {}", position.commit); } _ => {} } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); let mut sub = client.subscribe_to_all(&options).await; ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/appending-events.md' --- # Appending events When you start working with KurrentDB, your application streams are empty. The first meaningful operation is to add one or more events to the database using this API. ::: tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Append your first event The simplest way to append an event to KurrentDB is to create an `EventData` object and call `append_to_stream` method. ```rs{7-11} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` `append_to_stream` takes a collection or a single object that can be serialized in JSON or binary format, which allows you to save more than one event in a single batch. Outside the example above, other options exist for dealing with different scenarios. ::: tip If you are new to Event Sourcing, please study the [Handling concurrency](#handling-concurrency) section below. ::: ## Working with EventData Events appended to KurrentDB must be wrapped in an `EventData` object. This allows you to specify the event's content, the type of event, and whether it's in JSON format. In its simplest form, you need three arguments: **eventId**, **eventType**, and **eventData**. ### EventID This takes the format of a `UUID` and is used to uniquely identify the event you are trying to append. If two events with the same `UUID` are appended to the same stream in quick succession, KurrentDB will only append one of the events to the stream. For example, the following code will only append a single event: ```rs{12-15} let data = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default(); let _ = client .append_to_stream("order-123", &options, event.clone()) .await?; // attempt to append the same event again let _ = client .append_to_stream("order-123", &options, event) .await?; ``` ### EventType Each event should be supplied with an event type. This unique string is used to identify the type of event you are saving. It is common to see the explicit event code type name used as the type as it makes serialising and de-serialising of the event easy. However, we recommend against this as it couples the storage to the type and will make it more difficult if you need to version the event at a later date. ### Data Representation of your event data. It is recommended that you store your events as JSON objects. This allows you to take advantage of all of KurrentDB's functionality, such as projections. That said, you can save events using whatever format suits your workflow. Eventually, the data will be stored as encoded bytes. ### Metadata Storing additional information alongside your event that is part of the event itself is standard practice. This can be correlation IDs, timestamps, access information, etc. KurrentDB allows you to store a separate byte array containing this information to keep it separate. ### ContentType The content type indicates whether the event is stored as JSON or binary format. You can choose between `kurrentdb.ContentTypeJson` and `kurrentdb.ContentTypeBinary` when creating your `EventData` object. ## Handling concurrency When appending events to a stream, you can supply a *stream state*. Your client uses this to inform KurrentDB of the state or version you expect the stream to be in when appending an event. If the stream isn't in that state, an exception will be thrown. For example, if you try to append the same record twice, expecting both times that the stream doesn't exist, you will get an exception on the second: ```rs let data = OrderCreated { order_id: "1" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::NoStream); let _ = client .append_to_stream("order-456", &options, event) .await?; let data = OrderCreated { order_id: "2" }; let event = EventData::json("OrderCreated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-456", &options, event) .await?; ``` There are several available expected revision options: * `Any` - No concurrency check * `NoStream` - Stream should not exist * `StreamExists` - Stream should exist * `Exact` - Stream should be at specific revision This check can be used to implement optimistic concurrency. When retrieving a stream from KurrentDB, note the current version number. When you save it back, you can determine if somebody else has modified the record in the meantime. ```rs{8-13,22,36-38} struct OrderUpdated { pub order_id: String, pub status: String, } let options = ReadStreamOptions::default().position(StreamPosition::End); let last_event = client .read_stream("order-789", &options) .await? .next() .await? .expect("the stream to at least exist."); let data = OrderUpdated { order_id: "1".to_string(), status: "processing".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let options = AppendToStreamOptions::default().stream_state(StreamState::StreamRevision( last_event.get_original_event().revision, )); let _ = client .append_to_stream("order-789", &options, event) .await?; let data = OrderUpdated { order_id: "2".to_string(), status: "shipped".to_string(), }; let event = EventData::json("OrderUpdated", &data)?.id(Uuid::new_v4()); let _ = client .append_to_stream("order-789", &options, event) .await?; ``` ## User credentials You can provide user credentials to append the data as follows. This will override the default credentials set on the connection. ```rs let options = AppendToStreamOptions::default().authenticated(Credentials::new("admin", "changeit")); let _ = client .append_to_stream("order-123", &options, event) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/authentication.md' --- # Client x.509 certificate X.509 certificates are digital certificates that use the X.509 public key infrastructure (PKI) standard to verify the identity of clients and servers. They play a crucial role in establishing a secure connection by providing a way to authenticate identities and establish trust. ## Prerequisites 1. KurrentDB 25.0 or greater, or EventStoreDB 24.10 or later. 2. A valid X.509 certificate configured on the Database. See [configuration steps](@server/security/user-authentication.html#user-x-509-certificates) for more details. ## Connect using an x.509 certificate To connect using an x.509 certificate, you need to provide the certificate and the private key to the client. If both username or password and certificate authentication data are supplied, the client prioritizes user credentials for authentication. The client will throw an error if the certificate and the key are not both provided. The client supports the following parameters: | Parameter | Description | |----------------|--------------------------------------------------------------------------------| | `userCertFile` | The file containing the X.509 user certificate in PEM format. | | `userKeyFile` | The file containing the user certificate’s matching private key in PEM format. | To authenticate, include these two parameters in your connection string or constructor when initializing the client: ```rs let settings = "kurrentdb://localhost:2113?tls=true&userCertFile={pathToCaFile}&userKeyFile={pathToKeyFile}".parse()?; let client = Client::new(settings)?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/delete-stream.md' --- # Deleting Events In KurrentDB, you can delete events and streams either partially or completely. Settings like $maxAge and $maxCount help control how long events are kept or how many events are stored in a stream, but they won't delete the entire stream. When you need to fully remove a stream, KurrentDB offers two options: Soft Delete and Hard Delete. ## Soft delete Soft delete in KurrentDB allows you to mark a stream for deletion without completely removing it, so you can still add new events later. While you can do this through the UI, using code is often better for automating the process, handling many streams at once, or including custom rules. Code is especially helpful for large-scale deletions or when you need to integrate soft deletes into other workflows. ```rs let options = DeleteStreamOptions::default(); client .delete_stream(stream_name, &options) .await?; ``` ::: note Clicking the delete button in the UI performs a soft delete, setting the TruncateBefore value to remove all events up to a certain point. While this marks the events for deletion, actual removal occurs during the next scavenging process. The stream can still be reopened by appending new events. ::: ## Hard delete Hard delete in KurrentDB permanently removes a stream and its events. While you can use the HTTP API, code is often better for automating the process, managing multiple streams, and ensuring precise control. Code is especially useful when you need to integrate hard delete into larger workflows or apply specific conditions. Note that when a stream is hard deleted, you cannot reuse the stream name, it will raise an exception if you try to append to it again. ```rs options := kurrentdb.TombstoneStreamOptions{ let options = TombstoneStreamOptions::default(); client .tombstone_stream(stream_name, &options) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/getting-started.md' --- # Getting started This guide will help you get started with KurrentDB in your Rust application. It covers the basic steps to connect to KurrentDB, create events, append them to streams, and read them back. ## Required packages Add the following crates to your project by including them in the dependencies list located in your project's `Cargo.toml` file: ```toml [dependencies] serde = "1.0.188" futures = "0.3.28" tokio = {version = "1.32.0", features = ["full"]} [dependencies.eventstore] version = "4.0.0" ``` ## Connecting to KurrentDB To connect your application to KurrentDB, you need to configure and create a client instance. ::: tip Insecure clusters The recommended way to connect to KurrentDB is using secure mode (which is the default). However, if your KurrentDB instance is running in insecure mode, you must explicitly set `tls=false` in your connection string or client configuration. ::: KurrentDB uses connection strings to configure the client connection. The connection string supports two protocols: * **`kurrentdb://`** - for connecting directly to specific node endpoints (single node or multi-node cluster with explicit endpoints) * **`kurrentdb+discover://`** - for connecting using cluster discovery via DNS or gossip endpoints When using `kurrentdb://`, you specify the exact endpoints to connect to. The client will connect directly to these endpoints. For multi-node clusters, you can specify multiple endpoints separated by commas, and the client will query each node's Gossip API to get cluster information, then picks a node based on the URI's node preference. With `kurrentdb+discover://`, the client uses cluster discovery to find available nodes. This is particularly useful when you have a DNS A record pointing to cluster nodes or when you want the client to automatically discover the cluster topology. ::: info Gossip support Since version 22.10, kurrentdb supports gossip on single-node deployments, so `kurrentdb+discover://` can be used for any topology, including single-node setups. ::: For cluster connections using discovery, use the following format: ``` kurrentdb+discover://admin:changeit@cluster.dns.name:2113 ``` Where `cluster.dns.name` is a DNS `A` record that points to all cluster nodes. For direct connections to specific endpoints, you can specify individual nodes: ``` kurrentdb://admin:changeit@node1.dns.name:2113,node2.dns.name:2113,node3.dns.name:2113 ``` Or for a single node: ``` kurrentdb://admin:changeit@localhost:2113 ``` There are a number of query parameters that can be used in the connection string to instruct the cluster how and where the connection should be established. All query parameters are optional. | Parameter | Accepted values | Default | Description | |-----------------------|---------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| | `tls` | `true`, `false` | `true` | Use secure connection, set to `false` when connecting to a non-secure server or cluster. | | `connectionName` | Any string | None | Connection name | | `maxDiscoverAttempts` | Number | `10` | Number of attempts to discover the cluster. | | `discoveryInterval` | Number | `100` | Cluster discovery polling interval in milliseconds. | | `gossipTimeout` | Number | `5` | Gossip timeout in seconds, when the gossip call times out, it will be retried. | | `nodePreference` | `leader`, `follower`, `random`, `readOnlyReplica` | `leader` | Preferred node role. When creating a client for write operations, always use `leader`. | | `tlsVerifyCert` | `true`, `false` | `true` | In secure mode, set to `true` when using an untrusted connection to the node if you don't have the CA file available. Don't use in production. | | `tlsCaFile` | String, file path | None | Path to the CA file when connecting to a secure cluster with a certificate that's not signed by a trusted CA. | | `defaultDeadline` | Number | None | Default timeout for client operations, in milliseconds. Most clients allow overriding the deadline per operation. | | `keepAliveInterval` | Number | `10` | Interval between keep-alive ping calls, in seconds. | | `keepAliveTimeout` | Number | `10` | Keep-alive ping call timeout, in seconds. | | `userCertFile` | String, file path | None | User certificate file for X.509 authentication. | | `userKeyFile` | String, file path | None | Key file for the user certificate used for X.509 authentication. | When connecting to an insecure instance, specify `tls=false` parameter. For example, for a node running locally use `kurrentdb://localhost:2113?tls=false`. Note that usernames and passwords aren't provided there because insecure deployments don't support authentication and authorisation. ## Creating a client First, create a client and get it connected to the database. ```rs let settings = "kurrentdb://localhost:2113?tls=false&tlsVerifyCert=false".parse()?; let client = Client::new(settings)?; ``` The client instance can be used as a singleton across the whole application. It doesn't need to open or close the connection. ## Creating an event You can write anything to KurrentDB as events. The client needs a byte array as the event payload. Normally, you'd use a serialized object, and it's up to you to choose the serialization method. The code snippet below creates an event object instance, serializes it, and adds it as a payload to the `EventData` structure, which the client can then write to the database. ```rs let event = OrderCreated { order_id: Uuid::new_v4().to_string(), }; let event_data = EventData::json("OrderCreated", &event)?.id(Uuid::new_v4()); ``` ## Appending events Each event in the database has its own unique identifier (UUID). The database uses it to ensure idempotent writes, but it only works if you specify the stream revision when appending events to the stream. In the snippet below, we append the event to the stream `orders`. ```rs client .append_to_stream("order-123", &Default::default(), event_data) .await?; ``` Here we are appending events without checking if the stream exists or if the stream version matches the expected event version. See more advanced scenarios in [appending events documentation](./appending-events.md). ## Reading events Finally, we can read events back from the stream. ```rs let options = ReadStreamOptions::default().max_count(10); let mut stream = client.read_stream("order-123", &options).await?; while let Some(event) = stream.next().await? { // Handle the event } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/persistent-subscriptions.md' --- # Persistent Subscriptions Persistent subscriptions are similar to catch-up subscriptions, but there are two key differences: * The subscription checkpoint is maintained by the server. It means that when your client reconnects to the persistent subscription, it will automatically resume from the last known position. * It's possible to connect more than one event consumer to the same persistent subscription. In that case, the server will load-balance the consumers, depending on the defined strategy, and distribute the events to them. Because of those, persistent subscriptions are defined as subscription groups that are defined and maintained by the server. Consumer then connect to a particular subscription group, and the server starts sending event to the consumer. You can read more about persistent subscriptions in the [server documentation](@server/features/persistent-subscriptions.md). ## Creating a Subscription Group The first step in working with a persistent subscription is to create a subscription group. Note that attempting to create a subscription group multiple times will result in an error. Admin permissions are required to create a persistent subscription group. The following examples demonstrate how to create a subscription group for a persistent subscription. You can subscribe to a specific stream or to the `$all` stream, which includes all events. ### Subscribing to a Specific Stream ```rs client .create_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` ### Subscribing to `$all` ```rs let options = PersistentSubscriptionToAllOptions::default() .filter(SubscriptionFilter::on_stream_name().add_prefix("test")); client .create_persistent_subscription_to_all("subscription-group", &options) .await?; ``` ::: note As from EventStoreDB 21.10, the ability to subscribe to `$all` supports [server-side filtering](subscriptions.md#server-side-filtering). You can create a subscription group for `$all` similarly to how you would for a specific stream: ::: ## Connecting a consumer Once you have created a subscription group, clients can connect to it. A subscription in your application should only have the connection in your code, you should assume that the subscription already exists. The most important parameter to pass when connecting is the buffer size. This represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ### Connecting to one stream The code below shows how to connect to an existing subscription group for a specific stream: ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` ### Connecting to $all The code below shows how to connect to an existing subscription group for `$all`: ```rs let mut sub = client .subscribe_to_persistent_subscription_to_all("subscription-group", &Default::default()) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The `SubscribeToPersistentSubscriptionToAll` method is identical to the `SubscribeToPersistentSubscriptionToStream` method, except that you don't need to specify a stream name. ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If processing is successful, you must send an Ack (acknowledge) to the server to let it know that the message has been handled. If processing fails for some reason, then you can Nack (not acknowledge) the message and tell the server how to handle the failure. ```rs let mut sub = client .subscribe_to_persistent_subscription( "order-123", "subscription-group", &Default::default(), ) .await?; loop { let event = sub.next().await?; sub.ack(&event).await?; } ``` The *Nack event action* describes what the server should do with the message: | Action | Description | |:----------|:---------------------------------------------------------------------| | `Park` | Park the message and do not resend. Put it on poison queue. | | `Retry` | Explicitly retry the message. | | `Skip` | Skip this message do not resend and do not put in poison queue. | | `Stop` | Stop the subscription. | ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. ### RoundRobin (default) Distributes events to all clients evenly. If the buffer size is reached, the client won't receive more events until it acknowledges or not acknowledges events in its buffer. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the buffer size is reached. After that, the next client is selected in a round-robin style, and the process repeats. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the system `$by_category` projection. KurrentDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects, its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is **not a guarantee**, and you should handle the usual ordering and concurrency issues. ### PinnedByCorrelation The PinnedByCorrelation strategy is a consumer strategy available for persistent subscriptions It ensures that events with the same correlation id are consistently delivered to the same consumer within a subscription group. :::note This strategy requires database version 21.10.1 or later. You can only create a persistent subscription with this strategy. To change the strategy, you must delete the existing subscription and create a new one with the desired settings. ::: ## Updating a subscription group You can edit the settings of an existing subscription group while it is running, you don't need to delete and recreate it to change settings. When you update the subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```rs let options = PersistentSubscriptionOptions::default() .resolve_link_tos(true) .checkpoint_lower_bound(20); client .update_persistent_subscription("order-123", "subscription-group", &options) .await?; ``` ## Persistent subscription settings Both the create and update methods take some settings for configuring the persistent subscription. The following table shows the configuration options you can set on a persistent subscription. | Option | Description | Default | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | | `resolveLinkTos` | Whether the subscription should resolve link events to their linked events. | `false` | | `fromStart` | The exclusive position in the stream or transaction file the subscription should start from. | `null` (start from the end of the stream) | | `extraStatistics` | Whether to track latency statistics on this subscription. | `false` | | `messageTimeout` | The amount of time after which to consider a message as timed out and retried. | `30` (seconds) | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked. | `10` | | `liveBufferSize` | The size of the buffer (in-memory) listening to live messages as they happen before paging occurs. | `500` | | `readBatchSize` | The number of events read at a time when paging through history. | `20` | | `historyBufferSize` | The number of events to cache when paging through history. | `500` | | `checkpointLowerBound` | The minimum number of messages to process before a checkpoint may be written. | `10` seconds | | `checkpointUpperBound` | The maximum number of messages not checkpoint before forcing a checkpoint. | `1000` | | `maxSubscriberCount` | The maximum number of subscribers allowed. | `0` (unbounded) | | `namedConsumerStrategy` | The strategy to use for distributing events to client consumers. See the [consumer strategies](#consumer-strategies) in this doc. | `RoundRobin` | ## Deleting a subscription group Remove a subscription group with the delete operation. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```rs client .delete_persistent_subscription("order-123", "subscription-group", &Default::default()) .await?; ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/reading-events.md' --- # Reading Events KurrentDB provides two primary methods for reading events: reading from an individual stream to retrieve events from a specific named stream, or reading from the `$all` stream to access all events across the entire event store. Events in KurrentDB are organized within individual streams and use two distinct positioning systems to track their location. The **revision number** is a 64-bit signed integer (`long`) that represents the sequential position of an event within its specific stream. Events are numbered starting from 0, with each new event receiving the next sequential revision number (0, 1, 2, 3...). The **global position** represents the event's location in KurrentDB's global transaction log and consists of two coordinates: the `commit` position (where the transaction was committed in the log) and the `prepare` position (where the transaction was initially prepared). These positioning identifiers are essential for reading operations, as they allow you to specify exactly where to start reading from within a stream or across the entire event store. ## Reading from a stream You can read all the events or a sample of the events from individual streams, starting from any position in the stream, and can read either forward or backward. It is only possible to read events from a single stream at a time. You can read events from the global event log, which spans across streams. Learn more about this process in the [Read from `$all`](#reading-from-the-all-stream) section below. ### Reading forwards The simplest way to read a stream forwards is to supply a stream name, read direction, and revision from which to start. The revision can be specified in several ways: * Use `StreamPosition::Start` to begin from the very beginning of the stream * Use `StreamPosition::End` to begin from the current end of the stream * Use `StreamPosition::Position` with a specific revision number (64-bit signed integer) ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .forwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can also start reading from a specific revision in the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); let mut stream = client.read_stream("some-stream", &options).await?; ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } ``` There are a number of additional arguments you can provide when reading a stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events from the stream: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadStreamOptions::default() .position(StreamPosition::Start) .authenticated(Credentials::new("admin", "changeit")); let stream = client.read_stream("some-stream", &options).await; ``` ### Reading backwards In addition to reading a stream forwards, streams can be read backwards. To read all the events backwards, set the `fromRevision` to `END`: ```rs let options = ReadStreamOptions::default() .position(StreamPosition::End) .backwards(); let mut stream = client.read_stream("some-stream", &options).await?; ``` :::tip Read one event backwards to find the last position in the stream. ::: ### Checking if the stream exists Reading a stream returns a `ReadStream` that you can iterate over. When iterating over events from a non-existent stream, the `next()` method will return a `crate::Error::ResourceNotFound` error. It is important to handle this error when attempting to iterate a stream that may not exist. For example: ```rs{13-15} let options = ReadStreamOptions::default().position(StreamPosition::Position(10)); let mut stream = client.read_stream("order-0", &options).await?; match stream.next().await { Ok(Some(event)) => { let test_event = event.get_original_event().as_json::()?; println!("Event> {:?}", test_event); } Ok(None) => { println!("End of stream reached"); } Err(crate::Error::ResourceNotFound) => { println!("Stream does not exist"); } Err(e) => { return Err(e); } } ``` ## Reading from the $all stream Reading from the `$all` stream is similar to reading from an individual stream, but please note there are differences. One significant difference is the need to provide admin user account credentials to read from the `$all` stream. Additionally, you need to provide a transaction log position instead of a stream revision when reading from the `$all` stream. ### Reading forwards The simplest way to read the `$all` stream forwards is to supply a read direction and the transaction log position from which you want to start. The transaction log position can be specified in several ways: * Use `start` to begin from the very beginning of the transaction log * Use `end` to begin from the current end of the transaction log * Use `fromPosition` with a specific `Position` object containing commit and prepare coordinates ```rs let options = ReadAllOptions::default() .position(StreamPosition::Start) .forwards(); ``` You can also start reading from a specific position in the transaction log: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(Position { commit: 1_110, prepare: 1_110, })); ``` You can then iterate synchronously through the result: ```rs while let Some(event) = stream.next().await? { println!("Event> {:?}", event.get_original_event()); } ``` There are a number of additional arguments you can provide when reading the `$all` stream. #### maxCount Passing in the max count allows you to limit the number of events that returned. In the example below, we read a maximum of 10 events: ```rs let options = ReadAllOptions::default() .position(StreamPosition::Position(10)) .max_count(20); ``` #### resolveLinkTos When using projections to create new events you can set whether the generated events are pointers to existing events. Setting this value to true will tell KurrentDB to return the event as well as the event linking to it. ```rs let options = ReadAllOptions::default().resolve_link_tos(); ``` #### userCredentials The credentials used to read the data can be used by the subscription as follows. This will override the default credentials set on the connection. ```rs let options = ReadAllOptions::default() .authenticated(Credentials::new("admin", "changeit")) ``` ### Reading backwards In addition to reading the `$all` stream forwards, it can be read backwards. To read all the events backwards, set the *direction* to `kurrentdb.Backwards`: ```rs let options = ReadAllOptions::default().position(StreamPosition::End); let mut stream = client.read_all(&options).await?; ``` :::tip Read one event backwards to find the last position in the `$all` stream. ::: ### Handling system events KurrentDB will also return system events when reading from the `$all` stream. In most cases you can ignore these events. All system events begin with `$` or `$$` and can be easily ignored by checking the `event_type` property. ```rs let mut stream = client.read_all(&Default::default()).await?; while let Some(event) = stream.next().await? { if event.get_original_event().event_type.starts_with("$") { continue; } println!("Event> {:?}", event.get_original_event()); } ``` --- --- url: 'https://docs.kurrent.io/clients/rust/v1.2/subscriptions.md' --- # Catch-up Subscriptions Subscriptions allow you to subscribe to a stream and receive notifications about new events added to the stream. You provide an event handler and an optional starting point to the subscription. The handler is called for each event from the starting point onward. If events already exist, the handler will be called for each event one by one until it reaches the end of the stream. The server will then notify the handler whenever a new event appears. :::tip Check the [Getting Started](getting-started.md) guide to learn how to configure and use the client SDK. ::: ## Basic Subscriptions You can subscribe to a single stream or to `$all` to process all events in the database. **Stream subscription:** ```rs let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` **`$all` subscription:** ```rs let mut sub = client.subscribe_to_all(&Default::default()).await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { } _ => {} } } ``` When you subscribe to a stream with link events (e.g., `$ce` category stream), set `resolve_link_tos` to `true`. ## Subscribing from a Position Both stream and `$all` subscriptions accept a starting position if you want to read from a specific point onward. If events already exist after the position you subscribe to, they will be read on the server side and sent to the subscription. Once caught up, the server will push any new events received on the streams to the client. There is no difference between catching up and live on the client side. ::: warning The positions provided to the subscriptions are exclusive. You will only receive the next event after the subscribed position. ::: **Stream from specific position:** To subscribe to a stream from a specific position, provide a stream position (`StreamPosition::Start`, `StreamPosition::End` or a 64-bit signed integer representing the revision number): ```rs let options = SubscribeToStreamOptions::default().start_from(StreamPosition::Position(20)); client.subscribe_to_stream("order-123", &options).await; ``` **`$all` from specific position:** For the `$all` stream, provide a `Position` structure with prepare and commit positions: ```rs let options = SubscribeToAllOptions::default().position(StreamPosition::Position(Position { commit: 1_056, prepare: 1_056, })); client.subscribe_to_all(&options).await; ``` **Live updates only:** Subscribe to the end of a stream to get only new events: ```rs // Stream let options = SubscribeToStreamOptions::default().start_from(StreamPosition::End); // $all let options = SubscribeToAllOptions::default().position(StreamPosition::End); ``` ## Resolving link-to events Link-to events point to events in other streams in KurrentDB. These are generally created by projections such as the `$by_event_type` projection which links events of the same event type into the same stream. This makes it easier to look up all events of a specific type. ::: tip [Filtered subscriptions](subscriptions.md#server-side-filtering) make it easier and faster to subscribe to all events of a specific type or matching a prefix. ::: When reading a stream you can specify whether to resolve link-to's. By default, link-to events are not resolved. You can change this behaviour by setting the `resolve_link_tos` parameter to `true`: ```rs let options = SubscribeToStreamOptions::default() .start_from(StreamPosition::Start) .resolve_link_tos(); ``` ## Subscription Drops and Recovery When a subscription stops or experiences an error, it will be dropped. The subscription provides an `error` event in the `StreamSubscription` Node.js readable stream, which will get called when the subscription breaks. The `error` event allows you to inspect the reason why the subscription dropped, as well as any exceptions that occurred. Bear in mind that a subscription can also drop because it is slow. The server tried to push all the live events to the subscription when it is in the live processing mode. If the subscription gets the reading buffer overflow and won't be able to acknowledge the buffer, it will break. ### Handling Dropped Subscriptions An application, which hosts the subscription, can go offline for some time for different reasons. It could be a crash, infrastructure failure, or a new version deployment. The retry logic is built into the SDK to handle such cases and will automatically reconnect the subscription when the application is back online. ```rs let retry = RetryOptions::default().retry_forever(); let options = SubscribeToStreamOptions::default().retry_options(retry); let mut stream = client.subscribe_to_stream("some-stream", &options).await; loop { let event = stream.next().await?; } ``` ## Handling Subscription State Changes ::: info EventStoreDB 23.10.0+ This feature requires EventStoreDB version 23.10.0 or later. ::: When a subscription processes historical events and reaches the end of the stream, it transitions from "catching up" to "live" mode. You can detect this transition using the `caughtUp` event on the subscription. ```rs{15-17} let mut sub = client .subscribe_to_stream("order-123", &Default::default()) .await; loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; // handle the event } SubscriptionEvent::CaughtUp(position) => { // Handle the transition to live mode } _ => {} } } ``` ::: tip The `CaughtUp` event is only emitted when transitioning from catching up to live mode. If you subscribe from the end of a stream, you'll immediately be in live mode and this callback will be called right away. ::: ## User credentials The user creating a subscription must have read access to the stream it's subscribing to, and only admin users may subscribe to `$all` or create filtered subscriptions. The code below shows how you can provide user credentials for a subscription. When you specify subscription credentials explicitly, it will override the default credentials set for the client. If you don't specify any credentials, the client will use the credentials specified for the client, if you specified those. ```rs let options = SubscribeToAllOptions::default().authenticated(Credentials::new("admin", "changeit")); ``` ## Server-side Filtering KurrentDB allows you to filter events while subscribing to the `$all` stream to only receive the events you care about. You can filter by event type or stream name using a regular expression or a prefix. Server-side filtering is currently only available on the `$all` stream. ::: tip Server-side filtering was introduced as a simpler alternative to projections. You should consider filtering before creating a projection to include the events you care about. ::: **Basic filtering:** ```rs let filter = SubscriptionFilter::on_stream_name() .add_prefix("test-") .add_prefix("other-"); let options = SubscribeToAllOptions::default().filter(filter); client.subscribe_to_all(&options).await; ``` ### Filtering out system events System events are prefixed with `$` and can be filtered out when subscribing to `$all`: ```rs let filter = SubscriptionFilter::on_event_type().exclude_system_events(); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by event type **By prefix:** ```rs let filter = SubscriptionFilter::on_event_type().add_prefix("customer-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("^user|^company"); let options = SubscribeToAllOptions::default().filter(filter); ``` ### Filtering by stream name **By prefix:** ```rs let filter = SubscriptionFilter::on_stream_name().add_prefix("user-"); let options = SubscribeToAllOptions::default().filter(filter); ``` **By regular expression:** ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); ``` ## Checkpointing When a catch-up subscription is used to process an `$all` stream containing many events, the last thing you want is for your application to crash midway, forcing you to restart from the beginning. ### What is a checkpoint? A checkpoint is the position of an event in the `$all` stream to which your application has processed. By saving this position to a persistent store (e.g., a database), it allows your catch-up subscription to: * Recover from crashes by reading the checkpoint and resuming from that position * Avoid reprocessing all events from the start To create a checkpoint, store the event's commit or prepare position. ::: warning If your database contains events created by the legacy TCP client using the [transaction feature](https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.html#transactions), you should store both the commit and prepare positions together as your checkpoint. ::: ### Updating checkpoints at regular intervals The SDK provides a way to notify your application after processing a configurable number of events. This allows you to periodically save a checkpoint at regular intervals. ```rs loop { let event = sub.next_subscription_event().await?; match event { SubscriptionEvent::EventAppeared(event) => { let stream_id = event.get_original_stream_id(); let revision = event.get_original_event().revision; println!("Received event {}@{}", revision, stream_id); } SubscriptionEvent::Checkpoint(position) => { // Save commit position to a persistent store as a checkpoint println!("checkpoint taken at {}", position.commit); } _ => {} } } ``` By default, the checkpoint notification is sent after every 32 non-system events processed from $all. ### Configuring the checkpoint interval You can adjust the checkpoint interval to change how often the client is notified. ```rs let filter = SubscriptionFilter::on_event_type().regex("/^[^\\$].*/"); let options = SubscribeToAllOptions::default().filter(filter); let mut sub = client.subscribe_to_all(&options).await; ``` By configuring this parameter, you can balance between reducing checkpoint overhead and ensuring quick recovery in case of a failure. ::: info The checkpoint interval parameter configures the database to notify the client after `n` \* 32 number of events where `n` is defined by the parameter. For example: * If `n` = 1, a checkpoint notification is sent every 32 events. * If `n` = 2, the notification is sent every 64 events. * If `n` = 3, it is sent every 96 events, and so on. ::: --- --- url: 'https://docs.kurrent.io/clients/tcp/index.md' --- # Deprecated Clients As of EventStoreDB version 24.2, TCP is no longer a supported protocol. You will find documentation for the deprecated TCP clients at: * [.NET client](./dotnet/21.2/quick-tour.md) * [JVM client](https://github.com/kurrent-io/EventStore.JVM) --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/index.md' --- # Legacy .NET Client ::: warning The TCP client protocol is now deprecated and not available in the latest versions of EventStoreDB. ::: --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/appending.md' --- # Appending events You can use the client API to append one or more events to a stream atomically. You do this by appending the events to the stream in one operation, or by using transactions. ::: note Sending events to a non-existing stream, implicitly creates the stream. ::: It is possible to make an optimistic concurrency check during the append by specifying the version at which you expect the stream to be. Identical append operations are idempotent if the optimistic concurrency check is not disabled. You can find more information on optimistic concurrency and idempotence [here](./appending.md#idempotence). The writing methods all use a type named `EventData` to represent an event to store (described [below](#eventdata)). The client library doesn't perform any serialization work, so you'd need to serialize both your event and its metadata to byte arrays. It allows you to use any serializer. ## Append events to a stream The `AppendToStreamAsync` method appends a single event or list of events atomically to the end of a stream, working in an asynchronous manner. Method definitions: ```csharp // Append one or more events using default connection credentials Task AppendToStreamAsync( string stream, long expectedVersion, params EventData[] events ); Task AppendToStreamAsync( string stream, long expectedVersion, IEnumerable events ); // Append one or more events using explicit credentials Task AppendToStreamAsync( string stream, long expectedVersion, UserCredentials userCredentials, params EventData[] events ); ``` Parameters: | Parameter | Description | | :------------------------------ | :------------- | | `string stream` | The name of the stream to which to append. | | `long expectedVersion` | The version at which you expect the stream to be in order that an optimistic concurrency check can be performed. This should either be a positive integer, or one of the constants `ExpectedVersion.NoStream`, `ExpectedVersion.EmptyStream`, or to disable the check, `ExpectedVersion.Any`. See [here](./appending.md#idempotence) for a broader discussion of this. | | `IEnumerable events` | The events to append. There is also an overload of each method which takes the events as a `params` array. `events`'s length must not be greater than 4095.| | `userCredentials` | Specify user on behalf whom write will be executed. | Example single event in single write: ```csharp var sampleObject = new { a = 2 }; var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(sampleObject)); var metadata = Encoding.UTF8.GetBytes("{}"); var evt = new EventData(Guid.NewGuid(), "event-type", true, data, metadata); await conn.AppendToStreamAsync("newstream", ExpectedVersion.Any, evt); ``` Example list of events in single write: ```csharp static EventData CreateSample(int i) { var sampleObject = new { a = i }; var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(sampleObject)); var metadata = Encoding.UTF8.GetBytes("{}"); var eventPayload = new EventData(Guid.NewGuid(), "event-type", true, data, metadata); return eventPayload; } static async Task Main() { var conn = EventStoreConnection.Create(new Uri("tcp://admin:changeit@localhost:1113")); await conn.ConnectAsync(); await conn.AppendToStreamAsync( "newstream", ExpectedVersion.Any, new[] { CreateSample(1), CreateSample(2), CreateSample(3) }); } ``` ## Transactions You might perform multiple writes to EventStoreDB as one transaction. However, the transaction can only append events to one stream. Transactions across multiple streams are not supported. You can open a transaction by using the `StartTransactionAsync` method of the `IEventStoreConnection` instance. After you got the transaction instance, you can use it to append events. Finally, you can either commit or roll back the transaction. A transaction can be long-lived and opening a transaction for a stream doesn't lock it. Another process can write to the same stream. In this case, your transaction might fail if you use idempotent writes with expected version. Method definitions on `IEventStoreConnection`: ```csharp Task StartTransactionAsync(string stream, long expectedVersion); EventStoreTransaction ContinueTransaction(long transactionId); ``` Method definitions on `EventStoreTransaction`: ```csharp Task WriteAsync(IEnumerable events); Task WriteAsync(params EventData[] events); Task CommitAsync(); void Rollback(); ``` Example: ```csharp static EventData CreateSample(int i) { var sampleObject = new { a = i }; var data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(sampleObject)); var metadata = Encoding.UTF8.GetBytes("{}"); var eventPayload = new EventData( Guid.NewGuid(), "event-type", true, data, metadata ); return eventPayload; } public static async Task Main() { var conn = EventStoreConnection.Create(new Uri("tcp://admin:changeit@localhost:1113")); await conn.ConnectAsync(); using var transaction = await conn.StartTransactionAsync("newstream", ExpectedVersion.Any); await transaction.WriteAsync(CreateSample(1)); await transaction.WriteAsync(CreateSample(2)); await conn.AppendToStreamAsync("newstream", ExpectedVersion.Any, CreateSample(3)); await transaction.WriteAsync(CreateSample(4)); await transaction.WriteAsync(CreateSample(5)); await transaction.CommitAsync(); } ``` Because the event `3` is appended outside of the transaction scope and before the transaction commits, it appears first in the stream. The second atomic write to the database is the whole transaction with events `1`, `2`, `4` and `5`. The stream will therefore contain events in the following order: `3, 1, 2, 4, 5`. ## EventData The writing methods all use a type named `EventData` to represent an event to be stored. Instances of `EventData` are immutable. Event Store does not have any built-in serialisation, so the body and metadata for each event are represented in `EventData` as a `byte[]`. The members on `EventData` are: | Member | Description | | :---------------- | :------ | | `Guid EventId` | A unique identifier representing this event. Event Store uses this for idempotency if you write the same event twice you should use the same identifier both times. | | `string Type` | The name of the event type. You can use this for pattern matching in projections, so should be a "friendly" name rather than a CLR type name, for example. | | `bool IsJson` | If the data and metadata fields are serialized as JSON, you should set this to `true`. Setting this to `true` will cause the projections framework to attempt to deserialize the data and metadata later. | | `byte[] Data` | The serialized data representing the event to be stored. | | `byte[] Metadata` | The serialized data representing metadata about the event to be stored. | ::: note An event size (not only data and metadata) must not exceed 16,777,215 bytes (16MB-1B). ::: ## Optimistic concurrency Append operation supports an [optimistic concurrency](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/optimistic-concurrency) check on the version of the stream to which events are appended. The .NET API has constants which you can use to represent certain conditions: | Parameter | Description | |:-----------|:------------| | `ExpectedVersion.Any` | Disables the optimistic concurrency check. | | `ExpectedVersion.NoStream` | Specifies the expectation that target stream does not yet exist. | | `ExpectedVersion.EmptyStream` | Specifies the expectation that the target stream has been explicitly created, but does not yet have any user events appended in it. | | `ExpectedVersion.StreamExists` | Specifies the expectation that the target stream or its metadata stream has been created, but does not expect the stream to be at a specific event number. | | `Any other integer value` | The event number that you expect the stream to currently be at. | If the optimistic concurrency check fails during appending, a `WrongExpectedVersionException` is thrown. ## Idempotence If identical append operations occur, EventStoreDB treats them in a way which makes it idempotent. If a append is treated in this manner, EventStoreDB acknowledges it as successful, but duplicate events are not appended. The idempotence check is based on the `EventId` and `stream`. It is possible to reuse an `EventId` across streams whilst maintaining idempotence. The level of idempotence guarantee depends on whether the optimistic concurrency check is not disabled during appending (by passing `ExpectedVersion.Any` as the `expectedVersion` for the append). ### With expected version The specified `expectedVersion` is compared to the `currentVersion` of the stream. This will yield one of three results: * **`expectedVersion > currentVersion`** - a `WrongExpectedVersionException` is thrown. * **`expectedVersion == currentVersion`** - events are appended and acknowledged. * **`expectedVersion < currentVersion`** - the `EventId` of each event in the stream starting from `expectedVersion` are compared to those in the append operation. This can yield one of three further results: * **All events have been committed already** - the append operation is acknowledged as successful, but no duplicate events appended. * **None of the events were previously committed** - a `WrongExpectedVersionException` is thrown. * **Some events were previously committed** - this is considered a bad request. If the append operation contains the same events as a previous request, either all or none of the events should have been previously committed. This surfaces as a `WrongExpectedVersionException`. ### With ExpectedVersion.Any ::: warning Idempotence is **not** guaranteed if you use `ExpectedVersion.Any`. The chance of a duplicate event is small, but is possible. ::: The idempotence check will yield one of three results: * **All events have been committed already** - the append operation is acknowledged as successful, but no duplicate events appended. * **None of the events were previously committed** - the events are appended and the append operation acknowledged. * **Some events were previously committed** - this is considered a bad request. If the append operation contains the same events as a previous request, either all or none of the events should have been previously committed. This currently surfaces as a `WrongExpectedVersionException`. --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/connecting.md' --- # Connecting to EventStoreDB Describe connecting to the single node and to the cluster. Gossip seeds, using ip addresses and DNS. The .NET Client API communicates with EventStoreDB over TCP, using length-prefixed serialised protocol buffers. The API allows for reading and appending operations, as well as for subscriptions to individual event streams or all events appended. ## EventStoreConnection The `EventStoreConnection` class maintains a full-duplex connection between the client and the EventStoreDB server. `EventStoreConnection` is thread-safe, and we recommend that you create one node per application. EventStoreDB handles all connections asynchronously, returning either a `Task` or a `Task`. ::: tip To get maximum performance from a non-blocking connection, we recommend you use it asynchronously. ::: ## Quick start The code below shows how to connect to an EventStoreDB server, appends to a stream, and read back the events. For more detailed information, read the full pages for connecting to a server using [connection string](#connection-string) and [connection settings](#connection-settings), [reading events](reading.md) and [appending to a stream](appending.md) ::: tabs @tab JSON format event @[code{12-28}](./sample-code/DotNetClient/QuickStartJsonFormat.cs) @tab Plain-text format event @[code{12-28}](./sample-code/DotNetClient/QuickStartPlainFormat.cs) ::: ::: tip We recommended using the JSON format for data and metadata. ::: ## Connection string Many of the `EventStoreConnection.Create` overloads accept a connection string that you can use to control settings of the connection. A benefit to having these as a connection string instead of using the fluent API is that you can change them between environments without recompiling (i.e. a single node in `dev` and a cluster in `production`). For example, the following code will create a connection to an EventStoreDB local node, and then open the connection: ```csharp var connectionString = "ConnectTo=tcp://admin:changeit@localhost:1113;"; var connection = EventStoreConnection.Create(connectionString, builder); await connection.ConnectAsync(); ``` Here are all available overloads for the `Create` methods of the `EventStoreConnection` class, which use the connection string: | Method | Description | |:---------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------| | `Create(string connectionString)` | Connects to EventStoreDB with settings from connection string | | `Create(string connectionString, ConnectionSettingsBuilder builder)` | Connects to EventStoreDB by merging connection string settings with pre-populated builder for additional settings | The connection string format should look familiar to those who have used connection strings in the past. It consists of a series of key/value pairs separated by semicolons. You can set the following values using the connection string: | Name | Format | Description | |:----------------------------|:----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------| | ConnectTo | A URI in format described above to connect to | The URI to connect to | | GossipSeeds | Comma separated list of ip:port | A list of seeds to try to discover from | | ClusterDns | String | The DNS name of the cluster for discovery | | ExternalGossipPort | Integer | The port to try to gossip on | | DefaultUserCredentials | String in format username:password | The default credentials for the connection | | MaxQueueSize | Integer | Maximum number of outstanding operations | | MaxConcurrentItems | Integer | Maximum number of concurrent async operations | | MaxRetries | Integer | Maximum number of retry attempts | | MaxReconnections | Integer | The maximum number of times to try reconnecting | | RequireMaster | True/false | If set the server will only process if it is master | | ReconnectionDelay | Integer (milliseconds) | The delay before attempting to reconnect | | OperationTimeout | Integer (milliseconds) | The time before considering an operation timed out | | OperationTimeoutCheckPeriod | Integer (milliseconds) | The frequency in which to check timeouts | | UseSslConnection | True/false | Whether to use SSL for this connection | | TargetHost | String | The hostname expected on the certificate | | ValidateServer | True/false | Whether to validate the remote server | | FailOnNoServerResponse | True/False | Whether to fail on no server response | | HeartbeatInterval | Integer (milliseconds) | The interval at which to send the server a heartbeat | | HeartbeatTimeout | Integer (milliseconds) | The amount of time to receive a heartbeat response before timing out | | MaxDiscoverAttempts | Integer | The maximum number of attempts to try to discover the cluster | | GossipTimeout | Integer (milliseconds) | The amount of time before timing out a gossip response | | VerboseLogging | True/false | Enables verbose logging | | Compatibility Mode | auto, 20 | Enables the client to connect to either server configuration without needing to change the client's connection settings | You can specify only one of `ConnectTo`, `ClusterDns` and `GossipSeeds`. Also, you'd only need to define `ExternalGossipPort` if you connect to the cluster using the DNS name (`ClusterDns`). The gossip port is usually the external HTTP port. When connecting to the cluster using `GossipSeeds`, you need to specify the gossip port for each node address in the list of seeds. ::: tip You can also use spacing instead of camel casing in your connection string. ::: ``` ConnectTo=tcp://admin:changeit@localhost:1113; HeartBeatTimeout=500 ``` Sets the connection string to connect to `localhost` on the default port and sets the heartbeat timeout to 500ms. ``` Connect To = tcp://admin:changeit@localhost:1113; HeartBeatTimeout=500 ``` Using spaces: ``` ConnectTo=discover://admin:changeit@mycluster:3114; HeartBeatTimeout=500 ``` Tells the connection to try gossiping to a manager node found under the DNS `mycluster` at port `3114` to connect to the cluster. ``` GossipSeeds=192.168.0.2:1111,192.168.0.3:1111; HeartBeatTimeout=500 ``` Tells the connection to try gossiping to the gossip seeds `192.168.0.2` or `192.168.0.3` on port '1111' to discover information about the cluster. ::: tip You can also use the `ConnectionString` class to return a `ConnectionSettings` object. ::: ## Connection settings The `EventStoreConnection` class uses the static `Create` methods to create a new connection. All method overloads allow you to specify a name for the connection, which the connection returns when it raises events (see [Connection Events](#connection-events)). Instances of `ConnectionSettings` are created using a fluent builder class: ```csharp var settingsBuilder = ConnectionSettings.Create(); ``` This creates an instance of `ConnectionSettingsBuilder` with default settings. You can override these by chaining the additional builder methods described below. When you have a builder with all the settings configured, use the `Build` method to create the `ConnectionSettings` instance and then use it to create a connection: ```csharp var settings = settingsBuilder.Build(); var connection = EventStoreConnection.Create(settings); ``` Settings builder supports fluent API, so each of its configuration methods returns the reference to itself. It allows you to chain the configuration calls, finalising it by the `Build` method call: ```csharp var settings = ConnectionSettings .Create() .KeepReconnecting() .Build(); var connection = EventStoreConnection.Create( settings, new Uri("tcp://admin:changeit@localhost:1113") ); ``` You can also pass the builder instead of the `ConnectionSettings` instance. In this case, the builder will be implicitly converted to the settings instance by calling the `Build` method under the hood. Here are all available overloads for the `Create` methods of the `EventStoreConnection` class, which use connection settings: | Method | Description | |:------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------| | `Create(ConnectionSettings connectionSettings)` | Connects to EventStoreDB using specified settings | | `Create(Uri uri)` | Connects to EventStoreDB (see URIs below) with default settings | | `Create(ConnectionSettings connectionSettings, Uri uri)` | Connects to EventStoreDB (see URIs below) with specified settings | | `Create(string connectionString)` | Connects to EventStoreDB with settings from connection string | | `Create(string connectionString, ConnectionSettingsBuilder builder)` | Connects to EventStoreDB by merging connection string settings with pre-populated builder for additional settings | | `Create(ConnectionSettings connectionSettings, IEndPointDiscover endPointDiscover)` | Connects to an EventStoreDB cluster with custom settings. | ::: tip The connection returned by these methods is inactive. Use the `ConnectAsync()` method to establish a connection with the server. ::: ### URIs The create methods support passing of a URI to the connection as opposed to passing `IPEndPoints`. This URI should be in the format of: * **Single Node**: `tcp://user:password@myserver:11234` * **Cluster**: `discover://user:password@myserver:1234` Where the port number points to the TCP port of the EventStoreDB instance (1113 by default) or points to the manager gossip port for discovery purposes. With the URI based mechanism you can pass a DNS name and the client will resolve it. The client performs a blocking DNS call for single node. If you are worried about blocking DNS due to network issues etc., you should resolve the DNS yourself and pass in an IP address. ### Logging The .NET client can log to different destinations. By default logging is disabled. | Builder Method | Description | |:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `UseConsoleLogger()` | Output log messages using `Console.WriteLine` | | `UseDebugLogger()` | Output log messages using `Debug.WriteLine` | | `UseCustomLogger()` | Output log messages to the specified instance of `ILogger` (You should implement this interface in order to log using another library such as NLog or log4net). | | `EnableVerboseLogging()` | Turns on verbose logging. | By default, information about connection, disconnection and errors are logged, however it can be useful to have more information about specific operations as they are occurring. ### User credentials EventStoreDB supports [Access Control Lists](security.md#access-control-lists) that restrict permissions for a stream based on users and groups. `EventStoreConnection` allows you to supply credentials for each operation, however it is often more convenient to set default credentials for all operations on the connection. | Builder Method | Description | |:---------------|:------------| | `SetDefaultUserCredentials(UserCredentials credentials)` | Sets the default `UserCredentials` to use for this connection. If you don't supply any credentials, the operation will use these. | You create a `UserCredentials` object as follows: ```csharp var credentials = new UserCredentials("username", "password"); settingsBuilder.SetDefaultUserCredentials(credentials); ``` ### Security The .NET API and EventStoreDB can communicate either over SSL or an unencrypted channel (by default). To configure the client-side of the SSL connection, use the builder method below. For more information on setting up the server end of the EventStoreDB for SSL, see [SSL Setup](security.md). ```csharp UseSslConnection(string targetHost, bool validateServer) ``` Uses an SSL-encrypted connection where `targetHost` is the name specified on the SSL certificate installed on the server, and `validateServer` controls whether the connection validates the server certificate. ::: warning In production systems where credentials are sent from the client to EventStoreDB, you should always use SSL encryption and you should set `validateServer` to `true`. ::: ### Node preference When connecting to an EventStoreDB HA cluster you can specify that operations are performed on any node, or only on the node that is the master. | Builder Method | Description | |:------------------------|:-----------------------------------------------------------------------------------------------------------| | `PerformOnMasterOnly()` | Require the master to serve all write and read requests (Default). | | `PerformOnAnyNode()` | Allow for writes to be forwarded and read requests to be served locally if the current node is not master. | ### Handling failures The following methods on the `ConnectionSettingsBuilder` allow you to change the way the connection handles operation failures and connection issues. #### Reconnections | Builder Method | Description | |:---------------|:------------| | `WithConnectionTimeoutOf (TimeSpan timeout)` | Sets the timeout to connect to a server before aborting and attempting a reconnect (Default: 1 second). | | `LimitReconnectionsTo (int limit)` | Limits the number of reconnections this connection can try to make (Default: 10). | | `KeepReconnecting()` | Allows infinite reconnection attempts.| | `SetReconnectionDelayTo (TimeSpan reconnectionDelay)` | Sets the delay between reconnection attempts (Default: 100ms).| | `SetHeartbeatInterval (TimeSpan interval)`| Sets how often the connection should expect heartbeats (lower values detect broken sockets faster) (Default: 750ms). | | `SetHeartbeatTimeout (TimeSpan timeout)` | Sets how long to wait without heartbeats before determining a connection to be dead (must be longer than the heartbeat interval) (Default: 1500ms). | #### Operations | Builder Method | Description | |:--------------------------------------------------------|:-------------------------------------------------------------------------| | `SetOperationTimeout (TimeSpan timeout)` | Sets the operation timeout duration (Default: 7 seconds). | | `SetTimeoutCheckPeriodTo (TimeSpan timeoutCheckPeriod)` | Sets how often to check for timeouts (Default: 1 second). | | `LimitAttemptsForOperationTo (int limit)` | Limits the number of operation attempts (Default: 11). | | `LimitRetriesForOperationTo (int limit)` | Limits the number of operation retries (Default: 10). | | `KeepRetrying()` | Allows infinite operation retries. | | `LimitOperationsQueueTo (int limit)` | Sets the limit for number of outstanding operations (Default: 5000). | | `FailOnNoServerResponse()` | Marks that no response from server should cause an error on the request. | ### Cluster settings Cluster settings are now embedded to the connection settings and all overloads that use `ClusterSettings` are obsolete. You can use the connection settings instance to configure cluster connection: ```csharp var settings = ConnectionSettings .Create() .SetClusterDns("esdb.acme.cool") .SetClusterGossipPort(2113) .Build(); var connection = EventStoreConnection.Create(settings); ``` ### Connection events `EventStoreConnection` exposes events that your application can use to be notified of changes to the status of the connection. | Event | Description | |:------|:------------| | `Connected` | Successfully connected to server | | `Disconnected` | Disconnected from server by some means other than by calling the `Close` method. | | `Reconnecting` | Attempting to reconnect to server following a disconnection | | `Closed` | Connection got closed either using the `Close` method or when reconnection limits are reached without a successful connection being established | | `ErrorOccurred` | Connection experienced an error | | `AuthenticationFailed` | Failed to authenticate | ### Compatibility Mode Enables the client to connect to either server configuration without needing to change the client's connection settings. | Builder Method | Description | |:---------------|:------------| | `SetCompatibilityMode(string value)` | Specifies if the client should run in a specific version compatibility mode. Set `"auto"` for Auto-Compatibility Mode or `"20"` for v20 Compatibility Mode | #### Auto-compatibility mode Auto-compatibility mode was added to make the upgrade from an insecure v5 cluster to a secure v20+ cluster easier by allowing the client to connect to either server configuration without needing to change the client's connection settings. It does this by sending both an insecure and a secure gossip request when first discovering the server. Whichever of these requests succeeds sets whether the gossip is to be done over HTTP or HTTPS. Once the cluster is discovered, the client will check the gossip to determine whether the TCP connection should be secure or insecure, and connect accordingly. This means that you no longer need to specify `GossipOverTls` or `UseSslConnection` in the connection settings or connection string. ::: note Since auto-compatibility mode will send two requests at discovery and expect one to fail, you will see a gossip failure in the log when the client starts. This is expected to only happen on the first request. ::: ##### Usage Auto-Compatibility Mode is supported when connecting with Gossip Seeds or Cluster DNS Discovery. You can enable auto-compatibility mode with `CompatibilityMode=auto` in the connection string, or with `.SetCompatibilityMode("auto")` in the connection settings. You can connect to a cluster running insecure v5, insecure v20+, or secure v20+ with the following configurations: ###### Connection string ::: tabs @tab Cluster DNS Discovery ```csharp var clusterDnsConnectionString = $"ConnectTo=discover://{dns_address}:2113;TargetHost={dns_address};" + "CompatibilityMode=auto;ValidateServer=true;" ``` @tab Gossip Seeds ```csharp var gossipSeedConnectionSTring = $"GossipSeeds={node1}:2113,{node2}:2113,{node3}:2113;" + "CompatibilityMode=auto;ValidateServer=true;" ``` ::: ::: warning Auto-compatibility mode does not enable Server Certificate Validation by default. As such, we recommend that you enable this explicitly in your connection string. ::: ###### Connection settings ::: tabs @tab Cluster DNS Discovery @[code{AutoCompatibilityWithClusterDns}](./sample-code/DotNetClient/CompatibilityMode.cs) @tab Gossip Seeds @[code{AutoCompatibilityWithGossipSeeds}](./sample-code/DotNetClient/CompatibilityMode.cs) ::: ###### v5 Compatibility Mode The v5 Compatibility Mode allows the v21.2 client to connect to a v5 cluster. You can set this with `CompatibilityMode=5` in the connection string, or with `.SetCompatibilityMode("5")` in the connection settings. For example: ```csharp var connectionString = $"ConnectTo=discover://{cluster_dns}:2113?TargetHost={cluster_dns};CompatibilityMode=5;" ``` --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/embedded.md' --- # Embedded client ## EmbeddedVNodeBuilder The `EmbeddedVNodeBuilder` class sets up and builds an EventStoreDB node. You can configure your node through methods provided by the `EmbeddedVNodeBuilder` class. ::: tip The builder used for the `EmbeddedVNodeBuilder` is the same EventStoreDB uses internally to create the `ClusterNode`, see *EventStore.ClusterNode.Program.cs* for more examples on how to use it. ::: ## Building a node You have two options when you start creating a node, `EmbeddedVNodeBuilder.AsSingleNode()` or `EmbeddedVNodeBuilder.AsClusterMember(clusterSize)`, which will create a single node or a cluster node respectively. After creating the builder, you can configure the node through the methods provided by the `EmbeddedVNodeBuilder`. These are listed below. Once you have configured the node, build it with `EmbeddedVNodeBuilder.Build()` which returns the configured `ClusterVNode`. Start the node with `ClusterVNode.StartAndWaitUntilReady()` or `ClusterVNode.Start()`. `ClusterVNode.StartAndWaitUntilReady()` returns a task that completes once the node has started and all subsystems have finished loading. For example, to build a single node with default options : ```csharp var nodeBuilder = EmbeddedVNodeBuilder .AsSingleNode() .OnDefaultEndpoints() .RunInMemory(); var node = nodeBuilder.Build(); await node.StartAndWaitUntilReady(); ``` To build a node to be part of a cluster with custom endpoints and gossip seeds: ```csharp var nodeBuilder = EmbeddedVNodeBuilder .AsClusterMember(3) .RunOnDisk("node1db") .WithInternalHttpOn(new IPEndPoint(IPAddress.Loopback, 1112)) .WithExternalHttpOn(new IPEndPoint(IPAddress.Loopback, 1113)) .WithExternalTcpOn(new IPEndPoint(IPAddress.Loopback, 1114)) .WithInternalTcpOn(new IPEndPoint(IPAddress.Loopback, 1115)) .DisableDnsDiscovery() .WithGossipSeeds(new IPEndPoint[] { new IPEndPoint(IPAddress.Loopback, 2112), new IPEndPoint(IPAddress.Loopback, 3112) }); var node = nodeBuilder.Build(); node.Start(); ``` ::: warning When running an embedded cluster, the task returned by `StartAndWaitUntilReady()` only completes on the master node. ::: ## Connecting to an embedded node You can connect to an embedded EventStoreDB node with the `EmbeddedEventStoreConnection` class. Calling `EmbeddedEventStoreConnection.Create(ClusterVNode)` returns an `IEventStoreConnection` configured to connect to your embedded node. From there you can use the connection as normal in the .NET Client. ```csharp using var embeddedConn = EmbeddedEventStoreConnection.Create(node); await embeddedConn.ConnectAsync(); await embeddedConn.AppendToStreamAsync( "some-stream", ExpectedVersion.Any, new EventData(Guid.NewGuid(), "eventType", true, Encoding.UTF8.GetBytes("{\"Foo\":\"Bar\"}"), null) ); ``` ## Logging with an embedded node To enable logging for an embedded node, you need to initialize the `LogManager` and ensure that you configure the logger with a `log.config` file in your configuration directory. To initialize the `LogManager`, call this before building the nodes: ```csharp LogManager.Init(logComponentName, logDirectory, logConfigurationDirectory); ``` ## EmbeddedVNodeBuilder options The following options are available when building an Embedded Node. ### Application options | Method | Description | |:-------|:------------| | `AsSingleNode()` | Returns a builder set to construct options for a single node instance | | `AsClusterMember(int clusterSize)` | Returns a builder set to construct options for a cluster node instance with a cluster size | | `DisableHTTPCaching()` | Disable HTTP Caching | | `WithWorkerThreads(int count)` | Sets the number of worker threads to use in shared threadpool | | `WithStatsPeriod(TimeSpan statsPeriod)` | Sets the period between statistics gathers | | `EnableLoggingOfHttpRequests()` | Enable logging of HTTP requests and responses before they are processed | | `EnableHistograms()` | Enable the tracking of histograms, typically used for debugging | | `EnableTrustedAuth()` | Enable trusted authentication by an intermediary in the HTTP | ### Certificate options | Method | Description | |:-------|:------------| | `WithServerCertificateFromFile(string path, string password)` | Sets the Server SSL Certificate loaded from a file | | `WithServerCertificate(X509Certificate2 sslCertificate)` | Sets the Server SSL Certificate | | `WithServerCertificateFromStore(StoreLocation storeLocation, StoreName storeName, string certificateSubjectName, string certificateThumbprint)` | Sets the Server SSL Certificate loaded from a certificate store | | `WithServerCertificateFromStore(StoreName storeName, string certificateSubjectName, string certificateThumbprint)` | Sets the Server SSL Certificate loaded from a certificate store | ### Cluster options | Method | Description | |:-------|:------------| | `WithClusterGossipPort(int port)` | Sets the internal gossip port (used when using cluster DNS, this should point to a known port gossip will be running on) | | `WithGossipSeeds(params IPEndPoint[] endpoints)` | Sets the gossip seeds this node should talk to | | `WithClusterDnsName(string name)` | Sets the DNS name used for the discovery of other cluster nodes | | `DisableDnsDiscovery()` | Disable DNS discovery for the cluster | | `WithGossipInterval(TimeSpan gossipInterval)` | Sets the gossip interval | | `WithGossipAllowedTimeDifference(TimeSpan gossipAllowedDifference)` | Sets the allowed gossip time difference | | `WithGossipTimeout(TimeSpan gossipTimeout)` | Sets the gossip timeout | | `WithPrepareTimeout(TimeSpan prepareTimeout)` | Sets the prepare timeout | | `WithCommitTimeout(TimeSpan commitTimeout)` | Sets the commit timeout| | `WithPrepareCount(int prepareCount)` | Sets the number of nodes which must acknowledge prepares. | | `WithCommitCount(int commitCount)` | Sets the number of nodes which must acknowledge commits before acknowledging to a client. | | `WithNodePriority(int nodePriority)` | Sets the node priority used during master election | ### Database options | Method | Description | |:-------|:------------| | `RunInMemory()` | Sets the builder to run in memory | | `RunOnDisk(string path)` | Sets the builder to write database files to the specified path | | `MaximumMemoryTableSizeOf(int size)` | Sets the maximum size a memtable is allowed to reach (in count) before moved to be a ptable | | `DoNotVerifyDbHashes()` | Marks that the existing database files should not be checked for checksums on startup. | | `VerifyDbHashes()` | Marks that the existing database files should be checked for checksums on startup. | | `WithMinFlushDelay(TimeSpan minFlushDelay)` | Sets the minimum flush delay | | `DisableScavengeMerging()` | Disables the merging of chunks when scavenge is running | | `WithScavengeHistoryMaxAge(int scavengeHistoryMaxAge)` | The number of days to keep scavenge history (Default: 30) | | `WithIndexPath(string indexPath)` | Sets the path the index should be loaded or saved to | | `WithIndexCacheDepth(int indexCacheDepth)` | Sets the depth to cache for the mid point cache in index | | `WithUnsafeIgnoreHardDelete()` | Disables Hard Deletes (UNSAFE: use to remove hard deletes) | | `WithUnsafeDisableFlushToDisk()` | Disables Hard Deletes (UNSAFE: use to remove hard deletes) | | `WithBetterOrdering()` | Enable queue affinity on reads during write process to try to get better ordering. | | `WithTfChunkSize(int chunkSize)` | Sets the transaction file chunk size. Default is `TFConsts.ChunkSize` | | `WithTfCachedChunks(int cachedChunks)` | The number of chunks to cache in unmanaged memory. Default is `TFConsts.ChunksCacheSize` | ### Interface options | Method | Description | |:------ |:----------- | | `OnDefaultEndpoints()` | Sets the default endpoints on localhost (1113 tcp, 2113 http) | | `AdvertiseInternalIPAs(IPAddress intIpAdvertiseAs)` | Sets up the Internal IP to advertise | | `AdvertiseExternalIPAs(IPAddress extIpAdvertiseAs)` | Sets up the External IP to advertise | | `AdvertiseInternalHttpPortAs(int intHttpPortAdvertiseAs)` | Sets up the Internal HTTP port to advertise | | `AdvertiseExternalHttpPortAs(int extHttpPortAdvertiseAs)` | Sets up the External HTTP port to advertise | | `AdvertiseInternalSecureTCPPortAs(int intSecureTcpPortAdvertiseAs)` | Sets up the Internal Secure TCP port to advertise | | `AdvertiseExternalSecureTCPPortAs(int extSecureTcpPortAdvertiseAs)` | Sets up the External Secure TCP port to advertise | | `AdvertiseInternalTCPPortAs(int intTcpPortAdvertiseAs)` | Sets up the Internal TCP port to advertise | | `AdvertiseExternalTCPPortAs(int extTcpPortAdvertiseAs)` | Sets up the External TCP port to advertise | | `WithInternalHttpOn(IPEndPoint endpoint)` | Sets the internal HTTP endpoint to the specified value | | `WithExternalHttpOn(IPEndPoint endpoint)` | Sets the external HTTP endpoint to the specified value | | `WithInternalTcpOn(IPEndPoint endpoint)` | Sets the internal TCP endpoint to the specified value | | `WithInternalSecureTcpOn(IPEndPoint endpoint)` | Sets the internal secure TCP endpoint to the specified value | | `WithExternalTcpOn(IPEndPoint endpoint)` | Sets the external TCP endpoint to the specified value | | `WithExternalSecureTcpOn(IPEndPoint endpoint)` | Sets the external secure TCP endpoint to the specified value | | `EnableSsl()` | Sets that SSL should be used on connections | | `WithSslTargetHost(string targetHost)` | Sets the target host of the server's SSL certificate. | | `ValidateSslServer()` | Sets whether to validate that the server's certificate is trusted. | | `NoGossipOnPublicInterface()` | Disables gossip on the public (client) interface | | `NoAdminOnPublicInterface()` | Disables the admin interface on the public (client) interface | | `NoStatsOnPublicInterface()` | Disables statistics screens on the public (client) interface | | `AddInternalHttpPrefix(string prefix)` | Adds a HTTP prefix for the internal HTTP endpoint | | `AddExternalHttpPrefix(string prefix)` | Adds a HTTP prefix for the external HTTP endpoint | | `DontAddInterfacePrefixes()` | Don't add the interface prefixes (e.g. If the External IP is set to the Loopback address, add `http://localhost:2113/` as a prefix) | | `WithInternalHeartbeatInterval(TimeSpan heartbeatInterval)` | Sets the heartbeat interval for the internal network interface. | | `WithExternalHeartbeatInterval(TimeSpan heartbeatInterval)` | Sets the heartbeat interval for the external network interface. | | `WithInternalHeartbeatTimeout(TimeSpan heartbeatTimeout)` | Sets the heartbeat timeout for the internal network interface. | | `WithExternalHeartbeatTimeout(TimeSpan heartbeatTimeout)` | Sets the heartbeat timeout for the external network interface. | ### Projections options | Method | Description | |:------ |:----------- | | `StartStandardProjections()` | Start standard projections. | | `RunProjections(ProjectionType projectionType, int numberOfThreads = Opts.ProjectionThreadsDefault)` | Sets the mode and the number of threads on which to run projections. | | `RunProjections(ClientAPI.Embedded.ProjectionsMode projectionsMode, int numberOfThreads = Opts.ProjectionThreadsDefault)` | Sets the mode and the number of threads on which to run projections. | ## EmbeddedEventStoreConnection The following methods are available on `EmbeddedEventStoreConnection` for connecting to an embedded node. | Method | Description | |:------ |:----------- | | `Create(ClusterVNode eventStore, string connectionName = null)` | Creates a new embedded `IEventStoreConnection` to a single node with default connection settings | | `Create(ClusterVNode eventStore, ConnectionSettings connectionSettings, string connectionName = null)` | Creates a new embedded `IEventStoreConnection` to a single node using specific `ConnectionSettings` | --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/examples/index.md' --- # Usage examples In the *Examples* section you can find reference implementation for the most popular use cases of EventStoreDB in the context of Event Sourcing. --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/examples/aggregate-store.md' --- # Aggregate store --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/examples/aggregate.md' --- # Aggregate --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/examples/read-models.md' --- # Read models --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/migration-to-gRPC.md' --- # Migration to gRPC client The TCP client is considered legacy. We recommend migrating to the gRPC client. We decided to use gRPC as our communication protocol, as it enabled us to standardise client communication. Thanks to that, we benefit from the built-in gRPC functionalities like streaming, backpressure, etc., instead of building custom implementations. As gRPC is multi-platform and a widely adopted standard, it has enabled us to provide a unified approach and deliver clients for other dev environments like Go, JVM, NodeJS, and Rust. The TCP client will still get necessary bug fixes, but new database features will be only delivered for gRPC clients (e.g., persistent subscription to `$all`). Having all of that, we encourage you to migrate to the gRPC client. This document outlines the needed steps. Please see the [gRPC documentation](@clients/grpc/README.md) for more details on how to use it. See also the webinar that shows step by step on practical example of the whole migration process: ## Update the target .NET framework The gRPC client doesn't support .NET Standard. The change comes from using different gRPC implementations for the specific .NET Framework version to tune the performance. If you were using it in the .NET Standard library, you have to update it to the latest .NET. ```xml{3-4} - netstandard2.0 + net8.0 ``` If you use .NET Framework, you'll have to make sure that your application is targeting .NET Framework 4.8 or later. EventStoreDB only supports gRPC calls with HTTP/2, and it is only possible to use HTTP/2 on Windows 11 and Windows Server 2019 when building applications using .NET Framework. This limitation doesn't apply to .NET Core 3+ and .NET 5+. Check the compatibility note in [.NET documentation](https://learn.microsoft.com/en-us/aspnet/core/grpc/supported-platforms?view=aspnetcore-8.0#net-grpc-client-requirements). ## Update package references To use the gRPC client, you need to replace the TCP client package (`EventStore.Client`) with the gRPC one (`EventStore.Client.Grpc.Streams`). ```xml{2-3} - + ``` ::: tip Use the latest version of `EventStore.Client.Grpc.Streams` to access the latest server features and SDK improvements. ::: ## Migration strategies You may also consider a step by step migration: * Add the gRPC client and keep the TCP one * Gradually replace client usages. For example, start with append and read operations, keeping subscriptions on the TCP client. Once that's settled, move the subscriptions code to the gRPC client. * Remove the TCP client package reference as the last step. You may also consider wrapping common logic into extension methods or repository classes. Adding a temporary wrapping layer lets us centralise and isolate the changes we perform during the migration. Thanks to that, you can replace the inner implementations, keeping usages the same. Read more in Martin Fowler's article [An example of preparatory refactoring](https://martinfowler.com/articles/preparatory-refactoring-example.html). Sample wrapper for the TCP client: ```csharp public class EventStore { readonly IEventStoreConnection tcpConnection; public EventStore(IEventStoreConnection tcpConnection) => this.tcpConnection = tcpConnection; public Task AppendEvents( string streamName, long version, params object[] events ) { var preparedEvents = events .Select(ToEventData) .ToArray(); return tcpConnection.AppendToStreamAsync( streamName, version, preparedEvents ); static EventData ToEventData(object @event) => new EventData( Guid.NewGuid(), TypeMapper.GetTypeName(@event.GetType()), true, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)) ); } public Task AppendEvents( string streamName, params object[] events ) => AppendEvents(streamName, ExpectedVersion.Any, events); public async Task> LoadEvents(string stream) { const int pageSize = 4096; var start = 0; var events = new List(); do { var page = await tcpConnection.ReadStreamEventsForwardAsync( stream, start, pageSize, true ); if (page.Status == SliceReadStatus.StreamNotFound) throw new ArgumentOutOfRangeException( nameof(stream), $"Stream '{stream}' was not found" ); events.AddRange(page.Events.Select(Deserialize)); if (page.IsEndOfStream) break; start += pageSize; } while (true); return events; static object Deserialize(this ResolvedEvent resolvedEvent) { var dataType = TypeMapper.GetType(resolvedEvent.Event.EventType); var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); var data = JsonConvert.DeserializeObject(jsonData, dataType); return data; } } public async Task StreamExists(string stream) { var result = await tcpConnection.ReadEventAsync(stream, 1, false); return result.Status != EventReadStatus.NoStream; } } ``` ## Differences in connection management Both the TCP and gRPC clients manage reconnections. Therefore, it should be registered as a single instance in the application. The TCP client requires calling the `ConnectAsync` method at least once to initiate the connection. That wasn't ideal if you wanted to inject an already set-up connection, as you either had to call it asynchronously or risk deadlocks. ```csharp private async Task GetEventStoreConnection(string connectionString) { var tcpConnection = EventStoreConnection.Create(connectionString); await tcpConnection.ConnectAsync(); return tcpConnection; } ``` The TCP client connection logic is not thread-safe (`ConnectAsync` method), however the other operations are thread-safe. Because of that, you have to ensure that you won't have a race condition. You also should consider handling `Closed` events to manage reconnection if it is closed. The TCP client supports built-in reconnections; however, you need to be careful about setting the connections options properly. Wrongly defined settings can cause a flood of reconnections, increasing the chance for failure. For instance, additional retries may worsen if the reason was a high load on the database. Sample code with reconnections could look like this: ```csharp private async Task GetEventStoreConnection(string connectionString) { var tcpConnection = EventStoreConnection.Create( connectionString, ConnectionSettings.Create() .FailOnNoServerResponse() .KeepReconnecting() .SetOperationTimeoutTo(TimeSpan.FromSeconds(5)) .LimitAttemptsForOperationTo(7) .LimitRetriesForOperationTo(7) ); await tcpConnection.ConnectAsync(); return tcpConnection; } ``` In the gRPC client, the main difference is that there is no constantly open connection, so no reconnections have to be made. You also don't need to call the `ConnectAsync` method. That simplifies the client initialisation and dependency injection. For the TCP client, you either had to inject the `Task` and each time await it, e.g. ```csharp public class EventStore { readonly Task connect; public EventStore(Task connect) => this.connect = connect; public async Task AppendEvents( string streamName, long version, params object[] events ) { var preparedEvents = events .Select(ToEventData) .ToArray(); var tcpConnection = await connect; return await tcpConnection.AppendToStreamAsync( streamName, version, preparedEvents ); } } ``` Or initialise it in the application Startup. For the gRPC client, we can shorten that to: ```csharp private EventStoreClient GetEventStoreConnection(string connectionString) { var settings = EventStoreClientSettings.Create(connectionString); return new EventStoreClient(settings); } ``` If you're using Dependency Injection, you can safely register it as Singleton in DI Container, e.g.: ```csharp var client = GetEventStoreConnection(Configuration["eventStore:connectionString"]) services.AddSingleton(client); ``` ### Connection string For the gRPC client, we recommend switching from the settings object to using a connection string. All the settings are exposed through it. The TCP client and gRPC client connection strings are not compatible with each other. However, a unified approach to using connection strings instead of settings can help in the step by step migration. Read more [here](@clients/grpc/getting-started.md#connection-string) about the connection string format. ## Security EventStoreDB from version 20.6 is secured by default. The gRPC clients follow that approach. You can use an insecure connection by providing `tls=false` in the connection string, but we don't recommend it for scenarios other than local development. Access Control List checks are not performed on an insecure connection. Read more in [database security docs](@server/security/protocol-security.md). ## Appending events ### Event Data There are minor changes to the `EventData` signature: * The namespace was changed from `EventStore.ClientAPI` to `EventStore.Client` * Event id requires using the `Uuid` class instead of `Guid`. We adopted the [Universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier) standard to provide unified behaviour between all gRPC clients. * We have allowed providing a content type. It *potentially* enables more advanced serialisation scenarios (for example, using a few non-JSON serialisation formats). Instead of setting the `isJson` flag for the gRPC client, you should provide the text value of the content type. The default one is `application/json`, which is equivalent to setting `isJson` flag to `true`. If you're using the custom format, you should provide its name, e.g. `application/octet-stream`. ::: warning As per now, EventStoreDB only supports `application/json` and `application/octet-stream` content types. This limitation will be removed in the future. ::: ::: note In the samples below, we are using [Json.NET](https://www.newtonsoft.com/json), as it was commonly used to serialise JSON event data in TCP clients. You may consider [migrating to System.Text.Json](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to) as this may improve performance and remove package dependency. However, keep in mind that it doesn't have a full feature parity. If you use some more complex features, they may not work the same way. Check the [gRPC client documentation](@clients/grpc/README.md) for samples using `System.Text.Json`. ::: For the JSON Event Data, you have to change the TCP client logic from: ```csharp{1,7,9} public static EventStore.ClientAPI.EventData ToJsonEventData( object @event, string eventType, object? metadata = null ) => new EventData( Guid.NewGuid(), eventType, true, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event)), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(metadata ?? new { })) ); ``` to: ```csharp{1,7} public static EventStore.Client.EventData ToJsonEventData( object @event, string eventType, object? metadata = null ) => new EventData( EventStore.Client.Uuid.NewUuid(), eventType, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event)), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(metadata ?? new { })) ); ``` For binary event data, you have to switch from: ```csharp{1,7,9} public static EventStore.ClientAPI.EventData ToJsonEventData( object @event, string eventType, object? metadata = null ) => new EventData( Guid.NewGuid(), eventType, false, SerializeToByteArray(@event), SerializeToByteArray(metadata ?? new { }) ); ``` to: ```csharp{1,7,11} public static EventStore.Client.EventData ToJsonEventData( object @event, string eventType, object? metadata = null ) => new EventData( EventStore.Client.Uuid.NewUuid(), eventType, SerializeToByteArray(@event), SerializeToByteArray(metadata ?? new { }), "application/octet-stream" ); ``` ### Optimistic concurrency Optimistic concurrency handling rules didn't change between the TCP and the gRPC clients. However, the API semantics did. You always had to provide `long` value as the expected stream version in the TCP client. You could use the value that you got from the last event of the stream or general constant values: * `ExpectedVersion.Any` (-2) - Disables the optimistic concurrency check. * `ExpectedVersion.NoStream` (-1) - Specifies the expectation that the target stream does not yet exist. * `ExpectedVersion.StreamExists` (-4) - Specifies the expectation that the target stream or its metadata stream has been created but does not expect the stream to be at a specific event number. In the gRPC client, we aligned typing and naming around the optimistic concurrency checks: * we found that the `ExpectedVersion` word is not precise. It's not precisely stating what we are versioning and may be confused with the, e.g. event schema version. In the gRPC client, we're using the `ExpectedRevision` term instead. * as stream revision cannot be a negative number, the type was changed from `long` to `ulong`, * to reduce the chance of accidentally providing the wrong revision value, we created a dedicated type: `StreamState`. It has `Any`, `NoStream`, `StreamsExists` values. If you had wrapper methods similar to [presented above](#migration-strategies). You'd need to update it to: ```csharp{3-4,12-13,24-25} public Task AppendEvents( string streamName, - long version, + ulong version params object[] events ) { var preparedEvents = events .Select(ToEventData) .ToArray(); - return tcpConnection.AppendToStreamAsync( + return grpcClient.AppendToStreamAsync( streamName, version, preparedEvents ); } public Task AppendEvents( string streamName, params object[] events ) - => AppendEvents(streamName, ExpectedVersion.Any, events); + => AppendEvents(streamName, StreamState.Any, events); ``` ### Transactions The most significant breaking change in the gRPC client is that it **does not support transactions anymore**. The TCP client may perform multiple appends to EventStoreDB in one transaction. The transaction can only append events to one stream. Transactions across multiple streams are not supported. The gRPC client still supports appending more than one event to the single stream as an atomic operation. A transaction can be long-lived, and opening it for a stream doesn't lock it. Another process can write to the same stream. In this case, your transaction might fail if you use idempotent writes with the expected version. If you use transactions, we recommend reevaluating your consistency guarantees and stream modelling to reduce the need for appending events. If you still need to use them, you may consider adding your own Unit of Work implementation, as, for example: ```csharp public class EventStoreDBUnitOfWork { private readonly EventStoreClient grpcClient; private readonly List uncommittedEvents = new(); private readonly string streamName; private readonly StreamRevision expectedStreamRevision; private EventStoreDBUnitOfWork(EventStoreClient grpcClient, string streamName, StreamRevision expectedStreamRevision) { this.grpcClient = grpcClient; this.streamName = streamName; this.expectedStreamRevision = expectedStreamRevision; } public static EventStoreDBUnitOfWork Begin(EventStoreClient grpcClient, string streamName, StreamRevision expectedStreamRevision) => new(grpcClient, streamName, expectedStreamRevision); public Task Commit(CancellationToken cancellationToken = default) => grpcClient.AppendToStreamAsync( streamName, expectedStreamRevision, uncommittedEvents.ToArray(), cancellationToken: cancellationToken ); public void Append(params EventData[] eventData) => uncommittedEvents.AddRange(eventData); } ``` ## Reading Events ### Read direction The TCP Client has dedicated methods for reading events in the specific direction: * `ReadStreamEventsForwardAsync`, * `ReadStreamEventsBackwardAsync`, * `ReadAllEventsForwardAsync`, * `ReadAllEventsBackwardAsync`. In the gRPC client, we unified those methods into methods with the direction parameter: * `ReadStreamAsync`, * `ReadAllAsync`. To read a stream forwards, use: ```csharp await using var readResult = grpcClient.ReadStreamAsync( Direction.Forwards, streamName, StreamPosition.Start ); ``` To read a stream backwards, use: ```csharp await using var readResult = grpcClient.ReadStreamAsync( Direction.Backwards, streamName, StreamPosition.End ); ``` Accordingly, to read the `$all` stream forwards, use: ```csharp await using var readResult = grpcClient.ReadAllAsync( Direction.Forwards, streamName, Position.Start ); ``` and to read the `$all` stream backwards: ```csharp await using var readResult = grpcClient.ReadAllAsync( Direction.Backwards, streamName, Position.End ); ``` ### Read positions Both TCP and gRPC clients allow reading the stream from a specific position (representing the location of the particular event in the stream). We decided to make positions more explicit according to the stream revision changes in [appending new events](#appending-events). We expanded a `StreamPosition` class that centralises stream position handling. It still has a `Start` and an `End` constant representing the logical stream's position. However, they no longer return `long` values but a `StreamPosition` class instance. We replaced `long` values with `ulong` as stream position is always positive. `StreamPosition` class has an overloaded operator for `ulong` value assignment. ```csharp StreamPosition streamPosition = 100L; ``` You can also convert it from the `StreamRevision`: ```csharp var streamPosition = StreamPosition.FromStreamRevision(streamRevision); ``` For reading from `$all`, the TCP Client has already the `Position` class. gRPC client also has it, but using `ulong` instead of `long` for commit and prepare positions. ### Read result The TCP Client requires paging through the results. You must provide the maximum number of events you want to read in the single read call. In the gRPC client, this is optional. By default, it will try to read all events. To make it efficient, read methods return [IAsyncEnumerable](https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8). Which means that it won't load the whole stream at once but iterate sequentially, reducing the memory pressure. Instead of doing paging like this in the TCP client: ```csharp public async Task> LoadEvents(string stream) { const int pageSize = 4096; var readFrom = 0; var events = new List(); StreamEventsSlice page; do { var page = await tcpConnection.ReadStreamEventsForwardAsync( stream, readFrom, pageSize, true ); if (page.Status == SliceReadStatus.StreamNotFound) throw new ArgumentOutOfRangeException( nameof(stream), $"Stream '{stream}' was not found" ); events.AddRange( page.Events.Select(Deserialize) ); readFrom = stream.NextEventNumber; } while (!page.IsEndOfStream); return events; } ``` You can simplify that in the gRPC client into: ```csharp public async Task> LoadEvents(string stream) { await using var readResult = grpcClient.ReadStreamAsync( Direction.Forwards, stream, StreamPosition.Start ); if (await readResult.ReadState != ReadState.Ok) throw new ArgumentOutOfRangeException( nameof(stream), $"Stream '{stream}' was not found" ); return await readResult .Select(Deserialize) .ToListAsync(); } ``` ::: tip Reading events from a stream asynchronously as shown above is still limited in time by the specified deadline. The default deadline is 10 seconds, so for very long streams you'd either need to increase the deadline, or resort to paging. ::: ### Serialisation The gRPC client uses `ReadOnlyMemory` instead of a byte array to make event processing more efficient. To support that, you will need to modify your deserialisation logic slightly: ```csharp{4-5} object Deserialize(this ResolvedEvent resolvedEvent) { var dataType = TypeMapper.GetType(resolvedEvent.Event.EventType); - var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data); + var jsonData = Encoding.UTF8.GetString(resolvedEvent.Event.Data.Span); var data = JsonConvert.DeserializeObject(jsonData, dataType); return data; } ``` ## Built-in retries The gRPC client handles reconnections internally. But contrary to the TCP client, it does not have built-in retries for failed operations. It only does retries for the persistent subscriptions. If your codebase depends on them, you should wrap operations with your custom retry policy (e.g. using [Polly](https://github.com/App-vNext/Polly)). If you were using the following configuration: ```csharp var settings = ConnectionSettings.Create() .FailOnNoServerResponse() .KeepReconnecting() .SetOperationTimeoutTo(TimeSpan.FromSeconds(5)) .LimitRetriesForOperationTo(7); var tcpConnection = EventStoreConnection.Create( connectionString, settings ); ``` You can replace it with a retry policy using Polly: ```csharp public static class RetryScope { private static readonly AsyncPolicyWrap RetryPolicy = Policy .Handle() .RetryAsync(7) .WrapAsync(Policy.TimeoutAsync(5)); public static Task ExecuteAsync( this EventStoreClient grpcClient, Func action, CancellationToken cancellationToken ) => RetryPolicy.ExecuteAsync(ct => action(grpcClient, ct), cancellationToken); public static Task ExecuteAsync( this EventStoreClient grpcClient, Func> action, CancellationToken cancellationToken ) => RetryPolicy.ExecuteAsync(ct => action(grpcClient, ct), cancellationToken); } ``` and use it, e.g. as: ```csharp public static Task> ReadStreamWithRetryAsync( this EventStoreClient grpcClient, string streamName, CancellationToken cancellationToken ) => grpcClient.ExecuteAsync( async (es, ct) => { await using var readResult = es.ReadStreamAsync( Direction.Forwards, streamName, StreamPosition.Start, cancellationToken: ct ); if (await readResult.ReadState != ReadState.Ok) throw new ArgumentOutOfRangeException( nameof(streamName), $"Stream '{streamName}' was not found" ); return await readResult .ToListAsync(ct); }, cancellationToken); ``` ::: warning You should be careful in defining the retry policy. Not all operations are idempotent by default. Reads are idempotent, however, if you're not using [optimistic concurrency](@clients/grpc/appending-events.md#handling-concurrency) or do not provide the same event id for appends, it may result in duplicates. You need to decide which exceptions you'd like to retry, e.g. there is no point in retrying `StreamDeleted` as the stream won't reappear. ::: ## Catch-up Subscriptions We unified catch-up subscriptions API in the gRPC client. In the TCP client, you have multiple methods for subscribing to EventStoreDB, e.g., `SubscribeToStreamAsync`, `SubscribeToStreamFrom`, `FilteredSubscribeToAllAsync`, etc. Now you have two main options: * subscribing to a single stream using `SubscribeToStreamAsync`, * subscribing to the `$all` stream using `SubscribeToAllAsync`. Both methods include overloads for specific configurations and optional parameters with adequate defaults. The recommendation is to use the default settings and modify them when needed. ### Positions As with other operations, we aligned the subscription checkpoint/position handling. Accordingly, for [reading](#reading-events), instead of using `Nullable`, we introduced the following types: * `StreamPosition` for a single stream subscription, * `Position` for the `$all` stream. Each subscription method has an overload taking either a specific position or the default one (from the start). For instance, if you were previously using a unified approach: ```csharp long? checkpoint = GetLastCheckpoint(); tcpConnection.SubscribeToStreamFrom( streamName, checkpoint, true, EventAppeared, SubscriptionDropped ); ``` Now you'd need to call different overloads: ```csharp long? checkpoint = GetLastCheckpoint(); var startFrom = checkpoint == null ? FromStream.Start : FromStream.After(StreamPosition.FromInt64((long)checkpoint)); await grpcClient.SubscribeToStreamAsync( streamName, startFrom, EventAppeared, true, SubscriptionDropped ); ``` Accordingly, you need to perform the same change for subscription to the `$all` stream. ```csharp long? position = GetLastCheckpoint(); tcpConnection.SubscribeToAllFrom( position, false, EventAppeared, SubscriptionDropped ); ``` Now you'd need to call different overloads: ```csharp long? checkpoint = GetLastCheckpoint(); var startFrom = checkpoint == null ? FromAll.Start : FromAll.After(new Position(checkpoint.Value, checkpoint.Value)); await grpcClient.SubscribeToAllAsync( startFrom, EventAppeared, false, SubscriptionDropped ); ``` ### Events filtering EventStoreDB allows you to filter the events whilst you subscribe to the `$all` stream so that you only receive the events that you care about. The TCP client provides that option via the `FilteredSubscribeToAll` method. As was mentioned above, this method was unified with `SubscribeToAllAsync`. Thus, instead of such call in TCP: ```csharp var filter = Filter.ExcludeSystemEvents; var filteredSettings = CatchUpSubscriptionFilteredSettings.Default; tcpConnection.FilteredSubscribeToAllFrom( position, filter, filteredSettings, EventAppeared, LiveProcessingStarted, SubscriptionDropped ); ``` You need to use: ```csharp var filter = new SubscriptionFilterOptions (EventTypeFilter.ExcludeSystemEvents()); await grpcClient.SubscribeToAllAsync( position, EventAppeared, false, SubscriptionDropped, filter ); ``` The `CatchUpSubscriptionFilteredSettings` and `Filter` types from the TCP client were unified into single `SubscriptionFilterOptions` in gRPC. Read more in the [gRPC server-side filtering docs](@clients/grpc/subscriptions.md#server-side-filtering). ### Knowing when live processing started The TCP client provides the possibility to provide a handler that will be called when live processing starts: ```csharp{6} tcpConnection.SubscribeToStreamFrom( streamName, streamPosition, false, EventAppeared, subscription => Console.WriteLine("Processing live"), SubscriptionDropped ); ``` The new client supports reporting when the subscription switches between catching up and live modes using the new subscription APIs that uses `IAsyncAnumerable`. Read more about the new API in the [client documentation](../../../grpc/subscriptions.md). Here's an example of how to react to subscription mode changes: ```csharp await using var subscription = client.SubscribeToStream( streamName, FromStream.Start, cancellationToken: ct); await foreach (var message in subscription.Messages.WithCancellation(ct)) { switch (message) { case StreamMessage.Event(var evnt): await HandleEvent(evnt); break; case StreamMessage.CaughtUp: Console.WriteLine("Processing events live"); break; case StreamMessage.FellBehind: Console.WriteLine("Catching up"); break; } } ``` ### Resolving linked events [Projections in EventStoreDB](@server/features/projections/README.md) let you append new events or link existing events to streams. Links won't contain the original event data. EventStoreDB can resolve it automatically depending on the value you passed to the operation call. The TCP client by default resolved linked events. gRPC changes that behaviour to only resolve them if you ask for that explicitly. To do that, you need to pass `true` to the `resolveLinkTos` param, e.g., for a regular stream: ```csharp{4} await grpcClient.SubscribeToStreamAsync( streamName, EventAppeared, true, SubscriptionDropped ); ``` or for the `$all` stream: ```csharp{3} await grpcClient.SubscribeToAllAsync( EventAppeared, true, SubscriptionDropped ); ``` Read more in the [resolve link-to's gRPC docs](@clients/grpc/subscriptions.md#resolving-link-to-s). [//]: # "## Persistent subscriptions" [//]: # "TODO" --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/projections.md' --- # Projections This page provides an example of using [user-defined projections](@server/features/projections/README.md#types-of-projections) in your application. ## Adding sample data [Download](https://github.com/kurrent-io/documentation/tree/master/docs/clients/tcp/dotnet/21.2/sample-code/Seed) the following files that contain sample data used throughout this step of the getting started guide. * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1164.json](https://raw.githubusercontent.com/EventStore/EventStore/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1164.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1165.json](https://raw.githubusercontent.com/EventStore/EventStore/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1165.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1166.json](https://raw.githubusercontent.com/EventStore/EventStore/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1166.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1167.json](https://raw.githubusercontent.com/EventStore/EventStore/53f84e55ea56ccfb981aff0e432581d72c23fbf6/samples/http-api/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1167.json) Add the sample data to four different streams: First, we need a function to read JSON files and construct the list of `EventData` instances: @[code{ReadEventsFunction}](./sample-code/GettingStarted/UserProjections.cs) Then, we can use this function and push events to EventStoreDB: @[code{SeedEvents}](./sample-code/GettingStarted/UserProjections.cs) ## Creating your first projection ::: tip Next steps Read [this guide](@server/features/projections/custom.md) to find out more about the user defined projection's API. ::: The projection counts the number of 'XBox One S's that customers added to their shopping carts. A projection starts with a selector, in this case `fromAll()`. Another possibility is `fromCategory({category})` which this step discusses later, but for now, `fromAll` should do. The second part of a projection is a set of filters. There is a special filter called `$init` that sets up an initial state. You want to start a counter from 0 and each time EventStoreDB observes an `ItemAdded` event for an 'Xbox One S,' increment the counter. Here is the projection code: @[code](@httpapi/xbox-one-s-counter.js) You create a projection by calling the projection API and providing it with the definition of the projection. Here you decide how to run the projection, declaring that you want the projection to start from the beginning and keep running. You can send the projection code as text along the other parameters, using the `ProjectionsManager` instance: @[code{ProjectionsManager}](./sample-code/GettingStarted/UserProjections.cs) @[code{CreateUserProjection}](./sample-code/GettingStarted/UserProjections.cs) ::: tip Next steps [Read here](#managing-projections) for more information on creating projections with the .NET API and the parameters available, or [our projections section](@server/features/projections/custom.md) for details on projection syntax. ::: ## Querying projection state Now the projection is running, you can query the state of the projection. As this projection has a single state, query it with the following request: @[code{GetProjectionState}](./sample-code/GettingStarted/UserProjections.cs) #### Querying projection state by partition You can partition the projection state to only include some events for aggregating the state rather than processing all the events. Querying with partitions because you have to specify the partition and the name of the stream. @[code{GetPartitionedProjectionState}](./sample-code/GettingStarted/UserProjections.cs) The server then returns the state for the partition: @[code](@httpapi/projections/read-state-partition.json) ## Emitting new events The above gives you the correct result but requires you to poll for the state of a projection. What if you wanted EventStoreDB to notify you about state updates via subscriptions? ### Output state Update the projection to output the state to a stream by calling the `outputState()` method on the projection which by default produces a `$projections-{projection-name}-result` stream. Below is the updated projection: @[code](@httpapi/xbox-one-s-counter-outputState.js) To update the projection, edit the projection definition with the following request: @[code{ProjectionsManager}](./sample-code/GettingStarted/UserProjections.cs) @[code{UpdateUserProjection}](./sample-code/GettingStarted/UserProjections.cs) Then reset the projection you created above: @[code{ResetUserProjection}](./sample-code/GettingStarted/UserProjections.cs) You should get a response similar to the one below: You can now read the events in the result stream by issuing a read request. @[code{QueryUpdatedProjection}](./sample-code/GettingStarted/UserProjections.cs) ## Configuring projections You can configure properties of the projection by updating values of the `options` object. For example, the following projection changes the name of the results stream: @[code{2}](@httpapi/projections/update-projection-options.js) Then send the update to the projection: @[code{ProjectionsManager}](./sample-code/GettingStarted/UserProjections.cs) @[code{UpdateProjectionProperties}](./sample-code/GettingStarted/UserProjections.cs) ::: tip You can find all the options available in the [user defined projections guide](@server/features/projections/custom.md). ::: Now you can read the result as above, but use the new stream name: @[code{ReadUpdatedProjectionStream}](./sample-code/GettingStarted/UserProjections.cs) #### Example: number of items per shopping cart The example in this step so far relied on a global state for the projection, but what if you wanted a count of the number of items in the shopping cart per shopping cart. EventStoreDB has a built-in `$by_category` projection that lets you select events from a particular list of streams. Enable this projection with the following command. @[code{ProjectionsManager}](./sample-code/GettingStarted/UserProjections.cs) @[code{EnableCategoryProjection}](./sample-code/GettingStarted/UserProjections.cs) The projection links events from existing streams to new streams by splitting the stream name by a separator. You can configure the projection to specify the position of where to split the stream `id` and provide a separator. By default, the category splits the stream `id` by a dash. The category is the first string. | Stream Name | Category | |--------------------|----------------------------------------| | shoppingCart-54 | shoppingCart | | shoppingCart-v1-54 | shoppingCart | | shoppingCart | *No category as there is no separator* | You want to define a projection that produces a count per stream for a category, but the state needs to be per stream. To do so, use `$by_category` and its `fromCategory` API method. Below is the projection: @[code](@httpapi/projections/shopping-cart-counter.js) Create the projection with the following request: @[code{CreatePartitionedProjection}](./sample-code/GettingStarted/UserProjections.cs) ## Managing projections The EventStoreDB Client API includes helper methods that use the HTTP API to allow you to manage projections. This document describes the methods found in the `ProjectionsManager` class. All methods in this class are asynchronous. ### Enable a projection Enables an existing projection by name. You must have access to a projection to enable it. ```csharp Task EnableAsync(string name, UserCredentials userCredentials = null); ``` ### Disable a projection Disables an existing projection by name. You must have access to a projection to disable it. ```csharp Task DisableAsync(string name, UserCredentials userCredentials = null); ``` ### Abort a projection Aborts an existing projection by name. You must have access to a projection to abort it. ```csharp Task AbortAsync(string name, UserCredentials userCredentials = null); ``` ### Create a one-time projection Creates a projection that runs until the end of the log and then stops. The query parameter contains the JavaScript you want created as a one time projection. ```csharp Task CreateOneTimeAsync(string query, UserCredentials userCredentials = null); ``` ### Create a continuous projection Creates a projection that runs until the end of the log and then continues running. The query parameter contains the JavaScript you want created as a one time projection. Continuous projections have explicit names and you can enable or disable them via this name. ```csharp Task CreateContinuousAsync( string name, string query, UserCredentials userCredentials = null ); ``` ### List all projections Returns a list of all projections. ```csharp Task> ListAllAsync(UserCredentials userCredentials = null); ``` ### List one-time projections Returns a list of all One-Time Projections. ```csharp Task> ListOneTimeAsync(UserCredentials userCredentials = null); ``` ### Get statistics on a projection Returns the statistics associated with a named projection. ```csharp Task GetStatisticsAsync(string name, UserCredentials userCredentials = null); ``` ### Delete projection Deletes a named projection. You must have access to a projection to delete it. ```csharp Task DeleteAsync(string name, UserCredentials userCredentials = null); ``` ### Get state Retrieves the state of a projection. ```csharp Task GetState(string name, UserCredentials userCredentials = null); ``` ### Get partition state Retrieves the state of the projection via the given partition. ```csharp Task GetPartitionStateAsync( string name, string partition, UserCredentials userCredentials = null ); ``` ### Get result Retrieves the result of the projection. ```csharp Task GetResult(string name, UserCredentials userCredentials = null); ``` ### Get partition result Retrieves the result of the projection via the given partition. ```csharp Task GetPartitionResultAsync( string name, string partition, UserCredentials userCredentials = null ); ``` --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/quick-tour.md' --- # Quick tour This is a quick tour into the basic operations with EventStoreDB using the TCP client. We will look at creating a connection, appending an event and reading an event. ::: warning The TCP client is considered legacy. We recommend migrating to [the latest client](@clients/grpc/README.md). Check the [migration guide to learn more](migration-to-gRPC.md). ::: ## Requirements These examples have the following requirements: * At least [.NET Core SDK 3.1](https://dotnet.microsoft.com/download) * [Docker](https://www.docker.com/get-started) * A reference to the [EventStore.Client](https://www.nuget.org/packages/EventStore.Client/) NuGet package ## Run the server To run the EventStoreDB, create a new file called `docker-compose.yml` and copy the following contents into it: @[code](./sample-code/docker-compose.yml) Then run the command. ``` docker-compose up ``` This will launch a new instance of the EventStoreDB server. ## Connect to EventStoreDB [Install the .NET client API](https://www.nuget.org/packages/EventStore.Client) package to your project using your preferred method. And require it in your code: @[code{using}](./sample-code/Program.cs) To use a client API, you use port `1113` and create a connection: @[code{connect}](./sample-code/GettingStarted/Connection.cs) It will create a connection to EventStoreDB running locally in Docker container using the TCP protocol. ## Appending events The most basic operation is to append a single event to the database: @[code{appendEvent}](./sample-code/GettingStarted/ConnectEventStore.cs) ## Reading events After you wrote an event to the database, you can then read it back. Use the following method passing the stream name, the start point in the stream, the number of events to read and whether to follow links to the event data: @[code{ReadEvents}](./sample-code/Program.cs) --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/reading.md' --- # Reading events You can use the client API to read events from a stream starting from either end of the stream. There is a method for each direction and one for reading a single event. ## Read a single event To read one event, use the following method passing the stream name, the event you want to read and whether to return the event data: ```csharp Task ReadEventAsync(string stream, long eventNumber, bool resolveLinkTos); ``` Example: @[code{ReadOneEvent}](./sample-code/Program.cs) The `ReadEventAsync` method reads a single event from a stream at a specified position. This is the simplest case of reading events, but is still useful for situations such as reading the last event in the stream used as a starting point for a subscription. This function accepts three parameters: | Parameter | Description | |:----------|:------------| | `string stream` | The stream to read from | | `long eventNumber` | The event number to read (use `StreamPosition.End` to read the last event in the stream) | | `bool resolveLinkTos` | Determines whether any link events encountered in the stream will be resolved. See the discussion on [Resolved Events](#resolvedevent) for more information on this | This method returns an instance of `EventReadResult` which indicates if the read was successful, and if so the `ResolvedEvent` that was read. ## Read a specific stream forwards The `ReadStreamEventsForwardAsync` method reads the requested number of events in the order in which they were originally appended to the stream from a nominated starting point in the stream. ```csharp Task ReadStreamEventsForwardAsync( string stream, long start, int count, bool resolveLinkTos ); ``` The parameters are: | Parameter | Description | |:----------|:------------| | `string Stream` | The name of the stream to read | | `long start` | The earliest event to read (inclusive). For the special case of the start of the stream, you should use the constant `StreamPosition.Start`. | | `int count` | The maximum number of events to read in this request (assuming that many exist between the start specified and the end of the stream) | | `bool resolveLinkTos` | Determines whether any link events encountered in the stream will be resolved. See the discussion on [Resolved Events](#resolvedevent) for more information on this | Bear in mind that there could be many events in the stream. The .NET client always reads events in pages with the default page size of 4096 events. If you need to read the whole stream, you'd have to keep reading pages until the response will indicate that you reached the end of the stream. The example below uses the `ReadStreamEventsForwardAsync` method in a loop to page through all events in a stream, reading 200 events at a time to build a list of all the events in the stream. ```csharp var streamEvents = new List(); StreamEventsSlice currentSlice; var nextSliceStart = StreamPosition.Start; do { currentSlice = await _connection.ReadStreamEventsForwardAsync( "myStream", nextSliceStart, 200, false ); nextSliceStart = currentSlice.NextEventNumber; streamEvents.AddRange(currentSlice.Events); } while (!currentSlice.IsEndOfStream); ``` ::: tip It's unlikely that client code would need to build a list in this manner. It's far more likely that you would pass events into a left fold to derive the state of some object as of a given event. ::: ```csharp // Read a specific stream backwards Task ReadStreamEventsBackwardAsync( string stream, long start, int count, bool resolveLinkTos ); // Read all events forwards Task ReadAllEventsForwardAsync( Position position, int maxCount, bool resolveLinkTos ); // Read all events backwards Task ReadAllEventsBackwardAsync( Position position, int maxCount, bool resolveLinkTos ); ``` ::: tip These methods also have an optional parameter which allows you to specify the `UserCredentials` to use for the request. If you don't supply any, the default credentials for the `EventStoreConnection` are used ([see Connecting to a server - user credentials](connecting.md#user-credentials)). ::: ## Read a stream backwards The `ReadStreamEventsBackwardAsync` method reads the requested number of events in the reverse order from that in which they were originally appended to the stream from a specified starting point. The parameters are: | Parameter | Description | |:----------|:------------| | `string Stream` | The name of the stream to read | | `long start` | The latest event to read (inclusive). For the end of the stream use the constant `StreamPosition.End` | | `int count` | The maximum number of events to read in this request (assuming that many exist between the start specified and the start of the stream) | | `bool resolveLinkTos` | Determines whether any link events encountered in the stream will be resolved. See the discussion on [Resolved Events](#resolvedevent) for more information on this | ## Read all events EventStoreDB allows you to read events across all streams using the `ReadAllEventsForwardAsync` and `ReadAllEventsBackwardsAsync` methods. These work in the same way as the regular read methods, but use an instance of the global log file `Position` to reference events rather than the simple integer stream position described previously. They also return an `AllEventsSlice` rather than a `StreamEventsSlice` which is the same except it uses global `Position`s rather than stream positions. #### Example: Read all events forward from start to end ```csharp var allEvents = new List(); AllEventsSlice currentSlice; var nextSliceStart = Position.Start; do { currentSlice = await connection.ReadAllEventsForwardAsync( nextSliceStart, 200, false ); nextSliceStart = currentSlice.NextPosition; allEvents.AddRange(currentSlice.Events); } while (!currentSlice.IsEndOfStream); ``` ## StreamEventsSlice The reading methods for individual streams each return a `StreamEventsSlice`, which is immutable. The available members on `StreamEventsSlice` are: | Member | Description | |:-------|:------------| | `string Stream` | The name of the stream for the slice | | `ReadDirection ReadDirection` | Either `ReadDirection.Forward` or `ReadDirection.Backward` depending on which method was used to read | | `long FromEventNumber` | The sequence number of the first event in the stream | | `long LastEventNumber` | The sequence number of the last event in the stream | | `long NextEventNumber` | The sequence number from which the next read should be performed to continue reading the stream | | `bool IsEndOfStream` | Whether this slice contained the end of the stream at the time it was created | | `ResolvedEvent[] Events` | An array of the events read. See the description of how to interpret a [Resolved Events](#resolvedevent) below for more information on this | ## ResolvedEvent When you read events from a stream (or received over a subscription) you receive an instance of the `RecordedEvent` class packaged inside a `ResolvedEvent`. EventStoreDB supports a special event type called 'Link Events'. Think of these events as pointers to an event in another stream. In situations where the event you read is a link event, `ResolvedEvent` allows you to access both the link event itself, as well as the event it points to. The members of this class are as follows: | Member | Description | |:-------|:------------| | `RecordedEvent Event` | The event, or the resolved link event if this `ResolvedEvent` is a link event | | `RecordedEvent Link` | The link event if this `ResolvedEvent` is a link event | | `RecordedEvent OriginalEvent` | Returns the event read or which triggered the subscription. If this `ResolvedEvent` represents a link event, the link will be the `OriginalEvent`, otherwise it will be the event | | `bool IsResolved` | Indicates whether this `ResolvedEvent` is a resolved link event | | `Position? OriginalPosition` | The logical position of the `OriginalEvent` | | `string OriginalStreamId` | The stream name of the `OriginalEvent` | | `long OriginalEventNumber` | The event number in the stream of the `OriginalEvent` | ::: tip To ensure that the EventStoreDB server follows link events when reading, ensure you set the `ResolveLinkTos` parameter to `true` when calling read methods. ::: ## RecordedEvent `RecordedEvent` contains all the data about a specific event. Instances of this class are immutable, and expose the following members: | Member | Description | |:-------|:-------------| | `string EventStreamId` | The Event Stream this event belongs to | | `Guid EventId` | The Unique Identifier representing this | | `long EventNumber` | The number of this event in the stream | | `string EventType` | The event type (supplied when appending) | | `byte[] Data` | A byte array representing the data of this event | | `byte[] Metadata` | A byte array representing the metadata associated with this event | | `bool IsJson` | Indicates whether the content was internally marked as JSON | | `DateTime Created` | A timestamp representing when this event was created. | | `long CreatedEpoch` | A long representing the milliseconds since the epoch when the was created. | --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/security.md' --- # Security EventStoreDB supports basic authentication for HTTP API and TCP calls, and supports access control lists (ACL) for streams. ## Authentication and authorization EventStoreDB supports basic HTTP authentication to internal users. You create and manage these users with the RESTful API or the Admin UI. Once you have added users, you can use their details with HTTP requests or native client's authorization process. Alternatively, you can also use the 'trusted intermediary' header for externalized authentication that allows you to integrate almost any authentication system with EventStoreDB. Read more about [the trusted intermediary header](@server/security/user-authentication.md#trusted-intermediary). If you were to use the wrong user or no user when connecting to EventStoreDB, you receive a `401 Unauthorized` response for HTTP API or Exception for the native client. ::: tip Remember to change the default password for default users and disable unused users after the cluster setup is complete. ::: ## Secure EventStoreDB node We recommend you connect to EventStoreDB over SSL to encrypt the user information. [Read this guide for instructions](@server/security/protocol-security.md). ::: warning Avoid exposing EventStoreDB to the global internet network. ::: ## User management The EventStoreDB .NET client includes helper methods that use the HTTP API to allow for the management of users. This section describes the methods found in the `UsersManager` class. All methods in this class are asynchronous. ### Default users By default, EventStoreDB has two users `admin` and `ops` with the password `changeit`. ::: tip We recommend you create separate functional account with minimal access rights for any connected application or service. ::: ### Default groups By default, EventStoreDB has two user groups `$admins` `$ops`. However, it is possible to create custom groups with the .NET client. ### Create UsersManager instance @[code{UserManager}](./sample-code/DotNetClient/UsersCreateUsersManager.cs) Resolving the host name may be especially useful if the EventStoreDB Admin UI is not available under loopback address e.g., when container orchestrator assign dynamic DNS based on service name. ### Create a user Creates a user, the credentials for this operation must be a member of the `$admins` group. ```csharp Task CreateUserAsync( string login, string fullName, string[] groups, string password, UserCredentials userCredentials = null ); ``` ### Disable a user Disables a user, the credentials for this operation must be a member of the `$admins` group. ```csharp Task DisableAsync(string login, UserCredentials userCredentials = null); ``` ### Enable a User Enables a user, the credentials for this operation must be a member of the `$admins` group. ```csharp Task EnableAsync(string login, UserCredentials userCredentials = null); ``` ### Delete a user Deletes (non-recoverable) a user, the credentials for this operation must be a member of the `$admins` group. If you prefer this action to be recoverable, disable the user as opposed to deleting the user. ```csharp Task DeleteUserAsync(string login, UserCredentials userCredentials = null); ``` ### List all users Use this method to get a list of all users in the cluster. ```csharp Task> ListAllAsync(UserCredentials userCredentials = null); ``` ### Get user details Return the details of the user supplied in user credentials (e.g. the user making the request). ```csharp Task GetCurrentUserAsync(UserCredentials userCredentials); ``` ### Get details of logged-in user ```csharp Task GetUserAsync(string login, UserCredentials userCredentials); ``` ### Update user details and groups Although `UsersManager` does not have separate methods for idempotent adding/removing user groups it can be extended: @[code{Extensions}](./sample-code/DotNetClient/UsersManagerExtensions.cs) ```csharp Task UpdateUserAsync( string login, string fullName, string[] groups, UserCredentials userCredentials = null ); ``` ### Reset user password Resets the password of a user. The credentials for this operation must be part of the `$admins` group. ```csharp Task ResetPasswordAsync( string login, string newPassword, UserCredentials userCredentials = null ); ``` ## Access control lists Alongside authentication, EventStoreDB supports per stream configuration of Access Control Lists (ACL). To configure the ACL of a stream go to its head and look for the `metadata` relationship link to fetch the metadata for the stream. For more information on the structure of Access Control Lists read [Access Control Lists](@server/security/user-authorization.md#access-control-lists). ### ACL example The ACL below gives `writer` read and write permission on the stream, while `reader` has read permission on the stream. Only users in the `$admins` group can delete the stream or read and write the metadata. ```csharp var metadata = StreamMetadata.Build() .SetCustomPropertyWithValueAsRawJsonString( "customRawJson", @"{ ""$acl"": { ""$w"": ""writer"", ""$r"": [ ""reader"", ""also-reader"" ], ""$d"": ""$admins"", ""$mw"": ""$admins"", ""$mr"": ""$admins"" } }" ); await connection.SetStreamMetadataAsync( streamName, ExpectedVersion.Any, metadata, adminCredentials ); ``` --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/streams.md' --- # Stream metadata Every stream in EventStoreDB has metadata stream associated with it, prefixed by `$$`, so the metadata stream from a stream called `foo` is `$$foo`. Internally, the metadata includes information such as the ACL of the stream and the maximum count and age for the events in the stream. Client code can also put information into stream metadata for use with projections or through the client API. This information is not part of the actual event but is metadata associated with the event. EventStoreDB stores stream metadata as JSON, and you can access it over the HTTP APIs. ## Read stream metadata To read stream metadata over the .NET API you can use methods found on the `EventStoreConnection`. You can use the `GetStreamMetadata` methods in two ways. The first is to return a fluent interface over the stream metadata, and the second is to return you the raw JSON of the stream metadata. ```csharp Task GetStreamMetadataAsync( string stream, UserCredentials userCredentials = null ); ``` This returns a `StreamMetadataResult`. The fields on this result are: | Member | Description | |:--------------------------|:---------------------------------------------------------| | `string Stream` | The name of the stream | | `bool IsStreamDeleted` | `true` is the stream is deleted, `false` otherwise. | | `long MetastreamVersion` | The version of the metastream format | | `StreamMetadata Metadata` | A `StreamMetadata` object representing the metadata JSON | You can then access the `StreamMetadata` via the `StreamMetadata` object. It contains typed fields for well known stream metadata entries. | Member | Description | |:-------|:------------| | `long? MaxAge` | The maximum age of events in the stream. Items older than this will be automatically removed. | | `long? MaxCount` | The maximum count of events in the stream. When you have more than count the oldest will be removed. | | `long? TruncateBefore` | When set says that items prior to event 'E' can be truncated and will be removed. | | `TimeSpan? CacheControl` | The head of a feed in the atom api is not cacheable. This allows you to specify a period of time you want it to be cacheable. Low numbers are best here (say 30-60 seconds) and introducing values here will introduce latency over the atom protocol if caching is occurring. | | `StreamAcl Acl` | The access control list for this stream. | If instead you want to work with raw JSON you can use the raw methods for stream metadata. ```csharp Task GetStreamMetadataAsRawBytesAsync( string stream, UserCredentials userCredentials = null ); ``` This returns a `RawStreamMetadataResult`. The fields on this result are: | Member | Description | |:-------------------------|:---------------------------------------------------------------------------------------------| | `string Stream` | The name of the stream | | `bool IsStreamDeleted` | True is the stream is deleted, false otherwise. | | `long MetastreamVersion` | The version of the meta-stream (see [Expected Version](appending.md#optimistic-concurrency)) | | `byte[] Metadata` | The raw data of the metadata JSON | ::: tip If you enabled [security](connecting.md#security), reading metadata may require that you pass credentials. By default, it is only allowed for admins though you can change this via default ACLs. If you do not pass credentials, and they are required you will receive an `AccessedDeniedException`. ::: ## Writing metadata You can write metadata in both a typed and a raw mechanism. When writing it is generally easier to use the typed mechanism. Both writing mechanisms support an `expectedVersion` which works the same as on any stream and you can use to control concurrency, read [Expected Version](appending.md#optimistic-concurrency) for further details. ```csharp Task SetStreamMetadataAsync( string stream, long expectedMetastreamVersion, StreamMetadata metadata, UserCredentials userCredentials = null ); ``` The `StreamMetadata` passed above has a builder that you can access via the `StreamMetadata.Create()` method. The options available on the builder are: | Method | Description | |:-------|:------------| | `SetMaxCount(long count)` | Sets the maximum count of events in the stream. | | `SetMaxAge(TimeSpan age)` | Sets the maximum age of events in the stream. | | `SetTruncateBefore(long seq)` | Sets the event number from which previous events can be scavenged. | | `SetCacheControl(TimeSpan cacheControl)` | The amount of time the stream head is cacheables. | | `SetReadRoles(string[] roles)` | Sets the roles allowed to read the underlying stream. | | `SetWriteRoles(string[] roles)` | Sets the roles allowed to write to the underlying stream. | | `SetDeleteRoles(string[] roles)` | Sets the roles allowed to delete the underlying stream. | | `SetMetadataReadRoles(string[] roles)` | Sets the roles allowed to read the metadata stream. | | `SetMetadataWriteRoles(string[] roles)` | Sets the roles allowed to write the metadata stream. Be careful with this privilege as it gives all the privileges for a stream as that use can assign themselves any other privilege. | | `SetCustomMetadata(string key, string value)` | The SetCustomMetadata method and overloads allow the setting of arbitrary custom fields into the stream metadata. | You can add user-specified metadata via the `SetCustomMetadata` overloads. Some examples of good uses of user-specified metadata are: * which adapter is responsible for populating a stream. * which projection caused a stream to be created. * a correlation ID of some business process. ```csharp Task SetStreamMetadataAsync( string stream, long expectedMetastreamVersion, byte[] metadata, UserCredentials userCredentials = null ); ``` This method will put the data that is in metadata as the stream metadata. Metadata in this case can be anything in a vector of bytes. The server only understands JSON. Read [Access Control Lists](security.md#access-control-lists) for more information on the format in JSON for access control lists. ::: tip Writing metadata may require that you pass credentials if you have security enabled by default it is only allowed for admins though you can change this via default ACLs. If you do not pass credentials, and they are required you will receive an `AccessedDeniedException`. ::: ## Deleting a stream Although you cannot delete individual events from a stream, you can delete the whole stream. It's also possible to delete a head portion of the stream by updating [stream metadata](#stream-metadata). ::: note As EventStoreDB normally works in append-only manner, deleting streams or updating streams metadata would not physically delete anything from the database. Events will be purged from the database when the next scavenge operation runs. You should, therefore, ensure that your database is regularly scavenged. ::: ### Soft delete By default, when you delete a stream, EventStoreDB soft-deletes it. You can recreate the stream by appending new events to it. If you try to read a soft deleted stream you receive an error response. Technically, stream deletion is done by setting `$tb` stream metadata property to `long.MaxValue`. Note that if a deleted stream gets new events appended to it, event numbers for newly appended events don't start from zero as it would happen for a new stream. Although the stream has been deleted, EventStoreDB keeps the last known event number for all streams. ```csharp Task DeleteStreamAsync( string stream, long expectedVersion, UserCredentials userCredentials = null ); ``` ### Hard delete If you want to prevent new events to be appended to a deleted stream, you should use the hard-delete function. When a stream is hard-deleted, EventStoreDB will append a tombstone event to that stream. The tombstone event never gets scavenged, so the stream will forever be closed for appending new events. ::: warning A hard delete is permanent and the stream is not removed during scavenge. If you hard delete a stream, you cannot recreate the stream. ::: ```csharp Task DeleteStreamAsync( string stream, long expectedVersion, bool hardDelete, UserCredentials userCredentials = null ); ``` ### Deleted events and subscriptions As events don't get immediately removed from the database when a stream gets deleted or truncated, subscriptions might still receive deleted events. There are several possible scenarios for deleted events and catch-up subscriptions: #### Subscription to `$all` A catch-up subscription to `$all` gets all the events from all the streams in the database. The normal scenario for such a subscription to be always in live mode, so deletions don't affect it as they only affect already processed events. However, when you set a catch-up subscription to read from a position in the past (or from the beginning), it will also receive deleted events, which haven't been scavenged. It is especially relevant for small databases, as the active chunk never gets scavenged, so all the deleted events can remain in the database for a long time. You can work around this issue by reading the stream metadata form received events when the subscription is in catch-up mode, so you can check if the stream has been deleted or not. #### Subscription to stream with links Default projections like `by-category` emit links to special streams, for example `$ce-Order`. Custom projections written in JavaScript can also emit links. Links are very small events, which point to linked events. Normally, when subscribing to a stream with links, you set the `ResolveLinkTos` subscription option to true as you want to get linked events. EventStoreDB will check if the original event has been deleted, but it will still deliver a link to the subscription. The `Event` property in this case will be `null`, so you can skip those events, but can still update the subscription checkpoint. #### Subscription to a normal stream Subscriptions to regular streams are unaffected by deleted events as it will never get them. However, if you hard-delete the stream for which you also have a subscription, the subscription will fail. --- --- url: 'https://docs.kurrent.io/clients/tcp/dotnet/21.2/subscriptions.md' --- # Subscribe to changes A common operation is to subscribe to a stream and receive notifications for changes. As new events arrive, you continue following them. You can only subscribe to one stream. You can use server-side projections for linking events to new aggregated streams. System projections create pre-defined streams that aggregate events by type or by category and are available out-of-the box. Check the server documentation to learn more about system and user-defined projections. There are three types of subscription patterns, useful in different situations. ## Volatile subscriptions This subscription calls a given function for events appended after establishing the subscription. They are useful when you need notification of new events with minimal latency, but where it's not necessary to process every event. For example, if a stream has 100 events in it when a subscriber connects, the subscriber can expect to see event number 101 onwards until the time the subscription is closed or dropped. You can set up a volatile subscription the same way as a catch-up subscription (below) without specifying the start position. ## Catch-up Subscriptions Catch-up subscriptions serve the purpose of receiving events from a stream for a single subscriber. Subscribers for catch-up subscriptions get events in order and, therefore, are able to process events sequentially. There is nothing on the server that gets stored for such a subscriber. You can have multiple subscribers for the same stream and all those subscribers will get all the events from that stream. Subscriptions have no influence on each other and can run at their own pace. When creating a catch-up subscription on the client side, you can supply the starting position in the stream you are subscribing for. The subscriber will then get events from that position onwards. If the subscriber keeps the last known position in its own storage, it will be able to go down and resubscribe from the stored position in order to only get unprocessed events. When the subscription starts for the first time and the stream it subscribes to already has events, the subscription will get into a catch-up state and receive historical events. When the subscriber eventually catches up and processes all the historical events, it will switch to real-time mode and will get events as they are appended to the stream. If the stream gets more events that the subscriber can process in real time, the subscriber will lag behind and switch to the catch-up mode again until it manages to process all the pending events and then switches to real-time mode again. It is, therefore, a sole responsibility of the subscriber to keep the last processed event position, also known as the *checkpoint* in its own storage. If the subscriber doesn't know the last checkpoint, it will have to subscribe to the beginning of the stream. It is also possible to tell the subscriber to start processing events from the end of the stream, so all the historical events will be ignored. It is useful when you don't care about the history and want to start processing events from now on only. For regular streams, the checkpoint is a sequence number of the event, which is currently being processed by the subscription. For the `$all` stream, the checkpoint consists of two positions in the global event storage - prepare position and commit position. ### Use-case for catch-up subscription Catch-up subscriptions are typically used for producing *read models* in event-sourced systems that use the CQRS pattern. Subscribers that update read models are often called *projections* because they project the event payload to a piece of state in another database. Client-side projections use the same concept as EventStoreDB server-side projections but have a different purpose. ::: tip Storing checkpoints The best practice for subscriptions that project events to another storage, is to store checkpoints in the same storage. Projecting an event and storing the checkpoint in one transaction allows you to achieve the *exactly once* event processing. ::: ### Subscribing to a stream You can subscribe to any individual event stream in EventStoreDB. It could be a normal stream, where your software append events, or a stream produced by the server-side projection, either a system projection (like `$et-SomethingHappened`) or a custom projection. Use the `IEventStoreConnection.SubscribeToStreamFrom` method to initiate the subscription. The connection must be open by the time you call this method. You need to specify the stream which you want to subscribe to, the last known checkpoint, subscription settings, and the event handling function. Optionally, you can specify a function which gets called when the subscription switches from processing historical events to real-time processing, and another function for handling subscription drops. ::: tip Dropping subscription There are multiple reasons for a subscription to drop. The connection might close due to network issues, the subscription might get overloaded with events, or your event handler might throw an unhandled exception. It is usually a good idea to handle subscription drops and resubscribe when needed, to overcome transient issues. When a subscription drops, the application would keep working but will not process any events. ::: @[code{SubscribeToStream}](./sample-code/Subscriptions/CatchUp.cs) In this code, we create an instance of `CatchUpSubscriptionSettings`. You can also use `CatchUpSubscriptionSettings.Default` with default settings instead. ### Subscribing to `$all` Subscribing to the global event stream enables you to create read models from many different event streams. It is a powerful method to create, for example, reporting models with aggregated and denormalized data without using common database operations like `JOIN`. You must, however, carefully evaluate your subscription performance, as when you subscribe to `$all`, you'll get absolutely everything what gets appended to the EventStoreDB cluster. You might also need to filter out system events by checking if the event type starts with `$`. In normal applications, you won't need to process system events. As mentioned before, the checkpoint for `$all` is not a single numeric value, like it is for a single stream. You need to handle the checkpoint with two positions instead: commit and prepare position. For the rest, the code for subscribing to `$all` is very similar to the previous snippet: @[code{SubscribeToAll}](./sample-code/Subscriptions/CatchUp.cs) The differences here are: * You don't need to specify the stream name, as we know it's the `$all` stream. * The checkpoint argument type is `Position?`, not `long?` ### Unsubscribing Normally, you won't need to explicitly close the subscription as you want it to run as long as your application runs. When the application stops, it is a good practice to stop the connection (`IEventStoreConnection.Close`) and when the connection closes, it also stops all the subscriptions gracefully. If you need to stop the subscription without closing the connection, you can use the returned value of `ConnectToStreamFrom` or `ConnectToAllFrom`. Those methods return an instance of `EventStoreCatchUpSubscription` and `EventStoreAllCatchUpSubscription` respectively. You can use it also for something like processing gap metric, as it gives you access to the current checkpoint. When you need to stop the subscription, you can call its `Stop` method. ## Persistent subscriptions In contrast to volatile and catch-up types, persistent subscriptions are not dropped when the connection is closed. Moreover, this subscription type supports the "[competing consumers](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html)" messaging pattern and is useful when you need to distribute messages to many workers. EventStoreDB saves the subscription state server-side and allows for at-least-once delivery guarantees across multiple consumers on the same stream. It is possible to have many groups of consumers compete on the same stream, with each group getting an at-least-once guarantee. ::: tip The Administration UI includes a *Persistent Subscriptions* section where a user can create, update, delete and view subscriptions and their statuses. ::: ### Concept Persistent subscriptions serve the same purpose as catch-up or volatile subscriptions, but in a different way. All subscriptions aim to deliver events in real-time to connected subscribers. But, unlike other subscription types, persistent subscriptions are maintained by the server. In a way, catch-up and persistent subscriptions are similar. Both have a last known position from where the subscription starts getting events. However, catch-up subscriptions must take care about keeping the last known position on the subscriber side while persistent subscriptions keep the position on the server. Since it is the server who decides from where the subscription should start receiving events and knows where events are delivered, subscribers that use a persistent subscription can be load-balanced and process events in parallel. In contrast, catch-up subscriptions, which are client-driven, always receive and process events sequentially and can only be load-balanced on the client side. Therefore, persistent subscriptions allow using the competing consumers pattern that is common in the world of message brokers. In order for the server to load-balance subscribers, it uses the concept of consumer groups. All clients that belong to a single consumer group will get a portion of events and that's how load balancing works inside a group. It is possible to create multiple consumer groups for the same stream, and they will be completely independent of each other, receiving and processing events at their own pace and having their own last known position handled by the server. ![Consumer groups](./images/consumer-groups.jpg) ::: note Just as in the world of message brokers, processing events in a group of consumers running in parallel processes will most likely get events out of order within a certain window. For example, if a consumer group has ten consumers, ten messages will be distributed among the available consumers, based on the [strategy](#consumer-strategies) of the group. Even though some strategies make an attempt to consistently deliver ordered events to a single consumer, it's done on a best effort basis and there is no guarantee of events coming in order with any strategy. ::: ### Creating a subscription group The first step of dealing with a subscription group is to create one. You will receive an error if you attempt to create a subscription group multiple times. You must have admin permissions to create a persistent subscription group. ::: tip Normally you wouldn't create the subscription group in your general executable code. Maintaining subscription groups can be seen as a *migration* task, similar to RDBMS schema migrations and therefore needs to run only after it gets changed for some reason. ::: ```csharp var userCredentials = new UserCredentials("admin", "changeit"); var settings = PersistentSubscriptionSettings .Create() .StartFromCurrent(); var result = await connection.CreatePersistentSubscriptionAsync( "myStream", "agroup", settings, userCredentials ); ``` | Parameter | Description | |:------------------------------------------|:----------------------------------------------------------------| | `string stream` | The name of the stream which the persistent subscription is on. | | `string groupName` | The name of the subscription group to create. | | `PersistentSubscriptionSettings settings` | The settings to use when creating this subscription. | | `UserCredentials credentials` | The user credentials to use for this operation. | ### Connecting to an existing group Once you have created a subscription group, clients can connect to that subscription group. A subscription in your application should only have the connection in your code, and you should assume that the subscription was created via the client API, the restful API, or manually in the UI. The most important parameter to pass when connecting is the buffer size. This parameter represents how many outstanding messages the server should allow this client. If this number is too small, your subscription will spend much of its time idle as it waits for an acknowledgment to come back from the client. If it's too big, you waste resources and can start causing time out messages depending on the speed of your processing. ::: warning Slow consumers If you define a large buffer and your consumer is slow, the subscription might time out on the server and send the same buffer again. Such a situation leads to severe performance degradation of the persistent subscription and the cluster node. ::: ```csharp var subscription = await connection.ConnectToPersistentSubscriptionAsync( "myStream", "agroup", (_, evt) => Console.Out.WriteLineAsync("event appeared"), (sub, reason, exception) => Console.WriteLine($"Subscription dropped: {reason}") ); ``` | Parameter | Description | |:------------------------------|:-----------------------------------------------------------------------------| | `string stream` | The name of the stream which the persistent subscription is on. | | `string groupName` | The name of the subscription group to connect to. | | `Action eventAppeared` | The action to call when an event arrives over the subscription. | | `Action subscriptionDropped` | The action to call if the subscription is dropped. | | `UserCredentials credentials` | The user credentials to use for this operation. | | `int bufferSize` | The number of in-flight messages this client is allowed. | | `bool autoAck` | Whether to automatically acknowledge messages after `eventAppeared` returns. | ### Acknowledging events Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If you enable auto-ack, the subscription will automatically acknowledge messages once your handler completes them. If you throw an exception, it will shut down your subscription with a message and the uncaught exception. You can choose to not auto-ack messages. This can be useful when you have multi-threaded processing of messages in your subscriber and need to pass control to something else. There are methods on the subscription object that you can call: `Acknowledge` and `NotAcknowledge`. Both take a `ResolvedEvent` (the one you processed) and both also have overloads for passing an `IEnumerable`. ### Consumer strategies When creating a persistent subscription, the settings allow for different consumer strategies via the `WithNamedConsumerStrategy` method. Built-in strategies are defined in the enum `SystemConsumerStrategies`. #### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client is ignored until events are acknowledged/not acknowledged. This strategy provides equal load balancing between all consumers in the group. #### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round robin style, and the process is repeated. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. #### Pinned For use with an indexing projection such as the system `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects its buckets are assigned to other clients. When a client connects, it is assigned some of the existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. *This is not a guarantee*, and you should handle the usual ordering and concurrency issues. ### Replay parked messages Replays all parked messages for a particular persistent subscription `subscriptionName` on a `stream` that were parked by a negative acknowledgement action. ```csharp public Task ReplayParkedMessages( string stream, string subscriptionName, UserCredentials userCredentials = null ) ``` ### Updating a subscription group You can edit the settings of an existing subscription group while it is running instead of deleting the subscription group and recreating it to change its settings. When you update a subscription group, it resets itself internally, dropping the connections and having them reconnect. You must have admin permissions to update a persistent subscription group. ```csharp var settings = PersistentSubscriptionSettings .Create() .ResolveLinkTos() .StartFromCurrent(); var result = await connection.UpdatePersistentSubscriptionAsync( stream, "agroup", settings, MyCredentials ); ``` ::: tip If you change settings such as `startFromBeginning`, this doesn't reset the group's checkpoint. If you want to change the current position in an update, you must delete and recreate the subscription group. ::: | Parameter | Description | |:------------------------------------------|:----------------------------------------------------------------| | `string stream` | The name of the stream which the persistent subscription is on. | | `string groupName` | The name of the subscription group to update. | | `PersistentSubscriptionSettings settings` | The settings to use when updating this subscription. | | `UserCredentials credentials` | The user credentials to use for this operation. | ### Persistent subscription settings Both the `Create` and `Update` methods take a `PersistentSubscriptionSettings` object as a parameter. The methods use this object to provide the settings for the persistent subscription. A fluent builder is available for these options that you can locate using the `Create()` method. For example: ```csharp var settings = PersistentSubscriptionSettings .Create() .ResolveLinkTos() .StartFromCurrent(); ``` The following table shows the options you can set on a persistent subscription. | Member | Description | |:-----------------------------------------|:---------------------------------------------------------------------------------------------------------------------| | `ResolveLinkTos` | Tells the subscription to resolve link events. | | `DoNotResolveLinkTos` | Tells the subscription to not resolve link events. | | `PreferRoundRobin` | If possible, prefers a round robin between the connections with messages (Will use next available if not possible). | | `PreferDispatchToSingle` | If possible, prefers dispatching to a single connection (Will use next available if not possible). | | `StartFromBeginning` | Starts the subscription from the first event in the stream. | | `StartFrom(int position)` | Starts the subscription from the position-th event in the stream. | | `StartFromCurrent` | Starts the subscription from the current position. | | `WithMessageTimeoutOf(TimeSpan timeout)` | Sets the timeout for a client before retrying the message. | | `CheckPointAfter(TimeSpan time)` | The amount of time the system should try to checkpoint after. | | `MinimumCheckPointCountOf(int count)` | The minimum number of messages to write a checkpoint for. | | `MaximumCheckPointCountOf(int count)` | The maximum number of messages not checkpointed before forcing a checkpoint. | | `WithMaxRetriesOf(int count)` | Sets the number of times to retry a message should before considering it a bad message. | | `WithLiveBufferSizeOf(int count)` | The size of the live buffer (in memory) before resorting to paging. | | `WithReadBatchOf(int count)` | The size of the read batch when in paging mode. | | `WithBufferSizeOf(int count)` | The number of messages to buffer when in paging mode. | | `WithExtraStatistics` | Tells the backend to measure timings on the clients so statistics contain histograms of them. | ### Deleting a subscription group The delete operation can be used to remove a subscription group. Like the creation of groups, you rarely do this in your runtime code and is undertaken by an administrator running a script. ```csharp var result = await connection.DeletePersistentSubscriptionAsync( stream, "groupname", AdminCredentials ); ``` | Parameter | Description | |:------------------------------|:----------------------------------------------------------------| | `string stream` | The name of the stream which the persistent subscription is on. | | `string groupName` | The name of the subscription group to update. | | `UserCredentials credentials` | The user credentials to use for this operation. | ### Monitoring persistent subscriptions The client API includes helper methods that wrap the HTTP API to allow you to monitor persistent subscriptions. This page describes the methods found in the `PersistentSubscriptionsManager` class. All methods in this class are asynchronous. #### Create the manager instance Before accessing any of the methods described below, you need to create an instance of the `PersistentSubscriptionsManager` class. It needs a logger instance and one of the variations of the `EndPoint` abstract class. You would normally use either the `IpEndPoint` or `DnsEndPoint`. Since all HTTP calls would be redirected to the cluster leader, you can use the IP address of any cluster node. When using the `DnsEndPoint`, you can specify the DNS name of the cluster. For example: ```csharp var subscriptionManager = new PersistentSubscriptionsManager( new ConsoleLogger(), new DnsEndPoint("esdb.acme.org", 2113), TimeSpan.FromSeconds(1) ); ``` Notice that you need to specify the HTTP port that your application can reach. Most probably you'd need to use the external HTTP port, which is `2113` by default. #### Get persistent subscriptions from all streams Returns information about all persistent subscriptions from all streams. ```csharp Task List(UserCredentials userCredentials = null); ``` #### Get persistent subscriptions for a stream Returns information about the persistent subscription for a stream you specify with `stream`. You must have access to the stream. ```csharp Task List(string stream, UserCredentials userCredentials = null); ``` #### Get persistent subscription for a stream Gets the details of the persistent subscription `subscriptionName` on `stream`. You must have access to the persistent subscription and the stream. ```csharp Task Describe(string stream, string subscriptionName, UserCredentials userCredentials = null); ``` --- --- url: 'https://docs.kurrent.io/cloud/index.md' --- # Kurrent Cloud Kurrent Cloud documentation. --- --- url: 'https://docs.kurrent.io/cloud/automation/index.md' --- # Automations In addition to the [Cloud console][cloud console], Kurrent Cloud provides an API as well as the following automation tools: * [Terraform provider][terraform] * [Pulumi provider][pulumi] * Kurrent Cloud [CLI tool][esc cli github] You can use any of those tools to automate any operation accessible from the console. [esc cli github]: https://github.com/kurrent-io/esc [cloud console]: https://console.kurrent.cloud/ [cloud console tokens]: https://console.kurrent.cloud/authentication-tokens [cloud console organizations]: https://console.kurrent.cloud/organizations [pulumi]: https://www.pulumi.com/registry/packages/eventstorecloud/ [terraform]: https://registry.terraform.io/providers/kurrent-io/kurrentcloud/latest --- --- url: 'https://docs.kurrent.io/cloud/automation/pulumi.md' --- # Pulumi provider Kurrent Cloud provides a Pulumi provider to automate the provisioning of KurrentDB clusters. The provider is available via the [Pulumi Registry][pulumi provider]. ## Installation This package is available in many languages in the standard packaging formats. ### Get the plugin For projects that use .NET and Go Pulumi SDK you have to install the provider before trying to update the stack. Use the following command to add the plugin to your environment: ```bash pulumi plugin install resource eventstorecloud [version] \ --server https://github.com/kurrent-io/pulumi-eventstorecloud/releases/download/[version] ``` Example: ```bash pulumi plugin install resource eventstorecloud v0.2.3 \ --server https://github.com/kurrent-io/pulumi-eventstorecloud/releases/download/v0.2.7 ``` ### Configuration The following configuration points are available for the `eventstorecloud` provider: * `eventstorecloud:organizationId` - the organization ID for an existing organization in Kurrent Cloud * `eventstorecloud:token` - a valid refresh token for an Kurrent Cloud account with admin access to the organization ### Node.js (Java/TypeScript) To use from JavaScript or TypeScript in Node.js, install using either `npm`: ```bash npm install @eventstore/pulumi-eventstorecloud ``` or `yarn`: ```bash yarn add @eventstore/pulumi-eventstorecloud ``` ### .NET Add the NuGet package `Pulumi.EventStoreCloud` to your Pulumi project, which uses the .NET Pulumi SDK. ### Go To use from Go, use `go get` to grab the latest version of the library ```bash go get github.com/EventStore/pulumi-eventstorecloud/sdk/go/eventstorecloud ``` ## Usage Find comprehensive usage examples in the [Pulumi Registry][pulumi provider]. [pulumi provider]: https://www.pulumi.com/registry/packages/eventstorecloud/ --- --- url: 'https://docs.kurrent.io/cloud/automation/terraform.md' --- # Terraform provider Kurrent Cloud provider for Terraform is available in the public [provider registry][terraform registry]. Provider documentation is available there as well, on the [Documentation tab](https://registry.terraform.io/providers/kurrent-io/kurrentcloud/latest/docs). ::: warning Provider Migration Notice The legacy `EventStore/eventstorecloud` provider is deprecated. Users should migrate to the new `kurrent-io/kurrentcloud` provider registry location. See our [Migration Guide](#migration-from-eventstore-cloud-provider) below for detailed instructions. ::: ## Installation The current version of the provider is: {{ $frontmatter.terraform\_current\_version }}. The releases are available in Terraform official [registry][terraform registry] and via [GitHub releases][terraform github releases]. The binaries are available for the following platforms: | Processor | Operating system | Filename | |:----------|:-----------------|:--------------------------------------------------------------------------------------------------| | x64 | macOS | terraform-provider-kurrentcloud\_{{ $frontmatter.terraform\_current\_version }}*darwin\_amd64.zip | | x64 | FreeBSD | terraform-provider-kurrentcloud*{{ $frontmatter.terraform\_current\_version }}*freebsd\_amd64.zip | | x64 | Linux | terraform-provider-kurrentcloud*{{ $frontmatter.terraform\_current\_version }}*linux\_amd64.zip | | x64 | Windows | terraform-provider-kurrentcloud*{{ $frontmatter.terraform\_current\_version }}*windows\_amd64.zip | | arm64 | FreeBSD | terraform-provider-kurrentcloud*{{ $frontmatter.terraform\_current\_version }}*freebsd\_arm64.zip | | arm64 | Linux | terraform-provider-kurrentcloud*{{ $frontmatter.terraform\_current\_version }}\_linux\_arm64.zip | ### Terraform 0.13+ Terraform supports third party modules installed via the plugin registry. Add the following to your terraform module configuration: @[code](snippets/providers_kurrentcloud.tf.hcl) ### Terraform 0.12 In order for Terraform to find the plugin, place the appropriate binary into the Terraform third-party plugin directory. The location varies by operating system: * Linux and macOS `~/.terraform.d/plugins` * Windows `%APPDATA%\terraform.d\plugins` Alternatively, the binary can be placed alongside the main `terraform` binary. You can download the provider using the following commands: ::: code-tabs#os @tab Linux @[code](./snippets/download_provider_linux.sh) @tab macOS @[code](./snippets/download_provider_macos.sh) @tab Windows @[code](./snippets/download_provider_windows.ps1.powershell) ::: ### Building from source If you prefer to install from source, use the `make install` target in this [repository][terraform github]. You will need a Go 1.13+ development environment. ## Provider configuration The Kurrent Cloud provider must be configured with an access token. There are several additional options that may be useful. Provider configuration options are: | Option | Environment Variable | Description | |:------------------|:---------------------|:--------------------------------------------------------------------------------------------------------------------| | `token` | `ESC_TOKEN` | *Required*, your access token for Kurrent Cloud. | | `organization_id` | `ESC_ORG_ID` | *Required*, your Kurrent Cloud organization ID. | | `url` | `ESC_URL` | *Optional*, the URL of the Kurrent Cloud API. This defaults to the public cloud instance of Kurrent Cloud. | | `token_store` | `ESC_TOKEN_STORE` | *Optional*, the location on the local filesystem of the token cache. This is shared with the Kurrent Cloud CLI. | ### Obtaining the access token You can use the [Kurrent Cloud console][cloud console tokens] or the [Kurrent Cloud CLI][esc cli github releases] (`esc-cli`) to obtain a token Use the following command to get the access token using `esc-cli`: ```bash $ esc access tokens create --email Password: Token created for audience https://api.kurrent.cloud FDGco0u_1Ypw9WVVIfAHtIJh0ioUI_XbMhxMlEpiCUlHR ``` If you prefer to use the Cloud Console, navigate to the [Authentication Tokens](https://console.kurrent.cloud/authentication-tokens) page, then click on "Request refresh token" button. ![token in cloud console](images/token_console.png) ### Obtaining the organisation ID As for the token, you can use the Cloud Console, or `esc-cli` to get the organisation ID. That's how you do it with `esc-cli`: ```bash $ esc resources organizations list Organization { id: OrgId("9bdf0s5qr76g981z5820"), name: "Kurrent, Inc" ``` In the Cloud Console, open the [organisations page][cloud console organizations]. Then, select the organisation from the list and go to its settings. There, you can copy the organisation ID. ## Resources All resources in Kurrent Cloud can be provisioned using the Terraform provider. Existing projects can be queried using a data source in the provider. More complete samples can be found [here][terraform github samples]. Using the Terraform provider, you can create, manipulate, and delete the following resources in Kurrent Cloud: | Terraform resource | Kurrent Cloud resource | |:----------------------------------|:----------------------------------------------------------------| | `kurrentcloud_project` | [Project](#projects) | | `kurrentcloud_network` | [Network](#networks) | | `kurrentcloud_peering` | [Network peering](#network-peerings) | | `kurrentcloud_managed_cluster` | [Managed KurrentDB instance or cluster](#managed-kurrentdb) | ### Projects You can create Kurrent Cloud projects for the organisation using the `kurrentcloud_project` resource. You only need to provide the new project name, which must be unique within the organisation. You need a project to provision any other resource. #### Arguments | Name | Type | Description | |:-------|:---------|:-------------------------------------| | `name` | `string` | *Required*, the name of the project. | #### Attributes The Project Terraform resource will get the following attributes: | Name | Type | Description | |:-----|:---------|:------------| | `id` | `string` | Project ID | You will need the project ID to provision other resources within the project. #### Creating a project Here is an example of a Terraform script to create a project in Kurrent Cloud: @[code](./snippets/kurrentcloud_project.create.tf.hcl) ### Networks Before provisioning a database cluster, you need a network, which the cluster will connect to. Use the `kurrentcloud_network` resource to provision a new Kurrent Cloud network. The network should be in the same cloud provider, which you plan to use for the database cluster. #### Arguments | Name | Type | Description | |:--------------------|:---------|:---------------------------------------------------------------------------| | `name` | `string` | *Required*, the new network name. | | `project_id` | `string` | *Required*, the project ID of the new network (see [Projects](#projects)) | | `resource_provider` | `string` | *Required*, the network cloud provider (`aws`, `gcp`, `azure`). | | `region` | `string` | *Required*, the cloud region of the new network (cloud provider-specific). | | `cidr_block` | `string` | *Required*, the new network IP range. | #### Attributes | Name | Type | Description | |:-----|:---------|:---------------| | `id` | `string` | The project ID | Region names must be in the format used by the cloud resource provider, for example `us-west-2` for AWS, `East US` for Azure, `us-east1` for GCP. **Note** For the IP range, the maximum prefix length is `/9` and the minimum is `/24`. However, cloud providers have their own limitations on the network ranges they support. Learn more in your cloud provider documentation: * AWS [VPC Addressing](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-ip-addressing.html) * Azure [Virtual Network FAQ](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#what-address-ranges-can-i-use-in-my-vnets) * GCP [VPC Network](https://cloud.google.com/vpc/docs/vpc#valid-ranges) Smaller networks can hold fewer managed clusters, but may be easier to peer to infrastructure hosting your applications. #### Creating a network @[code](./snippets/kurrentcloud_network.create.tf.hcl) ### Network peerings When you got a network provisioned, you can already start creating database clusters. However, you won't be able to connect to your new cluster, unless you create a peering link between the network in Kurrent Cloud, and the network on your own cloud account or project. Use the `kurrentcloud_peering` resource to initiate the peering link. You will need to collect the details about your own cloud network (VPC or Virtual Network) as described in the arguments list below. Depending on the cloud provider, you'll need to complete some actions on your side to confirm the peering. At the moment, you can only peer the networks, which are in the same cloud region. ::: tip The format of the following arguments depends on the cloud provider: * `peer_account_id` * `peer_network_region` * `peer_network_id` ::: #### Arguments | Name | Type | Description | |:-------------------------|:---------|:--------------------------------------------------------------------------------------------------------| | `name` | `string` | *Required*, the new peering name. | | `project_id` | `string` | *Required*, the project ID for the new peering. | | `network_id` | `string` | *Required*, the Kurrent Cloud network ID, for which the peering will be created. | | `peer_resource_provider` | `string` | *Required*, the cloud resource provider of the given network (`aws`, `gcp`, `azure`). | | `peer_network_region` | `string` | *Required*, the cloud region of your own network, which you are going to peer with. | | `peer_account_id` | `string` | *Required*, your cloud account ID, your cloud network should belong to that account. | | `peer_network_id` | `string` | *Required*, the network ID for your own cloud network. | | `routes` | `string` | *Required*, CIDR blocks in your cloud network, which should be routed to the Kurrent Cloud network. | Use the following provider-specific values for the `peer_account_id` argument: | Cloud | Account ID is | |:------|:--------------| | AWS | Account ID | | GCP | Project ID | | Azure | Tenant ID | For the `peer_network_id`, use the following cloud network property: | Cloud | Network ID is | |:------|:----------------------------| | AWS | VPC ID | | GCP | VPC name | | Azure | Virtual network resource ID | ::: tip * `peer_resource_provider` - currently, this must be the same as the resource provider of the Kurrent Cloud network. * `peer_network_region` - currently, this must be the same as the region of the Kurrent Cloud network, and specified in the format used by your cloud. For example `us-west-2` for AWS, `westus2` for Azure and `us-east1` for GCP * `routes` - typically, this consists of one element, the address space of a subnet in your managed network. ::: #### Attributes After completing the operation, the peering Terraform resource will get the following attributes: | Name | Type | Description | |:----------------------|:---------|:-------------------------------------------------------------------------------------------------| | `id` | `string` | The peering ID. | | `provider_metadata` | `string` | The peering resource metadata, set by the cloud provider. | | `aws_peering_link_id` | `string` | The AWS peering link ID. | | `gcp_project_id` | `string` | GCP project ID. | | `gcp_network_name` | `string` | GCP VPC name. | | `gcp_network_id` | `string` | GCP network ID in URL format, which can be passed to `google_compute_network_peering` resources. | For AWS, you'd need to confirm the peering request, use the `aws_peering_link_id` resource attribute for that purpose. For GCP, you need to initiate a peering from your cloud account to Kurrent Cloud. Use the resource attributes with `gcp` prefix to automate that part. #### Creating a peering Here is an example how to initiate a peering from Kurrent Cloud to your own AWS account: @[code](./snippets/kurrentcloud_peering.create.tf.hcl) ### Managed KurrentDB Use the `kurrentcloud_managed_cluster` resource to provision a KurrentDB cluster or instance. You will need the [Project](#projects) and the [Network](#networks) resource information from previously created resources. #### Arguments | Name | Type | Description | |:-------------------|:---------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | `string` | *Required*, the name of the managed. cluster. | | `project_id` | `string` | *Required*, the ID of the project in which the managed cluster should be created. | | `network_id` | `string` | *Required*, the ID of the Kurrent Cloud network into which the managed cluster should be created. | | `topology` | `string` | *Required*, the topology of the managed cluster. This determines the fault tolerance of the cluster. Valid values are `single-node` and `three-node-multi-zone`. | | `instance_type` | `string` | *Required*, the size of the instances to use in the managed cluster. | | `disk_size` | `int` | *Required*, the size of the data disks in gigabytes. Minimal size is 10Gb. All cluster members will get a disk of the same size. | | `disk_type` | `string` | *Required*, `GP2`, `GP3` (AWS), `premium-ssd-lrs` (Azure), `ssd` (GCP). | | `disk_iops` | `int` | *Optional*, the number of IOPS for data disk. *Required* if disk\_type is `GP3`. | | `disk_throughput` | `int` | *Optional*, throughput in MB/s for data disk. *Required* if disk\_type is `GP3`. | | `server_version` | `string` | *Required*, `23.10`, `24.10`, `25.0`| | `projection_level` | `string` | *Optional*, default: `off` , the mode in which to enable projections. Valid values are `off` , `system` , `user`. | Supported instance sizes are: | Size | Specification | |:-------|:-----------------------------------------------------------------| | `F1` | 2 vCPU 1Gb RAM (burstable instance, not suitable for production) | | `C4` | 2 vCPU 8Gb RAM | | `M8` | 2 vCPU 8Gb RAM (same resources as `C4`, but storage-optimised) | | `M16` | 4 vCPU 16Gb RAM | | `M32` | 8 vCPU 32Gb RAM | | `M64` | 16 vCPU 64Gb RAM | | `M128` | 32 vCPU 128Gb RAM | ::: tip The actual implementation of each topology is specific to the resource provider. For GCP and AWS clusters you can resize the disks without downtime. In Azure, it is currently not supported, please plan the disk size according to your projected database size. ::: #### Attributes After completing the operation, the KurrentDB cluster Terraform resource will get the following attributes: | Name | Type | Description | |:--------------------|:---------|:------------------------------------------------------------------------------------------------| | `id` | `string` | the ID of the cluster. | | `dns_name` | `string` | the DNS name at which the cluster can be found. | | `resource_provider` | `string` | the resource provider into which the cluster was provisioned. | | `region` | `string` | the region in which the cluster was provisioned. | | `gcp_network_name` | `string` | network name for the peering link in GCP. | | `gcp_network_id` | `string` | GCP Network ID in URL format which can be passed to `google_compute_network_peering` resources. | ::: tip Attribute values for `region` and `resource_provider` are controlled by the network in which the cluster is created. ::: #### Creating a cluster Here are the cloud-specific examples of a Terraform script to create a managed KurrentDB cluster: ::: code-tabs @tab AWS @[code](./snippets/kurrentcloud_managed_cluster.create.aws.tf.hcl) @tab Azure @[code](./snippets/kurrentcloud_managed_cluster.create.az.tf.hcl) @tab GCP @[code](./snippets/kurrentcloud_managed_cluster.create.gcp.tf.hcl) ::: ## Data sources The following data source is available: | Terraform resource | Kurrent Cloud resource | |:--------------------------|:---------------------------| | `kurrentcloud_project` | [Project](#project) | ### Project Use the `kurrentcloud_project` data source to query your Kurrent Cloud projects. #### Arguments | Name | Type | Description | |:-----|:-------|:-------------------------------------| | name | string | *Required*, the name of the project. | | id | string | *Optional*, the name of the project. | #### Looking up a project @[code](./snippets/kurrentcloud_project.lookup.tf.hcl) ::: tip The value of `kurrentcloud_project.name` is case-sensitive, so `Production Project` is not the same as `^production project`. ::: ## Migration from EventStore Cloud Provider If you're migrating from the legacy `EventStore/eventstorecloud` provider to `kurrent-io/kurrentcloud`, this section provides comprehensive migration instructions. ### Quick Migration Summary 1. **Provider Registry**: Change from `EventStore/eventstorecloud` to `kurrent-io/kurrentcloud` 2. **Resource Prefixes**: `eventstorecloud_*` resources are deprecated, use `kurrentcloud_*` instead 3. **Version 2.0.0+**: Both prefixes supported for backward compatibility ### Provider Source Migration #### Before (deprecated) ```hcl terraform { required_providers { eventstorecloud = { source = "EventStore/eventstorecloud" version = "~> 1.0" } } } provider "eventstorecloud" { token = "your-token" organization_id = "your-org-id" } ``` #### After (recommended) ```hcl terraform { required_providers { kurrentcloud = { source = "kurrent-io/kurrentcloud" version = "~> 2.0" } } } provider "kurrentcloud" { token = "your-token" organization_id = "your-org-id" } ``` ### Terraform State Migration If you have existing resources managed by the old provider, migrate them using the `terraform state replace-provider` command: #### Step 1: Backup Your State ::: danger Important Always backup your Terraform state before making changes: ::: ```bash # Create a backup of your current state terraform state pull > terraform.tfstate.backup ``` #### Step 2: Replace Provider in State ```bash terraform state replace-provider \ registry.terraform.io/EventStore/eventstorecloud \ registry.terraform.io/kurrent-io/kurrentcloud ``` For automatic approval (useful in CI/CD pipelines): ```bash terraform state replace-provider -auto-approve \ registry.terraform.io/EventStore/eventstorecloud \ registry.terraform.io/kurrent-io/kurrentcloud ``` #### Step 3: Update Configuration and Reinitialize ```bash terraform init ``` ### Resource Prefix Migration #### Available Resources **New `kurrentcloud_` resources (recommended):** * `kurrentcloud_project` * `kurrentcloud_acl` * `kurrentcloud_network` * `kurrentcloud_peering` * `kurrentcloud_managed_cluster` * `kurrentcloud_scheduled_backup` * `kurrentcloud_integration` **Deprecated `eventstorecloud_` resources (still supported):** * `eventstorecloud_project` * `eventstorecloud_acl` * `eventstorecloud_network` * `eventstorecloud_peering` * `eventstorecloud_managed_cluster` * `eventstorecloud_scheduled_backup` * `eventstorecloud_integration` #### Updating Resource Prefixes ##### Method 1: Using terraform state mv ```bash # Backup your state first terraform state pull > terraform.tfstate.backup # Update configuration files, then move resources in state terraform state mv eventstorecloud_project.my_project kurrentcloud_project.my_project terraform state mv eventstorecloud_managed_cluster.my_cluster kurrentcloud_managed_cluster.my_cluster # Verify no changes needed terraform plan ``` ##### Method 2: Using moved Blocks (Terraform v1.1+) Add temporary `moved` blocks to your configuration: ```hcl # Temporary moved block - remove after successful migration moved { from = eventstorecloud_managed_cluster.my_cluster to = kurrentcloud_managed_cluster.my_cluster } # Updated resource with new prefix resource "kurrentcloud_managed_cluster" "my_cluster" { name = "production-cluster" project_id = "my-project-id" network_id = "my-network-id" instance_type = "F1" disk_size = 24 disk_type = "GP2" server_version = "23.10" } ``` Then run: ```bash terraform plan # Should show move operations terraform apply ``` ### Migration Best Practices 1. **Test in Non-Production First** - Always test the migration process in a development environment 2. **Plan Your Migration** - Review all resources that will be affected 3. **Gradual Migration** - Migrate one resource type at a time 4. **Version Pinning** - Pin your provider version during migration for consistency ### Troubleshooting Common Issues #### Issue: `terraform init` fails after state migration ``` Error: Failed to query available provider packages ``` **Solution**: Clear the `.terraform` directory and run `terraform init` again: ```bash rm -rf .terraform .terraform.lock.hcl terraform init ``` #### Issue: Provider not found during plan/apply ``` Error: Registry does not have a provider named registry.terraform.io/EventStore/eventstorecloud ``` **Solution**: Ensure you've completed both state migration and configuration updates, then run `terraform init`. #### Issue: Plan shows destroy/create operations ::: danger Warning If you see destroy/create operations, **DO NOT APPLY** - this would destroy your infrastructure. ::: **Solution**: 1. Check that you updated your configuration files correctly 2. Verify the state mv command syntax 3. Ensure both resource types point to the same underlying resource schema ### Getting Help ::: tip Additional Resources * **Detailed Migration Guide**: See our [GitHub MIGRATION.md](https://github.com/kurrent-io/terraform-provider-kurrentcloud/blob/trunk/MIGRATION.md) for comprehensive technical documentation * **Provider Documentation**: [Terraform Registry Documentation](https://registry.terraform.io/providers/kurrent-io/kurrentcloud/latest/docs) * **Support**: Open an issue in the [GitHub repository](https://github.com/kurrent-io/terraform-provider-kurrentcloud/issues) ::: ## FAQ **Error `error obtaining access token: error 400 requesting access token`** You need to add the access token to your environment variables or the provider configuration. See [here](#provider-configuration). **Error `... Forbidden: Access to the requested method for the requested resources was denied`** Make sure you used the correct organisation ID. Use [these guidelines](#provider-configuration) to get the correct value. **Error `Your query returned no results. Please change your search criteria and try again.`** Ensure you entered the correct project name. Remember that data source names are case-sensitive. See [here](#project). [terraform github releases]: https://github.com/kurrent-io/terraform-provider-kurrentcloud/releases [terraform github]: https://github.com/kurrent-io/terraform-provider-kurrentcloud [terraform github samples]: https://github.com/kurrent-io/terraform-provider-kurrentcloud/tree/trunk/examples [terraform registry]: https://registry.terraform.io/providers/kurrent-io/kurrentcloud/latest [esc]: https://eventstore.com/event-store-cloud/ [esc cli github]: https://github.com/kurrent-io/esc [esc cli github releases]: https://github.com/kurrent-io/esc/releases [cloud console]: https://console.eventstore.cloud/ [cloud console tokens]: https://console.eventstore.cloud/authentication-tokens [cloud console organizations]: https://console.eventstore.cloud/organizations [pulumi provider]: https://github.com/kurrent-io/pulumi-eventstorecloud --- --- url: 'https://docs.kurrent.io/cloud/faq.md' --- # FAQ ## Cluster provisioning #### Is it possible to change the cluster instance size or topology? You can [resize a cluster](ops/README.md#resizing-cluster-nodes), but we do not support changing the topology of an existing cluster. If you need a different topology, you can take a [backup](ops/backups.md#manual-backup) of the existing cluster, and [restore](ops/backups.md#restore-from-backup) that backup to a new cluster. #### Are there plans to support automatic resize of cluster nodes? We don't support automatic resizing of clusters. If you wish to automate cluster resizing, the `esc` command line tool, as well as the Terraform and Pulumi providers are options to consider. #### Are there plans to support automatic disk resize? Currently, you have to resize the disks yourself. Disks can be expanded on-demand without downtime. Because cloud providers limit how often disk resize operations can be performed the Kurrent Cloud may enforce a waiting period on recently resized clusters before they can get resized again. #### Can I switch a cluster from private to publicly accessible or vice versa? We don't support switching clusters from private to publicly accessible or vice versa. If you find yourself in a situation where you need to do that, you can take a [backup](ops/backups.md#manual-backup) of the existing cluster, and [restore](ops/backups.md#restore-from-backup) that backup to the network you need. ## Migrating to Cloud #### How can I migrate data from my self-hosted database to Kurrent Cloud? We have a [replication tool](guides/migration.md), which is available now. It has certain limitations, especially with performance. Get in touch, so we can help you to analyse your setup and requirements, before we can recommend using the replication tool. ## KurrentDB features unavailable in Kurrent Cloud The following features are available with self-managed KurrentDB server deployments only and are not available in Kurrent Cloud: * Archiving * Direct access to configuration * Encryption at rest * LDAP authentication * Logs download * OAuth authentication * OpenTelemetry exporter * Read-only replicas * Redaction * x.509 user certificates ## Performance #### Do you have indicative performance benchmarks for the offered cluster sizes? We published such benchmarks available on the [Cloud instance sizing guide](ops/sizing.md) page. Please remember, however, that each use case is different, and you might always get better or worse performance, compared with our advertised benchmarking figures. We can help you to analyse your needs and provide more detailed expected performance figures, please get in touch. ## Alerting and notifications #### Is there any type of alerting functionality for cluster issues? Yes. Learn more in the [Integrations](integrations/README.md) documentation section. #### How do you handle infrastructure related issues, which cause unavailability or degradation of service? Kurrent Cloud site reliability engineering (SRE) team manages cluster availability. More information about SLA levels provided by Kurrent Cloud can be found in our [Service Level Agreement](https://www.kurrent.io/terms/agreements/kurrent-cloud-services-addendum). ## Providers ### Azure #### Do you use Locally-Redundant Storage (LRS) or Zone-Redundant Storage (ZRS)? Customer data is only stored on Premium SSD block device targets. We do not utilize Azure file or blob storage for customer data. #### We use AKS, to simplify setup can you expose the cluster to the public internet? We have plans to expose clusters via a public IP address, but it's not available yet. We've provided a [guide](guides/kubernetes.md) on how to connect AKS clusters to Kurrent Cloud. ### Supported regions ### What regions do you support on AWS, GCP and Azure? At the moment, KurrentDB clusters are only available in regions with three or more availability zones. Each region also need to support a set of services necessary for the provisioning and monitoring of the clusters. You can see the list of supported regions when you create a new cluster or network in the Cloud Console. ::: note For AWS, we offer the regions that are enabled by default. We can enable [opt-in regions](https://docs.aws.amazon.com/controltower/latest/userguide/opt-in-region-considerations.html) for an organization on request. ::: If there is a region that you need, but is missing, please don't hesitate to [let us know](https://www.kurrent.io/contact). ## Managed KurrentDB #### Does the admin UI run on the provisioned cluster? Yes. Go to the clusters list in the Cloud Console, select the cluster you need and click on the **Addresses** tab on the bottom panel. You will find the admin UI URL there. You need to be connected to a network, which is routed to Kurrent Cloud to open the admin UI in your browser. #### Are there plans for the scheduled scavenge feature? Yes, we are actively working on it. ## Backup and restore #### Can I automate backups? The scheduled backup feature has been released in March 2021. Read more in [cloud backup documentation](ops/README.md#scheduled-backups). #### Can I download a backup to store it locally? Currently, it's not possible due to security considerations. In the future, we plan to offer data export functionality from the database. ## Service levels #### What is the SLA for Kurrent Cloud? You can find out about the SLA levels provided by Kurrent Cloud in our [Service Level Agreement](https://www.kurrent.io/terms/agreements/kurrent-cloud-services-addendum). #### Are maintenance windows part of the SLA? You can find out about the SLA levels provided by Kurrent Cloud in our [Service Level Agreement](https://www.kurrent.io/terms/agreements/kurrent-cloud-services-addendum). Any scheduled maintenances will be posted in the Kurrent Cloud Console, as well as on the [Kurrent Cloud Status Page](https://status.eventstore.cloud/). ## Support #### For same day response, what time zone is your 9am-5pm response against? Our support response time windows are provided in GMT time zone. We are expanding our SRE team to handle time zones in North America as well. #### What level of access does the customer have to resolve minor incidents? Customers do not have direct access to the compute instances where the database cluster nodes are running. However, most management functions are available via the HTTP API, which is available for customers to access and use. You can also use our cloud automation tools ([Terraform](automation/terraform.md) and [Pulumi](automation/pulumi.md) providers and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc)) to manage your cloud resources. #### If a node goes down in a cluster, how is the cluster recovered and who does it? Kurrent Support staff will be alerted if a node goes down. We will investigate the issue and take the necessary actions to return the cluster to a healthy state. If it is a due to load related to the customer's usage of the cluster or exhaustion of disk space, our support staff will attempt to reach out to the customer to address the issue. You can utilize our [integrations feature](integrations/README.md) to get notified of cluster events, such as high CPU usage, low disk space, or other events. Please ensure to learn about the SLA levels provided by Kurrent Cloud in our [Service Level Agreement](https://www.kurrent.io/terms/agreements/kurrent-cloud-services-addendum). ## Security #### How to get an audit log of access to the console? The Cloud Console access audit log is in our roadmap. Right now, you can [access the audit logs](ops/account-security.md#audit-log-api) using the `esc` command line tool. #### Does Kurrent have a security policy? Kurrent, Inc. has security policies in place. If you have specific questions please [contact us](https://www.kurrent.io/contact) #### Has the system been independently audited? Security is a priority at Kurrent, and every feature we build has security in mind. Our Cloud offering maintains [SOC 2](https://www.aicpa.org/interestareas/frc/assuranceadvisoryservices/aicpasoc2report.html) and [ISO 27001](https://www.iso.org/isoiec-27001-information-security.html) certifications, which require annual independent internal audits, as well as external audits as part of our certification process. If you have specific questions please [contact us](https://www.kurrent.io/contact) ## Troubleshooting ### Connectivity #### Unable to connect to a public cluster If you are trying to connect to a public cluster and you are not able to, please check the following: * You are able to resolve the cluster DNS name * Your IP address has been added to the IP Access List assigned to the cluster * Your local network policies permit outbound connections to TCP port 2113 It is possible that your local network may be using a transparent web proxy which could cause the common methods of determining your source IP address to be incorrect. If you are on a corporate network or VPN, you may be able to get your network administrator to assist you. #### DNS resolution issues For clusters on private networks, the DNS name will resolve to the private IP addresses of the cluster nodes. Some Internet providers, routers, and DNS servers will not resolve or filter out answers to DNS queries for `xxxx.mesdb.kurrent.cloud` because the DNS name resolves to a private IP range. You can check if that's the issue you're experiencing by running the following `nslookup` query. Replace the domain name by your cluster DNS name. ```bash nslookup buh63kqrh41nfqpviing.mesdb.kurrent.cloud ``` If the answer looks like the following, you're having the issue we just described: ``` Server: 192.168.192.1 Address: 192.168.192.1#53 Non-authoritative answer: *** Can't find buh63kqrh41nfqpviing.mesdb.kurrent.cloud: No answer ``` To check if the domain exists, perform the same check with a specific DNS server: ```bash nslookup buh63kqrh41nfqpviing.mesdb.kurrent.cloud 1.1.1.1 ``` Most probably, you will get a proper DNS resolution answer: ``` Server: one.one.one.one Address: 1.1.1.1 Non-authoritative answer: Name: buh63kqrh41nfqpviing.mesdb.kurrent.cloud Addresses: 172.29.98.189 172.29.98.150 172.29.98.108 ``` If the cluster DNS name resolves using an external DNS server, but your local DNS server fails or rejects to resolve it, you can try the following: * On a corporate network or VPN, check with your IT department or team responsible for network configuration to allow DNS queries which resolve to private IP ranges * On a home network, routers typically configure clients to use the DNS servers provided by your Internet provider, which could be blocking such queries * You can reconfigure your router to configure clients on your network to use public DNS servers like `1.1.1.1` or `8.8.8.8` * You can also change the DNS configuration of your local machine to use public DNS servers like `1.1.1.1` or `8.8.8.8` ### Cluster Creation Failed: Azure Capacity Constraints #### What Happened Your cluster creation failed because Azure returned an error that the specific virtual machine SKU required for the instance size you requested lacks sufficient capacity in that region for your subscription. #### Why This Happens Each Kurrent Cloud customer is assigned a dedicated Azure subscription. Azure VM SKU availability varies based on: * **Subscription-level capacity**: Azure allocates capacity per subscription, and some SKUs may not be available for all subscriptions in all regions. * **Regional capacity**: Certain VM sizes may be temporarily or permanently constrained in specific regions due to high demand. * **Availability zone constraints**: Some SKUs may be available in a region but not in all availability zones. This is an Azure platform limitation, not a Kurrent Cloud issue. #### Quick Workarounds 1. **Try a different instance size**: Select a different instance size when creating the cluster. If you requested a specific size (e.g., `F1`), try an alternative (e.g., `C4`). 2. **Try a different topology**: If you requested a three-node cluster, consider whether a single-node cluster would work for development/testing purposes. 3. **Try a different region**: Create the cluster in a different Azure region where the desired virtual machine type may have available capacity. #### If You Need a Specific Configuration If your use case requires a specific configuration in a specific region: 1. **Contact Kurrent Cloud Support**: Let us know the exact instance type, topology, and region you need. 2. **We will request capacity from Microsoft**: Our team can submit a capacity request to Azure on your behalf. Note that fulfillment depends on Azure capacity availability and there is no guaranteed timeline. 3. **Monitor for updates**: We will notify you once the capacity becomes available or if alternative options are recommended. #### Deleting the Failed Cluster The failed cluster will be in a `Defunct` state. You can delete it through the Kurrent Cloud console or CLI to clean up the resource. ## Operational characteristics of Kurrent Cloud Kurrent Cloud is a distributed fault-tolerant provisioning system and control plane. It is hosted in Amazon AWS. All data components and processing components are distributed across three availability zones. State is backed up, and the platform can be easily restored to another region in the event of a total region failure. ## The impact of outages on resources The following is a brief description of what can be expected given an outage of the given component. ### Kurrent Cloud #### Cloud API * Viewing, creating, updating, and deleting resources may be impacted. * Integrations and alerting may be impacted. * Scheduled backup jobs may be impacted. #### Web console * Viewing, creating, updating, and deleting resources may be impacted. #### Provisioned resources The availability of provisioned resources (networks, peering links, clusters, and backups) **will not** be affected in the case that Kurrent Cloud services become unavailable. ### Cloud providers Cloud Provider outages may affect the availability of resources or the ability to create, delete, and update resources within Kurrent Cloud. #### Networks and peerings While networks and peering links at cloud providers are fault-tolerant, occasionally events may occur that will degrade or, for a period of time remove network access to resources. #### Clusters In the event of a single availability zone failure, three-node topology clusters should continue to operate at a degraded level. If a leader node goes down, the remaining follower nodes will elect a new leader and the cluster will continue to function. When the leader node is able to re-join the cluster, it will join as a follower and catch up on any events that were written while it was down. In the event of a single availability zone failure, single node topology clusters may become unavailable until the service can be restored. Most failures will be handled by Kurrent Cloud, however there will be a period of lost connectivity. #### Backups Clusters and single nodes are backed up by volume snapshots at the selected cloud provider. These snapshots are stored within fault-tolerant object storage for the provisioned region. It is possible that access to backups may be impacted temporarily. There is also a possibility that scheduled backups will not run for the duration of the outage. --- --- url: 'https://docs.kurrent.io/cloud/getting-started/index.md' --- # Getting started ## Provisioning Kurrent Cloud allows you to provision managed KurrentDB clusters in AWS, GCP, and Azure backed by dedicated virtual machines. Clusters can be provisioned with public access enabled or restrict access to private network connection using VPC peering. ### Public access For public access, the provisioning process is straightforward. You can go to the Clusters screen and click on the **New cluster** button. Fill in the form values, then a network and IP Access List will be created and the cluster will be deployed. See the [Public Access Clusters](public.md) guide to get started. ### Private network connection For private network connection, the provisioning process is a bit more complex. Similar to public access, when you create a cluster, you can specify a private network for your chosen cloud provider. Once the network is created, you will need to initiate a VPC peering connection with your own VPC. The following guides will walk you through the process of creating a Private Access KurrentDB cluster for each cloud provider: * [Amazon Web Services (AWS)](private-access/aws.md) * [Google Cloud Platform (GCP)](private-access/gcp.md) * [Microsoft Azure](private-access/azure.md) ### Tutorials Once you have a cluster up and running, feel free to review the [tutorial](../../tutorials.md) section. --- --- url: 'https://docs.kurrent.io/cloud/getting-started/private-access/index.md' --- # Overview This section will guide you through deploying your first Managed KurrentDB cluster with private access via network peerings for each of the supported cloud providers. --- --- url: 'https://docs.kurrent.io/cloud/getting-started/private-access/aws.md' --- # Amazon Web Services For AWS customers, Kurrent Cloud allows provisioning a KurrentDB cluster in the same cloud. You can create a cluster in the same region to ensure the lowest latency. Pre-requisites: * You have an organization registered in Cloud console * You can log in to the Cloud console as admin * Your organization has at least one project * You are the admin of the project * A Virtual Private Cloud (VPC) in the AWS account from which the KurrentDB cluster will be accessed * You have access to AWS resources * Read access to VPC details * Write access to accept VPC Peering Connection requests for the VPC that will be peered with the Kurrent Cloud network * Write access to Route Tables for the subnets that will be used to access the KurrentDB cluster The provisioning process consists of three steps: 1. Create a network in Kurrent Cloud 2. Provision the KurrentDB instance or cluster 3. Peer the new network with your own AWS network ## Create a cluster In the Kurrent Cloud console, go to the [project context](../../../introduction.md#projects) under which you want to create the cluster and switch to **Clusters** view. Click on the **New cluster** button to begin the cluster creation process. ### Cluster name ![Cluster name](../images/new-cluster-name.png) Provide a descriptive name for the cluster in the **Cluster name** field. ### Network ![Create a network](images/aws/aws-new-cluster-private-network.png) In the **Network** section, if you have not created a network yet, you will see fields for creating a new network. If you have any existing Networks, you will see those listed, as well as the option to create a new Network. When creating a new network, you will need to provide the following information: * **Network name** is a descriptor to allow you to identify the network in the list of networks. * **Type** should be set to `Private`. * **Cloud provider** should be set to AWS. * **Region** is the AWS region where the cluster will be created. * **CIDR block** is the new network address range. As any other cloud network, the CIDR block must be within a range specified by RFC1918, e.g. `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. The minimum size of the CIDR block allowed is `/25`. ::: warning CIDR block overlap The network address range should not overlap with the address range of any other networks which you will be peering with. Once a network is created, you will not be able to change the CIDR block. To change the CIDR block, you will need to delete the network and create a new one. ::: ### Database ![Database configuration](../images/cluster-new-db.png) The **Database** section is where you can specify the database settings for the cluster. You will need to select the **Server Version**, and choose if [server-side projections](@server/features/projections/README.md) should be enabled and what level of projections should be enabled. ::: warning Projections impact on performance Both system projections and user-defined projections produce new events. Carefully consider the impact of enabled projections on database performance. Please refer to the [Performance impact](@server/features/projections/README.md#performance-impact) section of the projections documentation to learn more. ::: ### Instance size ![Instance size](../images/cluster-new-size-topology.png) The next section of the form allows choosing the instance size for cluster nodes. Use the provided [instance size guidelines](../../ops/sizing.md) to choose the right size for your cluster. Note that the `F1` size is using burstable VMs, which is not suitable for production use. ::: tip Vertical scaling If you find that your cluster is not performing as expected, you can always resize the cluster instances later. If you create a three-node cluster, a resize is done in a rolling fashion that should take only a few minutes and not impact the availability of the cluster. ::: You will also need to specify the topology of the cluster. We recommend three-node clusters to ensure high availability, but you can also create a single-node cluster for testing or development purposes. ### Storage ![Storage](images/aws/aws-new-cluster-storage.png) Now you need to configure the storage for the cluster. For all three providers, only one disk type is available at the moment via the Cloud console. The storage capacity is gigabytes, with 8GiB being the minimum for AWS, and 10GiB for Azure and GCP. Since we allow customers to expand the storage size online without service interruptions, you can start with smaller storage and expand it when you need more capacity. If you are creating an AWS cluster, you will see the option to specify IOPS and throughput. The default values are the defaults for GP3 volumes, so unless you need extra IOPS or throughput, you can leave the defaults. These values can be changed later. ::: note The cloud console only allows for the creation of GP3 clusters, but for backwards compatibility purposes it is still possible to create a cluster with GP2 storage using tools such as the Terraform provider or Kurrent Cloud CLI. ::: ### Pricing Finally, you will see the estimated monthly price for the selected cluster size and storage capacity. ::: note Network usage Since the network usage is billed based on actual usage, the estimated price will not reflect the full cost of the cluster. ::: ## Provisioning begins When you click on **Create cluster**, the provisioning process starts. You will be redirected to the cluster details page, where you can follow the progress of the provisioning process. As the creation process progresses, you will see the status of the cluster change. ![Cluster provisioning statuses](images/aws/aws-cluster-creation-steps.png) If you created a new network, it will be created first. You can see the status of the network creation in the **Networks** view. ![Network status](images/aws/aws-network-provisioning.png) Once you see the new cluster's status change to `Ok`, your cluster is ready to use, but you still need to setup a peering connection between your AWS VPC and the Kurrent Cloud network. ## Network peering When the cluster provisioning process finishes, you get a new cluster (or single instance), which is connected to the network selected or created in the first step. You won't be able to connect to the cluster since the network is not exposed to the Internet. In order to get access to the network and consequently to all the clusters in that network, you'd need to peer the Kurrent Cloud network to your own AWS network. Normally, your AWS network would be also accessible by applications, which you want to connect to the new cloud KurrentDB cluster. For this example, we'll use a VPC in AWS in the same region (`us-east-2`). ![AWS VPC details](images/aws/aws-vpc.png) If you navigate to the VPC section of the AWS console, then select the VPC you want to setup the peering connection with, you will see a **CIDRs** tab. The information provided in this tab along with the VPC details is enough to start the peering process. In Kurrent Cloud console, while in the same project context as the new network and cluster, click on **Networks** under the **Networking** section. Select the network you want to peer with the AWS VPC, and click on the **Peerings** tab in the network details. You should see there are no peerings yet. To begin the peering process, click on the **Initiate peering** button. ![Network with no peerings](images/aws/aws-peering-1.png) You will be taken to the peering creation form. Here you can give the new peering a name and provide some details about the AWS VPC with which you would like to initiate the peering connection. The following table will help you identify the values from the AWS console to fill out the form in the Kurrent Cloud Console. | Peering form | AWS terminology | |:--------------------|:--------------------------------------------| | Peer AWS Account ID | Owner ID | | Peer VPC ID | VPC ID | | Peer routes | One or more IPv4 CIDRs for the selected VPC | | AWS region | VPC region (not shown in the VPC details) | ![Initiate AWS peering](images/aws/aws-peering-2.png) You can specify more than one route if, for example, you want to peer a VPC with multiple subnets. ### Initiate peering When you click on the **Initiate peering** button, you'll be redirected back to the **Networks** screen, which should now show the new peering resource being provisioned. ![Peering provisioning](images/aws/aws-peering-3.png) After a few minutes, the Network's status will change to `Peering initiated`. ![Peering initiated](images/aws/aws-peering-4.png) When the Network's status changes to `Peering initiated`, you can click on the **Peering Connection ID** link to be taken to the **Peering connections** section of the AWS console, where you should see the incoming peering request. ![Incoming peering request](images/aws/aws-peering-pending.png) Select the pending peering and click on **Actions** - **Accept request**. Validate the request details and ensure that all the details match the peering, which you can see in Kurrent Cloud console. If everything is correct, click the **Accept request** button. ![Accept peering request](images/aws/aws-peering-accept.png) After you get a confirmation, you will see the peering in AWS console to become `Active`. Back in the Kurrent Cloud console, within a few moments the Network status will change to `Available` and the Peering status, under the Network details, will change to `Active`. ![Peering active](images/aws/aws-peering-active.png) Now, although both networks are now connected, AWS does not create the necessary routes for the VPC to reach the Kurrent Cloud network automatically. You will need to add a route for the peering connection to each of the route table(s) associated to the subnets that you want to be able to reach the Kurrent Cloud clusters. To finish the setup, open the AWS VPC details and then click on the **Resource map** tab. There you will see a list of **Subnets** and **Route tables**. ![AWS VPC resource map](images/aws/aws-vpc-resource-map.png) From the Resource map, click on the link to the Route table for the subnet(s) you want to be able to reach the Kurrent Cloud clusters. ![AWS VPC route table](images/aws/aws-rtb-1.png) Click on **Edit routes** and then **Add route**. In the **Destination** field, enter the CIDR of the Kurrent Cloud network. For the **Target**, choose the **Peering Connection** option. The list of available peering connections will pop up. Select the recently created peering from the list and click on **Save changes**. ![AWS route](images/aws/aws-rtb-2.png) Once saved, the route table should look similar to this: ![AWS route complete](images/aws/aws-rtb-3.png) If you are using one or more subnets associated to this VPC, make sure you update the routing table for all of them, not only on the main route table of the VPC. ::: tip Peering issues You might see the peering request getting stuck. There are several reasons this may happen, such as your cloud account quota or overlapping CIDR blocks. Check the Event Console in the Cloud console for diagnostic details, and contact support if you need assistance. ::: At this point, you should be able to connect to the KurrentDB cluster in the cloud from any VM or application connected to your AWS VPC network. Depending on your setup, you might already have a connection available from your local machine to the AWS VPC using a site-to-site VPN. If not, ask your AWS administrator about the connection details, which could be a Virtual Private Gateway or Client VPN Endpoint. ## Next steps You are now ready to start using the new KurrentDB cluster in the cloud. Head over to the [Operations](../../ops/README.md#connecting-to-a-cluster) page to learn how to connect to your cluster. --- --- url: 'https://docs.kurrent.io/cloud/getting-started/private-access/azure.md' --- # Microsoft Azure For Microsoft Azure customers, Kurrent Cloud allows provisioning a KurrentDB cluster in the same cloud. You can create a cluster in the same region to ensure the lowest latency. ::: warning Azure considerations Microsoft Azure has a few nuances that must be considered when using Kurrent Cloud with Azure. We listed all currently known limitations [below on this page](#considerations-for-microsoft-azure). Please ensure you are aware of them before starting to use KurrentDB in Azure. ::: Pre-requisites: * You have an organization registered in Cloud console * You can log in to the Cloud console as admin * Your organization has at least one project * You are the admin of the project * A Virtual Network in the Azure subscription from which the KurrentDB cluster will be accessed * You have the Azure CLI installed and logged in to your Azure subscription * You have access to create resources in the Azure subscription that will be used to access the KurrentDB cluster * Service principal for the Kurrent Cloud application * Role assignments for the service principal to grant access to create a Virtual Network Peering for the Virtual Network that will be peered with the Kurrent Cloud network The provisioning process consists of three steps: 1. Create a network in Kurrent Cloud 2. Provision the KurrentDB instance or cluster 3. Peer the new network with your own Azure network ## Create a cluster In the Kurrent Cloud console, go to the [project context](../../../introduction.md#projects) under which you want to create the cluster and switch to **Clusters** view. Click on the **New cluster** button to begin the cluster creation process. ### Cluster name ![Cluster name](../images/new-cluster-name.png) Provide a descriptive name for the cluster in the **Cluster name** field. ### Network ![Create a network](images/azure/azure-new-cluster-network.png) In the **Network** section, if you have not created a network yet, you will see fields for creating a new network. If you have any existing Networks, you will see those listed, as well as the option to create a new Network. When creating a new network, you will need to provide the following information: * **Network name** is a descriptor to allow you to identify the network in the list of networks. * **Type** should be set to `Private`. * **Cloud provider** should be set to Azure. * **Region** is the AWS region where the cluster will be created. * **CIDR block** is the new network address range. As any other cloud network, the CIDR block must be within a range specified by RFC1918, e.g. `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. The minimum size of the CIDR block allowed is `/25`. ::: warning CIDR block overlap The network address range should not overlap with the address range of any other networks which you will be peering with. Once a network is created, you will not be able to change the CIDR block. To change the CIDR block, you will need to delete the network and create a new one. ::: ### Database ![Database configuration](../images/cluster-new-db.png) The **Database** section is where you can specify the database settings for the cluster. You will need to select the **Server Version**, and choose if [server-side projections](@server/features/projections/README.md) should be enabled and what level of projections should be enabled. ::: warning Projections impact on performance Both system projections and user-defined projections produce new events. Carefully consider the impact of enabled projections on database performance. Please refer to the [Performance impact](@server/features/projections/README.md#performance-impact) section of the projections documentation to learn more. ::: ### Instance size ![Instance size](../images/cluster-new-size-topology.png) The next section of the form allows choosing the instance size for cluster nodes. Use the provided [instance size guidelines](../../ops/sizing.md) to choose the right size for your cluster. Note that the `F1` size is using burstable VMs, which is not suitable for production use. ::: tip Vertical scaling If you find that your cluster is not performing as expected, you can always resize the cluster instances later. If you create a three-node cluster, a resize is done in a rolling fashion that should take only a few minutes and not impact the availability of the cluster. ::: You will also need to specify the topology of the cluster. We recommend three-node clusters to ensure high availability, but you can also create a single-node cluster for testing or development purposes. ### Storage ![Storage](images/azure/azure-new-cluster-storage.png) Now you need to configure the storage for the cluster. For all three providers, only one disk type is available at the moment via the Cloud console. The storage capacity is gigabytes, with 8GiB being the minimum for AWS, and 10GiB for Azure and GCP. Since we allow customers to expand the storage size online without service interruptions, you can start with smaller storage and expand it when you need more capacity. ### Pricing Finally, you will see the estimated monthly price for the selected cluster size and storage capacity. ::: note Network usage Since the network usage is billed based on actual usage, the estimated price will not reflect the full cost of the cluster. ::: ## Provisioning begins When you click on **Create cluster**, the provisioning process starts. You will be redirected to the cluster details page, where you can follow the progress of the provisioning process. As the creation process progresses, you will see the status of the cluster change. ![Cluster provisioning statuses](images/azure/azure-cluster-creation-steps.png) If you created a new network, it will be created first. You can see the status of the network creation in the **Networks** view. ![Network status](images/azure/azure-network-provisioning.png) Once you see the new cluster's status change to `Ok`, your cluster is ready to use, but you still need to setup a peering link between your Azure Virtual Network and the Kurrent Cloud network. ## Network peering When the cluster provisioning process finishes, you get a new cluster (or single node), which is connected to the network created in the first step. You won't be able to connect to the cluster since the network is not exposed to the Internet. In order to get access to the network and consequently to all the clusters in that network, you'd need to peer the Kurrent Cloud Network to your own Azure Virtual Network where your applications are running. For this example, we'll use an Azure network the same region (Central US). In Kurrent Cloud console, navigate to the **Networks** view and click on the Private Network you want to setup a peering link for and click on the **Initiate peering** button. ![Network status](images/azure/azure-network-ready.png) ### Azure Virtual Network details You will need to gather the following information about your Azure Virtual Network: | Field | Description | |:----------------|:----------------------------------------------------------------------------------| | Peer Tenant ID | Tenant ID from Azure AD | | Peer Network ID | Network resource ID (can be found on the network Properties page or in JSON view) | | Azure region | Network region, cannot be changed | | Peer routes | One or more IP ranges for the selected VPC | You also need to find your tenant ID, which is only visible on the [Microsoft Entra admin center page](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/TenantOverview.ReactView). Alternatively, you can use the `Get-AzureADTenantDetails` [PowerShell command](https://docs.microsoft.com/en-us/powershell/module/azuread/Get-AzureADTenantDetail?view=azureadps-2.0). Peer Network ID is the one that requires the most attention, because it's not very obvious how to find it. You can find it in the Azure portal, in the Virtual Network's properties page or in the JSON view of the network. ![Azure network ID](images/azure/azure-network-resource-id-example.png) For our example, here is the complete form: ![Azure peering - complete form](images/azure/azure-peering-1.png) ### Initiate peering When you click on the `Next` button, Kurrent Cloud will check if it has permissions to create the peering (see [Azure Considerations](#network-peering-in-azure)). The Cloud console will display a set of pre-populated Azure CLI commands, which you need to execute in order for Kurrent Cloud to be able to create the peering. ![Azure peering - sa](images/azure/azure-peering-2.png) ::: tip Service principal Kurrent Cloud uses one service principal. It means that once you created it, the principal will be used for all the peerings you create. Therefore, you only need to execute the command `az ad sp create` once. ::: After completing all those commands, click on the `Initiate peering` button. You'll be redirected to the Networks view. If you select the Private Network, in the Network Details section, click on "Peerings". You will see a list of peerings that have been created for that Network. You should see the peering you just created in the list. After a little while, the status will change to `Intiated`. ::: tip Peering issues If the status doesn't change after 10 minutes, delete the peering and try again, ensuring the details were entered correctly. Mismatching network region and address range are the most common reasons for the peering to not being provisioned properly. ::: In a short while, the peering status in the Kurrent Cloud console should change to `Active`. ![Azure peering - status](images/azure/azure-peering-3.png) If you want to check the status of the peering in Azure, go back to Virtual Network in the Azure Portal and navigate to the `Peering` blade. There, you will see the newly provisioned peering, which should have `Connected` in the `Peering status` column. ![Azure peering - status](images/azure/azure-peering-blade.png) ::: tip Peering issues You might see the peering request getting stuck. There are several reasons for this to happen, such as entering the incorrect region, overlapping CIDR blocks, or provider quota issues. You can find all the necessary diagnostics in the Event Console in Kurrent Cloud. ::: You should now be able to connect to the KurrentDB cluster in the cloud from systems and applications that are connected to your Azure network. ## Next steps You are now ready to start using the new KurrentDB cluster in the cloud. Head over to the [Operations](../../ops/README.md#connecting-to-a-cluster) page to learn how to connect to your cluster. ## Considerations for Microsoft Azure Due to differences between Microsoft Azure and other cloud providers, the provisioning process in Kurrent Cloud is different from AWS and GCP. We've made a list of these differences here in order to help you make an informed decision about Cloud providers. ### Network peering in Azure When creating a peering link, Azure requires the user to configure a security principal, referencing the application ID of Kurrent Cloud, and also configure and apply a role allowing that principal to modify the network resource of the remote network. ### Disk We're aware that Azure Premium SSD volumes have a strict IOPS limit and this limit depends on the volume size. Very small volumes do not provide enough throughput for most production scenarios. We suggest you consider using at least 246 GiB disks to get enough IOPS for the database. You can check the current throughput limits for Azure Premium SSD volumes in [Azure documentation](https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#premium-ssd). --- --- url: 'https://docs.kurrent.io/cloud/getting-started/private-access/gcp.md' --- # Google Cloud Platform For Google Cloud customers, Kurrent Cloud allows provisioning a KurrentDB cluster in the same cloud. You can create a cluster in the same region to ensure the lowest latency. Pre-requisites: * You have an organization registered in Cloud console * You can log in to the Cloud console as admin * Your organization has at least one project * You are the admin of the project * You have access to create Google Cloud resources in the GCP project of your organization The provisioning process consists of three steps: 1. Create a network in Kurrent Cloud 2. Provision the KurrentDB instance or cluster 3. Peer the new network with your own network in GCP ## Create a cluster In the Kurrent Cloud console, go to the [project context](../../../introduction.md#projects) under which you want to create the cluster and switch to **Clusters** view. Click on the **New cluster** button to begin the cluster creation process. ### Cluster name ![Cluster name](../images/new-cluster-name.png) Provide a descriptive name for the cluster in the **Cluster name** field. ### Network ![Create a network](images/gcp/gcp-new-cluster-network.png) In the **Network** section, if you have not created a network yet, you will see fields for creating a new network. If you have any existing Networks, you will see those listed, as well as the option to create a new Network. When creating a new network, you will need to provide the following information: * **Network name** is a descriptor to allow you to identify the network in the list of networks. * **Type** should be set to `Private`. * **Cloud provider** should be set to `Google Cloud`. * **Region** is the GCP region where the cluster will be created. * **CIDR block** is the new network address range. As any other cloud network, the CIDR block must be within a range specified by RFC1918, e.g. `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. The minimum size of the CIDR block allowed is `/25`. ::: warning CIDR block overlap The network address range should not overlap with the address range of any other networks which you will be peering with. Once a network is created, you will not be able to change the CIDR block. To change the CIDR block, you will need to delete the network and create a new one. ::: ### Database ![Database configuration](../images/cluster-new-db.png) The **Database** section is where you can specify the database settings for the cluster. You will need to select the **Server Version**, and choose if [server-side projections](@server/features/projections/README.md) should be enabled and what level of projections should be enabled. ::: warning Projections impact on performance Both system projections and user-defined projections produce new events. Carefully consider the impact of enabled projections on database performance. Please refer to the [Performance impact](@server/features/projections/README.md#performance-impact) section of the projections documentation to learn more. ::: ### Instance size ![Instance size](../images/cluster-new-size-topology.png) The next section of the form allows choosing the instance size for cluster nodes. Use the provided [instance size guidelines](../../ops/sizing.md) to choose the right size for your cluster. Note that the `F1` size is using burstable VMs, which is not suitable for production use. ::: tip Vertical scaling If you find that your cluster is not performing as expected, you can always resize the cluster instances later. If you create a three-node cluster, a resize is done in a rolling fashion that should take only a few minutes and not impact the availability of the cluster. ::: You will also need to specify the topology of the cluster. We recommend three-node clusters to ensure high availability, but you can also create a single-node cluster for testing or development purposes. ### Storage ![Storage](images/gcp/gcp-new-cluster-storage.png) Now you need to configure the storage for the cluster. For all three providers, only one disk type is available at the moment via the Cloud console. The storage capacity is gigabytes, with 8GiB being the minimum for AWS, and 10GiB for Azure and GCP. Since we allow customers to expand the storage size online without service interruptions, you can start with smaller storage and expand it when you need more capacity. ### Pricing Finally, you will see the estimated monthly price for the selected cluster size and storage capacity. ::: note Network usage Since the network usage is billed based on actual usage, the estimated price will not reflect the full cost of the cluster. ::: ## Provisioning begins When you click on **Create cluster**, the provisioning process starts. You will be redirected to the cluster details page, where you can follow the progress of the provisioning process. As the creation process progresses, you will see the status of the cluster change. ![Cluster provisioning statuses](images/gcp/gcp-cluster-creation-steps.png) If you created a new network, it will be created first. You can see the status of the network creation in the **Networks** view. ![Network status](images/gcp/gcp-network-provisioning.png) Once you see the new cluster's status change to `Ok`, your cluster is ready to use, but you still need to setup a peering link between your GCP Virtual Network and the Kurrent Cloud network. ## Network peering When the cluster provisioning process finishes, you get a new cluster (or single instance), which is connected to the network created in the first step. You won't be able to connect to the cluster since the network is not exposed to the Internet. In order to get access to the clusters in the new Network, you must configure a peering link between your GCP VPC network and the Kurrent Cloud network. Besides services running on compute instances, your GCP VPC network would also be accessible by applications that you may want to connect to the new cloud KurrentDB cluster. For this example, we'll use a VPC network in GCP in the same region (`us-west3`). In Kurrent Cloud console, while in the same project context as the new network and cluster, click on **Networks** under the **Networking** section. Select the network you want to peer with the GCP VPC, and click on the **Peerings** tab in the network details. You should see there are no peerings yet. To begin the peering process, click on the **Initiate peering** button. ![Network status](images/gcp/gcp-network-ready.png) ### GCP VPC details You will need to gather the following information about your GCP VPC network: | Field | Description | |:--------------------|:--------------------------------------| | Peer GCP Project ID | GCP project ID | | Peer Network Name | The VPC name | | GCP region | VPC subnet region, cannot be changed | | Peer routes | One or more VPC subnet address ranges | To find the GCP project ID, in the [Google Cloud Console](https://console.cloud.google.com/), click on the project selector in the top left corner, and select the project that contains the VPC you want to peer with. ![GCP project selector](images/gcp/gcp-project-selector.png) ![GCP project ID](images/gcp/gcp-project-id.png) For our example, here is the complete form: ![GCP peering - complete form](images/gcp/gcp-peering-1.png) ::: note Multiple peer routes are useful in case you want to peer with a subnet that has multiple IP ranges (aliases). A typical example would be if you have a VPC-native GKE cluster, and you need pods in that cluster to work with Kurrent Cloud. Then, you need to add the pod IP range to the peer route in addition to the subnet's primary IP range. ::: ::: warning After you create a peering, you can't change the peer routes. If you would need to modify the routes, delete the peering and create it again with the updated set of peer routes. The limitation applies even in cases where a cloud provider supports dynamic route updates, e.g. in GCP. The reason is that the KurrentDB automation creates firewall rules for the peer routes when the peering is created. The firewall rules are not updated dynamically to prevent security breaches. ::: ### Initiate peering When you click on the **Initiate peering** button, you'll be redirected back to the **Networks** screen, which should now show the new peering resource being provisioned. ![Peering provisioning](images/gcp/gcp-peering-2.png) After a few minutes, the Network's status will change to `Peering initiated`. ![Peering initiated](images/gcp/gcp-peering-3.png) The information on the **Peerings** tab of the **Network details** section provides some essential information to complete the peering process from GCP side. Go back to Google Cloud console and navigate to **VPC network peering** tab of the VPC network you want to peer with, then click **Add Peering**. ![GCP peering list](images/gcp/gcp-peering-4.png) Give the new peering a name and fill out the values using the initiated peering details from the Kurrent Cloud console: | Kurrent Cloud | GCP connection peering | |:------------------|:-----------------------| | Peer Project ID | Project ID | | Peer Network Name | VPC network name | **Important**: under the **Exchange custom routes** section, enable both **Import** and **Export custom routes** options. This will instruct GCP to create routes automatically. Here is what our example GCP peering form would look like: ![GCP peering form](images/gcp/gcp-peering-5.png) Click the **Create** button, which will return you to the **VPC network peering** list. Shortly after, the peering status should change to `Active`. ![Peering active](images/gcp/gcp-peering-6.png) The peering status in Kurrent Cloud console should also change to `Active`. ![Peering active](images/gcp/gcp-peering-finished.png) ::: tip Peering issues You might see the peering request getting stuck. There are several reasons this happen, such as cloud account quota or overlapping CIDR blocks. Check the Event Console in the Cloud console for diagnostic details, and contact support if you need assistance. ::: At this point, you should be able to connect to the KurrentDB cluster in the cloud from any VM or application connected to your GCP VPC network. Depending on your setup, you might already have a connection available from your local machine to the GCP VPC using a site-to-site VPN. If not, ask your operations team about the connection details. ### Next steps You are now ready to start using the new KurrentDB cluster in the cloud. Head over to the [Operations](../../ops/README.md#connecting-to-a-cluster) page to learn how to connect to your cluster. --- --- url: 'https://docs.kurrent.io/cloud/getting-started/public.md' --- # Public Access Clusters With Kurrent Cloud, provisioning KurrentDB clusters with public access is simple. Pre-requisites: * You have an organization registered in Cloud console * You can log in to the Cloud console as admin * Your organization has at least one project * You are the admin of the project Note, see the [Quick Start](../../introduction.md#cloud-quick-start) section for more details on how to configure these items. ## Create a cluster In the Kurrent Cloud console, go to the [project context](../../introduction.md#projects) and switch to **Clusters**. Then, click on the **New cluster** button to begin the cluster creation process. ### Cluster name ![Cluster name](images/new-cluster-name.png) Provide a descriptive name for the cluster in the **Cluster name** field. ### Network ![Create a network](images/public/cluster-new-public-access-network.png) In the **Network** section, if you have not created a network yet, you will see fields for creating a new network. If you have any existing Networks, you will see those listed, as well as the option to create a new Network. When creating a new network for a public access cluster, you will need to provide the following information: * **Network name** is a descriptor to allow you to identify the network in the list of networks. * **Type** should be set to `Public`. * **Cloud provider** should be set to AWS. * **Region** is the AWS region where the cluster will be created. ### IP Access List ![Create an IP Access List](images/public/cluster-new-public-access-ip-access-list.png) If you have not created an IP Access List yet, you will see fields for creating a new IP Access List. All clusters with public access enabled are protected by an IP Access List. This list is used to control access to the cluster from the public Internet. **IP Access List name** is a descriptor to allow you to identify the IP Access List in the list of IP Access Lists. **Addresses** are where you can specify the IP addresses or IP ranges in CIDR notation that are allowed to access the cluster from the public Internet. You can optionally add a comment for each address to help you keep track of what the address is for. You can use the **Add my IP address** button to add your current IP address to the list. If you are not sure about your IP address, you can update the IP Access List later. ::: tip Add my IP address Depending on your network configuration, the **Add my IP address** button may not get the correct IP address. If you are on a public network or a network that you don't use regularly, it would be best to wait and add your IP address later once you are on a network that you will use to connect to the cluster. ::: ### Database The Database section is where you can specify the database settings for the cluster. You will need to select the **Server Version**, and choose whether to start server-side projections by default. ::: warning Projections impact on performance Both system projections and user-defined projections produce new events. Carefully consider the impact of enabled projections on database performance. Please refer to the [Performance impact](@server/features/projections/README.md#performance-impact) section of the projections documentation to learn more. ::: ### Instance size The next section of the form allows choosing the instance size for cluster nodes. Use the provided [instance size guidelines](../ops/sizing.md) to choose the right size for your cluster. Note that the `F1` size is using burstable VMs, which is not suitable for production use. ::: tip Vertical scaling If you find that your cluster is not performing as expected, you can always resize the cluster instances later. If you create a three-node cluster, a resize is done in a rolling fashion that should take only a few minutes and not impact the availability of the cluster. ::: You will also need to specify the topology of the cluster. We recommend three-node clusters to ensure high availability, but you can also create a single-node cluster for testing or development purposes. ### Storage Further, you need to configure the storage for the cluster. For all three providers, only one disk type is available at the moment via the Cloud console. The storage capacity is gigabytes, with 8GiB being the minimum for AWS, and 10GiB for Azure and GCP. Since we allow customers to expand the storage size online without service interruptions, you can start with smaller storage and expand it when you need more capacity. If you are creating an AWS cluster, you will see the option to specify IOPS and throughput. The default values are the defaults for GP3 volumes, so unless you need extra IOPS or throughput, you can leave the defaults. These values can be changed later. ::: note The cloud console only allows for the creation of GP3 clusters, but for backwards compatibility purposes it is still possible to create a cluster with GP2 storage using tools such as the Terraform provider or Kurrent Cloud CLI. GP3 disks are recommended for all new clusters because they offer better performance, more flexibility because IOPS and throughput can be adjusted, and are less expensive than GP2 disks. ::: ### Pricing Finally, you will see the estimated monthly price for the selected cluster size, topology, and storage capacity. ::: note Network usage Since the network usage is billed based on actual usage, the estimated price will not reflect the full cost of the cluster. ::: ## Provisioning Begins When you click on **Create cluster**, the provisioning process starts. The cluster will immediately be visible in the list of clusters. If you have opted to create a Network and/or IP Access List, those resources will be created, followed by the cluster itself. After a few minutes, your new cluster will be ready to use. ## Next steps You are now ready to start using the new Managed KurrentDB cluster in the cloud. Head over to the [Operations](../ops/README.md#connecting-to-a-cluster) page to learn how to connect to your cluster. --- --- url: 'https://docs.kurrent.io/cloud/guides/index.md' --- # Guides We have some guides to help you with specific use cases when using Kurrent Cloud. If you have any suggestions for guides you would like to see, please [let us know](https://www.kurrent.io/contact#contact-form). --- --- url: 'https://docs.kurrent.io/cloud/guides/kubernetes.md' --- # Connect from Kubernetes Below, you can find instructions for connecting workloads running cloud-managed Kubernetes clusters to managed KurrentDB clusters running in Kurrent Cloud. ## AWS Elastic Kubernetes Services In this section, you find instructions on how to set up an AWS Elastic Kubernetes Services (EKS) cluster, so it can connect to a KurrentDB cluster in Kurrent Cloud. As a prerequisite, you have experience with Kubernetes, AWS and networking in Kubernetes, as well as in AWS cloud platform. EKS clusters require at least two subnets, which are connected to internet using an Internet Gateway. Both subnets must have the auto-assign public IP setting enabled, otherwise the node group won't get properly provisioned. Before you provision a cluster in Kurrent Cloud, you need to have a network, to which the cluster nodes will connect. Nodes in the cluster will get IP addresses from the specified network CIDR block. You can find more information about the steps needed for provisioning Kurrent Cloud resources and connecting them to your own AWS account in the [provisioning](../getting-started/private-access/aws.md) section. In this example, we'll use the following network configuration: * Kurrent Cloud AWS network with IP range `172.29.98.0/24` (same as we used in the provisioning guidelines) * VPC in the same region (`eu-central-1`) with IP range `172.16.0.0/16` * Two subnets in the VPC with ranges `172.16.0.0/18` and `172.16.64.0/18`, which are both part of the VPC IP range The AWS VPC has a peering connection established with the Kurrent Cloud network, as described in the provisioning guide. For the peering link, we used the whole VPC IP range `172.16.0.0/16`: ![ESC peering](./images/eks-1.png) Now we can provision the EKS cluster. In the networking configuration section of the new cluster, we need to choose the VPC, which is peered with Kurrent Cloud: ![EKS networking](./images/eks-2.png) After creating the cluster, you need to add the node group, as usual. Each node will get a network interface per subnet of the EKS cluster, so in our case nodes will be attached to two subnets. When all the deployments are completed, and you added the EKS cluster to your local config, you can try deploying an ephemeral workload using the `busybox` container image, so you can test the connectivity: ```bash:no-line-numbers $ kubectl run -i --tty --rm debug --image=busybox --restart=Never -- sh ``` If you run `ifconfig` in the pod, you will see that the pod got an IP address from one of the subnets: ```bash / # ifconfig eth0 Link encap:Ethernet HWaddr 66:93:89:8F:D7:CF inet addr:172.16.101.19 Bcast:0.0.0.0 Mask:255.255.255.255 UP BROADCAST RUNNING MULTICAST MTU:9001 Metric:1 RX packets:12 errors:0 dropped:0 overruns:0 frame:0 TX packets:7 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:1252 (1.2 KiB) TX bytes:640 (640.0 B) ``` Then, it should be possible to ping the managed KurrentDB cluster node from the pod, ensuring that everything works as expected: ```bash / # ping c1ut3oto0aembuk4mi6g.mesdb.eventstore.cloud PING c1ut3oto0aembuk4mi6g.mesdb.eventstore.cloud (172.29.98.112): 56 data bytes 64 bytes from 172.29.98.112: seq=0 ttl=63 time=1.049 ms 64 bytes from 172.29.98.112: seq=1 ttl=63 time=0.716 ms 64 bytes from 172.29.98.112: seq=2 ttl=63 time=0.713 ms ``` At this moment, any workload deployed to the EKS cluster should be able to connect to the Kurrent Cloud managed KurrentDB cluster. ## Google Kubernetes Engine In this section, you find instructions on how to set up a Google Kubernetes Engine (GKE) cluster, so it can connect to a KurrentDB cluster in Kurrent Cloud. As a prerequisite, you have experience with Kubernetes, GKE and networking in Kubernetes as well as in Google Cloud Platform (GCP). Before you provision a cluster in Kurrent Cloud, you need to have a network, to which the cluster nodes will connect. Nodes in the cluster will get IP addresses from the specified network CIDR. In order to be able to work with the cluster, you need to establish a connection between the Kurrent Cloud network and your own VPC Network in Google Cloud. You do it by provisioning a network peering between those two networks. You can find more information about the steps needed for provisioning Kurrent Cloud resources and connecting them to your own GCP project in the [provisioning](../getting-started/private-access/gcp.md) section. ### Planning IP ranges The challenge is to set up IP ranges for both VPCs, the GKE cluster, and the peering in a way that pods running in Kubernetes would be able to reach the KurrentDB cluster in Kurrent Cloud. ![GKE and ESC topology](./images/gke-1.png) When creating the Standard GKE cluster from GCP Console, the Networking section has the *Advanced networking* options subsection. There you find the *Pod address range* setting. It is available for clusters with any type of routing (static routes or VPC-native). ![GKE network settings](./images/gke-2.png) If the *Pod address range* option is left blank, the cluster will get a private IP range for the pods. For example, the cluster with static routing got the `10.120.0.0/14` CIDR block for the pods: ![GKE network settings](./images/gke-3.png) When using VPC-native routing, this new range would be added to the selected VPC network subnet, in addition to the main IP range. The issue that you'll experience when leaving the pod IP range blank and letting the provisioner to define it for you, is that IP range won't be a part of the configured network peering IP range in Kurrent Cloud. For the current example, the `subnet-esc` has the IP range `172.30.0.0/16` and that's what we used when creating the peering as described in our documentation. As the new pod address range (`10.120.0.0/14`) is not part of the IP range known to the peering, those pods won't be able to connect to the KurrentDB cloud cluster. It could be solvable when using the VPC-native routing for the GKE cluster, as the pod IP range would be added to the subnet. However, the currently available Kurrent Cloud version only allows you to have one peering with a given subnet, and the peering has only one remote IP range. It means that we can neither add the pods IP range to the existing peering, nor could we create a new peering with this new range. The issue can be fixed by defining a custom pod address IP range, which is a part of the peering IP range. In our documentation we recommend using your VPN network subnet CIDR block as the *Remote Address* field value when creating the peering. However, you can specify a larger range, which could include both the main subnet range, and the additional range for the GKE pods. ### Example IP ranges Let's have a look at a working configuration. In this example, we use the following resources: * VPC network `vpc-esc` on the customer's side * Subnet `subnet-esc` within the `vpc-esc` network with the primary IP range `172.30.0.0/16` * VPC-native GKE cluster (Standard or Autopilot) with pod address range `172.31.128.0/17` Now, for the Kurrent Cloud network peering, we need to find a range, which covers both the subnet default range, and the additional GKE range. In our case, we could use the `172.30.0.0/15` range as it includes both ranges. The `172.30.0.0/15` netmask can be divided as follows: | Subnet address | Range of addresses | Usable IPs | Hosts | |:----------------|:------------------------------|:------------------------------|:------| | 172.30.0.0/16 | 172.30.0.0 - 172.30.255.255\` | 172.30.0.1 - 172.30.255.254 | 65534 | | 172.31.0.0/17 | 172.31.0.0 - 172.31.127.255 | 172.31.0.1 - 172.31.127.254 | 32766 | | 172.31.128.0/17 | 172.31.128.0 - 172.31.255.255 | 172.31.128.1 - 172.31.255.254 | 32766 | So, here we see that the subnet uses the larger range, and the GKE pod address range uses a smaller range, plus we have one range free for something else. ### GKE networking configuration When provisioning the GKE cluster, we should specify `172.31.128.0/17` as the pod address range in the cluster networking configuration for a new Standard cluster: ![GKE network settings](./images/gke-5.png) Similarly, the range can be specified for a new Autopilot cluster: ![GKE network settings](./images/gke-6.png) When the cluster is deployed, you can see that the range is properly assigned: ![GKE network settings](./images/gke-7.png) When looking at the `subnet-esc`, we can see those ranges shown in the GCP Console: ![GKE subnet](./images/gke-8.png) The network peering resource in Kurrent Cloud in this case looks like this: ![ESC peering](./images/gke-9.png) When creating a peering, you don't have to specify a particular subnet of the VPC network, which you peer with. There's no check if the given IP range actually exists in the peered network. Therefore, we can specify the range we need, even if it doesn't match with the primary range of any subnet. By using the larger range, which covers both the primary and secondary ranges, we made the KurrentDB cluster available for pods in the GKE cluster. ### Ensure the connection To confirm that everything works as expected, you can deploy an ephemeral `busybox` container to the Kubernetes cluster and try reaching out to the KurrentDB cloud cluster: ```bash:no-line-numbers $ kubectl run -i --tty --rm debug --image=busybox --restart=Never -- sh ``` If you don't see a command prompt, try pressing enter. ```bash / # ping c0brj8qrh41g7drr2i4g-0.mesdb.eventstore.cloud PING c0brj8qrh41g7drr2i4g-0.mesdb.eventstore.cloud (172.22.110.29): 56 data bytes 64 bytes from 172.22.110.29: seq=0 ttl=63 time=2.655 ms 64 bytes from 172.22.110.29: seq=1 ttl=63 time=1.234 ms 64 bytes from 172.22.110.29: seq=2 ttl=63 time=1.246 ms ``` Finally, we can deploy applications to the GKE cluster, which can connect to the Kurrent Cloud managed KurrentDB instances. The overall network topology would look like this, when we complement the initial diagram with IP addresses and network masks: ![ESC GKE topology](./images/gke-10.png) ## Azure Kubernetes Services In this section, you find instructions on how to set up an Azure Kubernetes Services (AKS) cluster, so it can connect to a KurrentDB cluster in Kurrent Cloud. As a prerequisite, you have experience with Kubernetes, Azure and networking in Kubernetes as well as in Azure Cloud platform. Before you provision a cluster in Kurrent Cloud, you need to have a network, to which the cluster nodes will connect. Nodes in the cluster will get IP addresses from the specified network CIDR block. You can find more information about the steps needed for provisioning Kurrent Cloud resources and connecting them to your own Azure account in the [provisioning](../getting-started/private-access/azure.md) section. When provisioning a new AKS cluster, you can choose one of the following network configurations: * **kubenet**, which will create a new VNet using default values * **Azure CNI**, which allows you to connect the new cluster to an existing VNet ### AKS with Azure CNI When using the Azure CNI network configuration, you need an existing VNet, or you can configure a new VNet, which will be deployed together with the AKS cluster. In this example, we'll create the VNet before creating an AKS cluster. The VNet will get the `172.16.0.0/15` IP range, and the `default` subnet will get the `172.16.0.0/16` IP range, so you have enough IP space for other purposes. ![Create a VNet](./images/aks-1.png) To be able to work with the cluster, you need to establish a connection between the Kurrent Cloud network and your own Virtual Network in Azure. You do it by provisioning a network peering between those two networks as described in the provisioning guide. When creating the peering link, specify the VNet IP range as the remote address. ![Peering](./images/aks-2.png) Next, when creating the AKS cluster, choose the previously created VNet and the subnet: ![AKS networking](./images/aks-3.png) With Azure CNI, each pod will get an IP address directly from the VNet subnet range, so all the pods will be able to reach the Kurrent Cloud resources using the peering link. When the AKS cluster is provisioned, you can deploy a workload, and it will be able to connect to a managed KurrentDB instance. ### AKS with kubenet When creating an AKS cluster with kubenet network configuration, a new VNet will be created in a separate resource group. It is the same resource group where the Kubernetes node pool instances are provisioned. There are no network settings, which you need to change when creating the cluster, except choosing the kubenet configuration. ![AKS with kubenet](./images/aks-5.png) After all the resources are deployed, you will find a new resource group in the resource groups list. Such a resource group has a name, which starts with `MC`, for example `MC_aks-setup-docs_esc-demo_westeurope`. Uou can then open the new resource group and find the VNet there: ![Resource group](./images/aks-4.png) Open the VNet to find its IP range: ![Default VNet](./images/aks-6.png) Use this address space to create a new peering link from Kurrent Cloud to the AKS VNet. When the peering is provisioned, it will look like this: ![Peering](./images/aks-7.png) When the peering becomes active, you can deploy a workload to the AKS cluster, and it should be able to reach a managed KurrentDB instance via the peering link. --- --- url: 'https://docs.kurrent.io/cloud/guides/migration.md' --- # Migrating data When replacing a self-hosted KurrentDB cluster with the managed cluster in Kurrent Cloud, you might need to migrate the data, so your workloads can switch to the cloud cluster. Currently, the only way to migrate data to the cloud cluster or instance is live migration by replication. Live migration is done by reading events from the source database (from `$all`) using a client protocol (TCP or gRPC) and then writing those events to the target database. ## Limitations Consider the following limitations of live migration to understand if it will work for you. ### Write performance If the target database must get events in exactly the same order as they are in the source database, it's impossible to use concurrent writers. Therefore, the speed of replication will be limited by how much time it takes to append one event to the cloud cluster. For example, if it takes 5 ms to append one event, replicating one million events will take about an hour. In case your system only requires events to be ordered within a stream, and you have a lot of streams; it is possible to use concurrent writers. As those writers get events partitioned by stream name, the events order in each stream will be kept, but the global order will not. The advantage of using concurrent partitioned writes is performance increase. For example, six writers on a C4-sized instance would give you over 1000 events per second replication speed. In that case, in one hour you can replicate four million events, not one. ### Write load on the source If the source cluster keeps getting new events appended to its database, ensure that the number of events appended to the source database is significantly less than the number of replicated events for a given time period. The goal there is to ensure that the replication process will ever finish. For example, when you see one million new events written to the source cluster per hour, and you observe one million events being replicated per hour, the replication will never finish. ### System metadata When reading events from KurrentDB, you get several system metadata properties in the event structure on the client side: * Event number (position of the event in the stream) * Created date (timestamp when the event was appended to the log) * Commit position (physical event position in the global log) The event number in the target database will start from zero, although it could be any number in the source database if the stream was ever truncated or deleted. As all the events written to the target database will be "new", the *Created date* timestamp will be set to the moment when the event was replicated. Due to the fact that in the source database the global log has gaps caused by deleted or truncated streams, as well as expired events, the commit position in the target database will not be the same for all events. ## Effect on workloads In addition to the points mentioned in the previous section, keep in mind that changes in event number and commit position will affect your subscriptions. For catch-up subscriptions, you will have to ensure to provide new checkpoints when moving those workloads to the replicated database. For persistent subscriptions, you'd need to re-create them in the replicated database, with start position that corresponds with the last known persistent subscription checkpoint in the source database. The same applies for custom JavaScript projections, which emit new events, except of those that emit links. Links will not be replicated, as all the system events get filtered out (except stream metadata events). Therefore, all the links would normally be recreated. For example, if you use a category projection and `$ce` streams, those streams will be re-created by the system projection in the target database. In case you have custom projections, which emit links, those projections need to be recreated and started in the target database, so they can recreate all the links. ## Executing the migration Kurrent provides a tool that allows you to replicate events between two clusters or instances. The tool is called Kurrent Replicator, and it has its own [documentation website](https://replicator.eventstore.org). ::: warning Kurrent Replicator is an Open Source Software, provided as-is, without warranty, and not covered by the Kurrent support contract that you might have. ::: ### Deployment You need to run the replication tool on your own infrastructure. The primary condition is that the replicator must be able to reach both source and target KurrentDB clusters. For example, if you replicate from a self-hosted cluster in AWS to Kurrent Cloud in AWS, you'd need to peer between the VPC of the self-hosted cluster and the Kurrent Cloud network. We provide [detailed instructions](https://replicator.eventstore.org/docs/deployment/) about running the replicator in Kubernetes and using Docker Compose. For the cloud migration scenario, the simplest case that involves no filtering (except scavenge) and transformations, you can use the following configuration: ```yaml replicator: reader: connectionString: ConnectTo=tcp://admin:changeit@my-instance.acme.company:1113; HeartBeatTimeout=500; UseSslConnection=false; protocol: tcp sink: connectionString: kurrentdb+discover://username:password@clusterid.mesdb.eventstore.cloud:2113 protocol: grpc partitionCount: 1 bufferSize: 1000 scavenge: false transform: null filters: [] checkpoint: path: "./checkpoint" ``` As the replication process runs continuously until you stop it, you can test the source cluster data and gradually move read-only workloads to it, like subscriptions. When you confirm that everything looks fine from the target database, you can move all the other workloads to the new cluster. ### Example scenario Here are the steps, which you can perform to migrate in one go: * Configure and deploy ES Replicator * Wait until it gets in to the restart loop after all the historical events are replicated * Stop all the workloads, which write to the source database * Ensure all the remaining events are replicated, then stop the replicator * Ensure that all your subscriptions processed all the events in the source database * Record all the necessary checkpoints, or have the ability to subscribe from *now* in your subscriptions * Move workloads with subscriptions to the target cluster * Move workloads that append events to the target cluster Another option is to move the subscriptions first: * Configure and deploy Kurrent Replicator * Wait until it gets in to the restart loop after all the historical events are replicated * Stop all the workloads, which write to the source database * Stop the subscription workload, and find the corresponding checkpoint value in the target database (stream position or global position in `$all`) * Migrate the subscription workload to the target cluster, using the new checkpoint As the replication process will continue endlessly, you can move some of the subscriptions first, ensure that everything works as expected, then continue by moving more subscriptions as well as workloads, which append events. ## More information Find out more about replicator features, limitations, as well as deployment guidelines [in the documentation](https://replicator.eventstore.org). If you experience an issue when using Replicator, or you'd like to suggest a new feature, please open an issue in the [GitHub project](https://github.com/kurrent-io/replicator). --- --- url: 'https://docs.kurrent.io/cloud/guides/tailscale.md' --- # Connect with Tailscale ## What is Tailscale? [Tailscale](https://tailscale.com) is a commercial product built on top of WireGuard®. It allows you to set up a private tunnel VPN in a mesh-style network. In addition to direct connection, Tailscale also has the subnet routing feature using a gateway machine, which should be connected to the target network. You can use the Solo plan with Tailscale free of charge. ## Get started First, set up a Tailscale account, then install their client software on your machine. The client will ask you to log in after the installation, and then give you your first machine connected to your private VPN. Follow the [installation instructions](https://tailscale.com/kb/1017/install) to get started. ## Provision the cloud VM Next, you need to create a small VM in the cloud, connected to the VPC with the Kurrent Cloud. You can choose the smallest available instance size, like `t4g.nano` in AWS, `f1.micro` in GCP, or `Standard B1ls` in Azure. For this guide we use Ubuntu 20.04 LTS image (18.04 LTS in Azure). When creating the VM, make sure you: * Connect the default network interface to the VPC peered with Kurrent Cloud * Enable public IP if you want to connect to the VM from your local machine * GCP: enable *IP Forwarding* on the default NIC * AWS: disable *Source / destination checking* For existing VMs, you can enable IP forwarding too. ::: tabs#cloud @tab AWS Select the EC2 instance in the list of instances and in the `Actions` menu choose `Networking` and then `Change source/destination check`. Ensure that the `Stop` checkbox is *enabled*: ![AWS enable ip forward](./images/aws-ip-forward.png) @tab GCP On GCP you can enable IP Forward only when creating the VM instance. On the new VM instance page and scroll down to the `Management, security, disks, networking, sole tenancy` section, expand it, find the `Network interfaces` section and click on the pen icon. There, set the `IP forwarding` to `On`: ![GCP enable ip forward](./images/gcp-ip-forward.png) ::: Remember to create the VM instance in the same region as the VPC, which is peered with Kurrent Cloud. ## Set up Tailscale subnet routing When you get the cloud VM instance running, connect to it using SSH. The easiest way is to use the cloud browser console. After logging in, install the Tailscale client for the Linux distribution used for the cloud VM, following the [Tailscale guidelines](https://tailscale.com/kb/1017/install). Here you can also find required steps for [Ubuntu 20.04 LTS (focal)](https://tailscale.com/download/linux/ubuntu-2004) and [Ubuntu 18.04 LTS (bionic)](https://tailscale.com/download/linux/ubuntu-1804) distributions. When the initial steps are completed, you should be able to ping the cloud VM using its internal IP address from your local machine. Next, visit the Kurrent Cloud console and open the peering page. There you will find the peering you created when following the provisioning guidelines. Write down the details from the `Local Address` and `Remote Address` fields. For this example, we will use the following peering details: ![Peering page example](./images/peering-example.png) With all the necessary details collected, follow these steps on the cloud VM instance: *Enable IP forwarding on the machine:* ```bash echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf sudo sysctl -p /etc/sysctl.conf ``` *Restart Tailscale client with subnet routing:* ```bash sudo tailscale up --advertise-routes=10.164.0.0/20,172.22.101.0/24 --accept-routes ``` In the example above we used both address spaces of both sides of the peering as the `advertise-routes` parameter values (comma-separated). Next, visit your Tailscale Admin Console, find the cloud VM in the list and use the three dots popup menu to enable subnet routing and disable key expiry. Now, visit the Kurrent Cloud console, switch to the Clusters page and choose the KurrentDB cluster. In the cluster details select the `Addresses` tab and click on the UI link. You should then get the KurrentDB Admin UI opened in your local machine browser. This is how the network looks like when using Tailscale: ![ES\_Cloud\_Networking\_tailsacle](./images/es-cloud-networking-tailscale.svg) --- --- url: 'https://docs.kurrent.io/cloud/integrations/index.md' --- # Integrations Kurrent Cloud offers integrations between internal sources such as cluster health, issue detection, notifications events, KurrentDB logs, KurrentDB metrics and sinks which include external services such as Slack and Amazon CloudWatch. ## Integration sources "Sources" are driven by events or other mechanism inside the Kurrent Cloud. Currently, supported sources include: * Issues - issues represent a potentially problematic condition detected inside a cluster or other Kurrent Cloud resource. Issues consist of multiple "open" events and a single "closed" event and have different levels of severity. * Logs - these are the logs generated by the DB itself. * Metrics - these are the metrics generated by both Kurrent Cloud issue detection processes running on each cluster node and KurrentDB itself. * Notifications - notifications are noteworthy events detected within Kurrent Cloud resources or the backend. For example notifications may be emitted when a cluster fails to provision. ## Issues Issues represent possibly problematic states detected within the Kurrent Cloud. Below, you can find several issue examples. ::: note These examples are a subset of issues created by the system. The exact details of why issues are created are subject to change, but the cause of the issue and steps to resolve it will be clear in the messages associated with the related events. ::: ### Core load count For each node of a cluster, the core load average is measured and divided by the number of physical cores on the node. If the result exceeds 2.0 an issue is opened. This issue is closed when the result consistently dips under 2.0. If this happens consider increasing the size of the instance type for the cluster. ### Disk usage For each node of a cluster, the disk usage is measured several times a minute. If it starts to consistently exceed 80% an issue is opened. The issue is closed when the usage drops below 80%. If this happens consider either removing data, running scavenge or increasing the disk size for the cluster. ### Memory usage For each node of a cluster, the memory usage is measured several times a minute. If it exceeds 90% an issue is opened. The issue is closed when memory usage consistently drops below 90%. If this happens consider increasing the size of the instance type for the cluster. ### Cluster consensus Every node on a cluster has it's gossip status queried twice each minute. An issue is opened if either the query fails or if the reported gossip state for each node is not identical on a multi-node cluster. The issue closes when the gossip status again returns expected values. ## Notifications Notifications represent noteworthy events which occur within the Kurrent Cloud. Below you can find notifications examples. ::: note The following represent a subset of events which can lead to notifications. ::: ### Cluster provisioning failure If, for some reason, the instances backing a cluster fail to provision the resource is marked as defunct by the API and a notification is sent with the message `Cluster instances failed to provision`. ### Volume expansion failure If the volume fails to expand while expanding an instances size a notification event is created with the message `Cluster volumes failed to provision`. ## Logs and metrics - BETA KurrentDB logs are sent in the form of JSON objects. Each object's `message` field contains the original KurrentDB log message. Metrics are name / value pairs from the KurrentDB service, as well as system level metrics such as core load average, memory, and disk usage. ::: note Beta features Logs and metrics are currently in beta. The data provided and/or structure of the output may change before these features are fully released for production. ::: ## Integration sinks "Sinks" are services outside the Kurrent Cloud which events from sources can be forwarded to. * [AwsCloudWatchLogs](cloudwatch.md#logs-sink) - **BETA** Amazon CloudWatch allows you to track metrics, display them with create custom dashboards, and create alarms from them. * [AwsCloudWatchMetrics](cloudwatch.md#metrics-sink) - **BETA** Amazon CloudWatch allows for logs to be uploaded, viewed and searched, and consumed by other AWS services. * [Opsgenie](opsgenie.md) - Opsgenie is an alerting and incidence response tool. It is possible to set up integrations to create Opsgenie alerts when cluster health issues are detected. * [Slack](slack.md) - Slack is a communication platform. It is possible to set up integrations which send Slack messages when issues and notifications are created or updated. --- --- url: 'https://docs.kurrent.io/cloud/integrations/cloudwatch.md' --- # Amazon CloudWatch Kurrent Cloud supports integration with Amazon CloudWatch for both logs and metrics. Below you'll find instructions on how to set up both. ::: info The Amazon CloudWatch integration is currently in closed beta and may change over time. To access it, please contact support. ::: ## Logs sink Amazon CloudWatch is a monitoring service. The "logs" portion of its functionality is separated into it's own integration in Kurrent Cloud for ease of use. You can cause all the logs generated by each KurrentDB running in your clusters by creating a custom Amazon CloudWatch log group, an IAM user, and setting up an integration as described below. ::: note If you're using Terraform, instructions on how to perform the steps below can be found [here](https://github.com/kurrent-io/terraform-provider-eventstorecloud/blob/trunk/docs/resources/integration_awscloudwatch_logs.md#example-usage). ::: ### Create a CloudWatch Log group In the AWS console, go to the `CloudWatch` page. Make sure the region you'd like to use is selected in the upper right corner (this will ideally be the same region used by the Kurrent Cloud clusters you'll be using as the source). In the left sidebar, expand `Logs` and click `Log groups`. Then click on the button labeled `Create log group`. Enter a log group. If you have multiple projects it could be a good idea to include it in the name, such as `EventStoreLogs-Production`. Select a retention policy depending on your needs, any tags if necessary, and then click create. Now find the new log group in the list, and click on it. The next page will show you it's details, including the ARN. Copy it for the next step. ### Create IAM credentials ::: note You should create dedicated credentials for use with the Kurrent Cloud integration that only have access to the CloudWatch Log group, as shown here. ::: In the AWS console, go to the `IAM` homepage. In the sidebar, click on `Users`, and then on the button labeled `Add users`. Enter an appropriate name, select the AWS credential type `Access key`, and then click the next button. Under set permissions, select "Attach existing policies directly". Don't click on "create policy". Now click the next button until you get to the page titled "Review." Give the policy an appropriate name and click "Create user". At this point copy the access key ID and secret access key as you'll need them later. Then click "close" You should now see the users list again. Click on the newly created user. Under the `Permissions` table click "Add inline policy". On the "Create Policy" screen, click the tab labeled `JSON`, and then enter the following: ```json { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:DescribeLogGroups" ], "Effect": "Allow", "Resource": "${DescribeARN}" }, { "Action": [ "logs:CreateLogStream", "logs:DescribeLogStreams", "logs:PutLogEvents" ], "Effect": "Allow", "Resource": "${ARN}:*" } ] } ``` Now make two replacements: * Replace `${ARN}` with the ARN you copied when you create the CloudWatch Log group in the previous step * For `${DescribeARN}`, take the ARN and remove the last part before the asterisk. So for example, if the ARN is `arn:aws:logs:us-west-2:123456789012:log-group:EventStoreLogs-Production:*"` you'll want this value to be `arn:aws:logs:us-west-2:123456789012:log-group:*"`. Click on Review policy. As you can see, the recommended policy will ultimately give the integration permission to describe the log groups in your account and write to the newly create log group. Give the policy some appropriate name, and click on "Create policy". ### Add a new integration 1. In the Kurrent Cloud console, select an organization and then a project. 2. Once viewing a project, you should see `Integrations` under the heading `Project` in the sidebar to the left. Click it. 3. Click `New Integration`. Enter a name that will make it easier to find later, and then select Logs. Next select the Sink `AwsCloudWatchLogs` as in the screen below. 4. Under `Configuration`, by `Group Name` enter the name of the CloudWatch group you created earlier (for example, `EventStoreLogs-Production`). 5. Under `Configuration`, by `Aws Region` enter the AWS region of the CloudWatch group you created earlier. 6. Under `Configuration`, by `Clusters` select one or more clusters whose logs you want sent to the CloudWatch group. 7. Under `Credentials` enter Access Key Id which was shown when you created the IAM user earlier. 8. Under `Credentials` enter Secret Access Key which was shown when you created the IAM user earlier. ### Testing the integration Log integration with Kurrent Cloud will take a few minutes to be fully active. If you're making sure the integration is set up correctly it's a good idea to write a few test events to the cluster. Clusters which are seeing no activity (such as dev or test clusters) will sometimes emit no logs at all. Additionally, the Kurrent Cloud lacks a way to assert if the IAM credentials are valid. If no new logs appears in your Amazon CloudWatch group, please double-check that the credentials given to the integration are correct and that the IAM user has the appropriate permissions. ## Metrics sink Amazon CloudWatch is a monitoring service. The "metrics" portion of its functionality is separated into its own integration in Kurrent Cloud for ease of use. You can cause all the metrics generated by each KurrentDB running in your clusters by creating a custom IAM user and setting up an integration as described below. :::note If you're using Terraform, instructions on how to perform the steps below can be found [here](https://github.com/kurrent-io/terraform-provider-eventstorecloud/blob/trunk/docs/resources/integration_awscloudwatch_metrics.md#example-usage). ::: ### Create new IAM resources ::: note You should create credentials especially for use with the Kurrent Cloud integration which only have permission to perform the `cloudwatch:PutMetricData` action on a small set of namespaces, as shown here. ::: First, decide what namespace you want your KurrentDB metrics to appear under. A decent choice could be simply "EventStoreCloud". In the AWS console, go to the `IAM` homepage. In the sidebar, click on `Users`, and then on the button labeled `Add users`. Enter an appropriate name, select the AWS credential type `Access key`, and then click the next button. Under set permissions, select "Attach existing policies directly". Don't click on "create policy". Now click the next button until you get to the page titled "Review." Give the policy an appropriate name and click "Create user". At this point copy the access key ID and secret access key as you'll need them later. Then click "close" You should now see the users list again. Click on the newly created user. Under the `Permissions` table click "Add inline policy". On the "Create Policy" screen, click the tab labeled `JSON`, and then enter the following: ```json { "Version": "2012-10-17", "Statement": [ { "Action": [ "cloudwatch:PutMetricData" ], "Effect": "Allow", "Resource": "*", "Condition": { "ForAnyValue:StringEqualsIgnoreCase": { "cloudwatch:namespace": [ "EventStoreCloud", "EventStoreCloud/KurrentDB", "EventStoreCloud/host" ] } } } ] } ``` If you want to use a namespace other than `EventStoreCloud`, simply replace it in the policy above. Click on Review policy. Give the policy some appropriate name, and click on "Create policy". ### Add a new integration 1. In the Kurrent Cloud console, select an organization and then a project. 2. Once viewing a project, you should see `Integrations` under the heading `Project` in the sidebar to the left. Click it. 3. Click `New Integration`. Enter a name that will make it easier to find later, and then select Logs. Next select the Sink `AwsCloudWatchMetric` as in the screen below. 4. Under `Configuration`, by `Namespace` enter the namespace you decided on earlier (for example, `EventStoreLogs-Production`). 5. Under `Configuration`, by `Aws Region` enter the AWS region that matches the clusters you'll be pulling from. 6. Under `Configuration`, by `Clusters` select one or more clusters whose metrics you want sent to CloudWatch. 7. Under `Credentials` enter Access Key Id which was shown when you created the IAM user earlier. 8. Under `Credentials` enter Secret Access Key which was shown when you created the IAM user earlier. ### Testing the integration Metrics integration with Kurrent Cloud will take a few minutes to be fully activated. Metrics will typically appear after that without any action on your part. Additionally, the Kurrent Cloud lacks a way to assert if the IAM credentials are valid. If no metrics appear under the given namespace in CloudWatch, please double-check that the credentials given to the integration are correct and that the IAM user has the appropriate permissions. --- --- url: 'https://docs.kurrent.io/cloud/integrations/opsgenie.md' --- # Opsgenie Kurrent Cloud platform is using [Opsgenie] for its alerting system. Our minimal configuration requires an API key. For simplicity’ sake, we recommend the API key to belong to a responder team. These instructions assume you are starting from scratch and don't have a team set up yet. We also assume that you are currently on the landing page after logging in [Opsgenie]. ## Create a team in Opsgenie Log in to your Opsgenie instance, then complete the following steps to set up a new team. Skip this step if you already have a team. 1. Click on the `Teams` tab up top. 2. Click on the `Add team` top left. 3. A popup should show up. Enter your team info like its name and members. Keep in mind that the team will be considered as the responder team in Kurrent Cloud. 4. Once you confirm your new team creation, you should be redirected to your new team dashboard page. ## Generate the team API key Follow these steps to generate an API key for the team, which should be alerted when issues happen in Kurrent Cloud. 1. Click on the `Teams` tab up top. 2. Select your team in the team table. 3. By selecting your team, you should be redirected to your team dashboard. 4. Click on `Integrations`, located in the left sidebar. 5. Click on the `Add integration` button. 6. In the integration list, click on `API` then the `Add` button. 7. By default, the form should be already pre-filled. Make sure that `Read Access`, `Create and Update Access`, `Delete Access` and `Enabled` are checked. 8. Click on `Save Integration` at the bottom. 9. You can get your API key that should be located just below the `Name` property. ## Complete the integration 1. In the Kurrent Cloud console, select an organization and then a project. 2. Once viewing a project, you should see `Integrations` under the heading `Project` in the sidebar to the left. Click it. 3. Click `New Integration`. Enter a name that will make it easier to find later, and then select Issues or Notifications. 4. Select the Opsgenie sink and put the API Key in the form, then click on the `Create integration` button. If all the details were entered correctly, the new integration should be set up. See the example on the screen below. ![Opsgenie details](./images/opsgenie-details.png) [Opsgenie]: https://www.atlassian.com/software/opsgenie --- --- url: 'https://docs.kurrent.io/cloud/integrations/slack.md' --- # Slack Slack is a business communications platform. You can be notified of new events and notifications in your Slack workspace by creating a Slack App and setting up an integration as described below. ## Setup Slack Before adding a Slack integration, you need to create a Slack App and obtain a token through slack.com. 1. Go to `https://api.slack.com/apps` and click `Create New App`. Give it any name you wish and select the target workspace, then click `Create App`. 2. On the next screen, click on `OAuth & Permissions`. Under `Scopes`, `Bot Token Scopes` and add a scope of `chat:write`. 3. At the top of the page, click `Install to Workspace`. 4. After installing the app, the `OAuth & Permissions` page will have a `Bot User OAuth Token` shown at the top. Copy it for the next step. 5. Finally, you'll need to invite the Slack app user to the channel you want it to chat with. In Slack, go to the appropriate channel and write the following: ``` /invite @ ``` ## Add a new integration 1. In the Kurrent Cloud console, select an organization and then a project. 2. Once viewing a project, you should see `Integrations` under the heading `Project` in the sidebar to the left. Click it. 3. Click `New Integration`. Enter a name that will make it easier to find later, and then select Issues or Notifications. Then select the Sink `Slack` as in the screen below. 4. Under `Configuration`, next to `Channel ID`, enter the channel you want the slack bot to communicate with (this must be the same as the place you invited the bot earlier). Remember to start the channel ID with a hash sign if appropriate. 5. In the box next to `Token` enter the OAuth token you copied from Slack's website. 6. Finally, click "Create Integration." 7. Now back at the "Integrations" page, click on the row with recently created integration. In the bottom pane you should see a button marked `Test integration` as in the screen below. Click it. 8. If your Slack App was created correctly you should see a test message in the channel you selected. 9. If you get an error message double check that you copied the OAuth token and Channel ID into the integration correctly. You may also wish to review the previous step to ensure you gave the app appropriate permissions. You can edit the integration by clicking on the pencil icon in its row. 10. If you see a Slack message, you can rest you will receive events from the configured source when they occur. ## Details If the source is `Notifications`, a single message will be sent to the configured slack bot for each notification. If the source is `Issues`, a single message will be sent for each open issue and given a red bar. Open issues receive new events continuously, however the original Slack message will only be updated to reflect the latest event every five minutes. When the issue is closed, the original message will be changed to reflect this, and the bar will be changed to green. In order to send messages to multiple channels, create more integrations in Kurrent Cloud using the same credentials. --- --- url: 'https://docs.kurrent.io/cloud/introduction.md' --- # Introduction ## What is Kurrent Cloud? Kurrent Cloud enables seamless deployment of managed KurrentDB clusters with major cloud providers. ::: tip Learn more Not using Kurrent Cloud and want to learn more? Find more information and sign up links [on our website](https://www.kurrent.io/kurrent-cloud). ::: As a customer of Kurrent Cloud, you get access to the [Cloud console](https://console.kurrent.cloud), where you can provision and manage KurrentDB clusters, backup and restore your data, and establish the connection between Kurrent Cloud networks and your own cloud infrastructure. Kurrent Cloud operations can be automated using the [Terraform provider](https://github.com/kurrent-io/terraform-provider-kurrentcloud), [Pulumi provider](https://www.pulumi.com/registry/packages/eventstorecloud/), and the [CLI tool](https://github.com/kurrent-io/esc) which is built on top of the same API. ## Cloud quick start Follow the steps in this guide to access the Kurrent Cloud console, provision your first cluster, and connect it to your cloud infrastructure. Kurrent offers video demonstrations for running Kurrent Cloud on [AWS](https://www.youtube.com/watch?v=UeYMA28fOlE) and [Azure](https://www.youtube.com/watch?v=D42c7omFiXA) that adhere closely to the following instructions. ### Get an account KurrentDB offers a single sign-on (SSO) for our customers. Using a single account, you can access various free and paid services. To get an account, proceed to the [Cloud console](https://console.kurrent.cloud/), where you'll see the login screen. There, you'll have the option to sign up. ::: tip If you already have an account at our community forum (Discuss), you can use it to log in to Kurrent Cloud. ::: To finalize the sign-up process, you must confirm your email address by clicking the link in the confirmation email. It might happen that the confirmation email doesn't get through, please check the junk email folder in your email client as you might find it there. ### Login to the Kurrent Cloud console With the new account, you can log in to the [Cloud console](https://console.kurrent.cloud). In the console, you first get to the list of organizations you have access to. ![Cloud organisations](images/intro/cloud-console-orgs.png) You start with an empty list, so you must add an organization. ::: tip The Cloud console has the concept of Context. You'll typically be in the organization or project context. ::: ### Organizations With your account, you might have more than one organization. Each organization has its own billing account and is, therefore, invoiced separately for all the resources within the organization. For example, if you have several customers and want to have a separate KurrentDB cluster for each customer and also be able to bill the customer for the cloud resources, you can separate each customer into its own organization. When you click on an organization in the list, you get to the projects screen, where you can see all the projects for the selected organization. ![Projects within the organisation](images/intro/cloud-org-projects.png) Within the organization's scope, you also have the list of users and roles, billing information, alerts, etc. You can always switch to another organization by clicking on the selected organization name, either selecting it from the list or clicking on `All organizations` to return to the list. ### Access control Each organization has its own access control, which includes the list of users who have access to the organization, groups, roles, policies, and identity providers. When you create an organization, you become its admin by default. To invite more people, click on the `Access control` menu and switch to `Invitations`. You will see the `Invite member` button, which opens the invite screen. You must enter the new member's email address and the group to which the invited member will be added when they accept the invite. When you invite someone, the invitation remains inactive in the `Invitations` list until the invite is accepted. If the invitee accidentally removes it from their inbox, it can be resent. Groups allow you to fine-tune access so that not every organization member is the admin. The `Organization admins` group is automatically created when you create a new organization, and members of this group have full access to the organization. Each project also gets its own `Project admins` group. ### Projects An organization can have multiple projects. Projects serve as a logical grouping of cloud resources. For example, you might create separate projects per environment (test, staging, production) or per application, where each application might have one cluster per environment. To create a new project, click the `New project` button. Then, enter the project name and add administrators from the list of organization users. Each project also gets its own `Project admins` group, and all the users you add as administrators when creating the new project will become members of this group. You get to the project context screen when you click on a project in the project list. ![Project context](images/intro/cloud-project-screen.png) Within the project context, you can manage project clusters, backups, networks, etc. ### Provision a cluster You are now ready to start provisioning cloud resources with Kurrent Cloud. Please proceed to the [Getting Started guide](getting-started/README.md) to learn how to provision your first cluster. --- --- url: 'https://docs.kurrent.io/cloud/networking/index.md' --- # Networking Each major cloud provider implements the concept of a Virtual Private Cloud (VPC). AWS and GCP have VPCs, while Azure has Azure Virtual Network (VNet). A VPC gives you as the cloud user an isolated private network, to which you attach cloud compute resources (virtual machines, container orchestrators, etc) or managed resources, like database servers. VPCs are always in private network address spaces and therefore not directly accessible from the outside of the VPC without additional configuration and infrastructure. Kurrent Cloud Networks are project level resources. When you create a network in Kurrent Cloud, a VPC/VNet is created in the cloud provider's region requested. All Kurrent Cloud Networks are created with at least three availability zones. This is to ensure that the cluster can withstand the loss of a single availability zone with minimal impact to the cluster's availability. If a single node is unable to communicate with the other nodes in the cluster, once connectivity is restored, that node will catch up on the events that were missed while the node was offline and resume normal operation as a follower node. When you create a KurrentDB Managed Cluster, you choose to create a cluster on an existing Network or to create a new Network. Once the cluster is created, you cannot change the Network of the cluster. If you need to change the Network of a cluster, you can create a backup of the cluster and restore that backup to a new cluster on the desired network. When this is done, a new cluster ID is generated and any existing connection strings will need to be updated to use the new cluster address. ::: tip Network Isolation Even though multiple clusters can share the same Network, each cluster is isolated from other clusters using the cloud provider's network security groups/firewall rules. This means that you can create multiple clusters on the same network without risk of a cluster being used as a way to gain unauthorized access to another cluster's data. ::: ## Network Types Kurrent Cloud supports two types of networks: * **Private Network**: A private network is a network have no inbound access from the Internet. Clusters on Private Networks are only accessible from a VPC/VNet peered with the Kurrent Cloud Private Network. * **Public Network**: A public network is a network that is accessible from the public internet. Clusters on Public Networks are protected by an IP Access List. ## Network Costs Kurrent Cloud charges for networks based on the cloud provider and the region of the network, as well as the type of connection. Private Networks have a higher base cost than Public Networks because they require additional resources compared to Public Networks. However, Private Networks also have a lower cost per GB of data transfer. ### Network Transfer Usage Kurrent Cloud charges for network transfer usage based on the cloud provider and region, as well as the source and destination of the traffic. Some cloud providers charge for inbound and outbound traffic across availability zones, while others do not. For three-node clusters, individual nodes are created in different availability zones, so in addition to usage from client connections, there is also usage between the cluster nodes that traverses availability zones. When using peering links with Private Networks, when both sides of the peering link are in the same region, the traffic is typically charged the same as that for traffic across availability zones. When using cross-region peering, traffic is charged based on the source and destination regions of the traffic. For Public Networks, data transfer between Kurrent Cloud Networks and the public internet is charged only for egress traffic. If the provider charges for cross availability zone traffic, there will also be usage charges for three-node clusters. --- --- url: 'https://docs.kurrent.io/cloud/networking/private-network.md' --- # Private Networks When you create a KurrentDB cluster with private access, that cluster is only accessible from the VPC/VNet that is peered with the Kurrent Cloud Private Network. ## Cloud network peering For all of the cloud providers supported by Kurrent Cloud, you can peer a Private Network with one or more VPC/VNets in the same or different region. This allows you a great deal of flexibility in how you can access your clusters. When you use cross-region peering, you will see increased latency for your connections to the cluster, and there are increased costs for data transfer depending on the source and destination regions of the traffic. When possible, we recommend keeping peerings within the same region. ### Security for peering links It is important to understand that peering links can allow bi-directional traffic from either side of the peering link. While security is a top priority for Kurrent Cloud and we do everything we can to ensure the security of the compute instances, always follow best practices to limit what traffic is allowed to transit the peering link into your VPCs/VNets. Unless you are using the [Connectors](/server/v24.10/features/connectors/README.md) feature, due diligence should be taken to ensure inbound traffic originating from the Kurrent Cloud Private Network is denied. See your cloud provider's documentation to learn how to configure security groups, firewall rules, etc. to restrict network traffic from peered VPC/VNets. :::tip Connectors With the release of KurrentDB 24.10, we have added the [Connectors](/server/v24.10/features/connectors/README.md) feature. Connectors run on the server-side and use a catch-up subscription to receive events, filter or transform them, and push them to an external system. This feature is a great way to extend the capabilities of your KurrentDB cluster, but it also means that the KurrentDB cluster nodes will be connecting to external systems. If you are using Connectors to push events to a system that is on the peered VPC/VNet, you will need to ensure that your network configuration allows ingress traffic from the KurrentDB cluster nodes. ::: :::note Alternative Private Network Connectivity AWS PrivateLink, GCP Private Service Connect, and Azure Private Link provide private connectivity to third party services from within a VPC/VNet without the need for establishing peering links. We plan to add support for these services in the near future. ::: ## End-user connectivity ### VPN connection from on-premises Normally, organizations build internal practices in regard to VPCs isolation and access level. In particular, you would expect that some or all VPCs are available for direct access from local networks on-premises or via VPN connections for developers to be able to access cloud resources. Cloud providers let their customers to connect on-premises networks to VPCs using site-to-site VPN connections. Cloud customers often set up VPN gateways in their VPCs to allow point-to-site VPN connections for their remote users. Kurrent Cloud deploys KurrentDB clusters on a project-level VPC (network). By peering that network with your own VPC at the same cloud provider, you get access to the KurrentDB cluster provisioned and managed by Kurrent Cloud. Normally, your Ops engineers would be able to configure the routing and allow connecting to KurrentDB clusters in the cloud. #### Note for Azure Azure has a property for peering links called [Allow gateway transit](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-peering-overview#gateways-and-on-premises-connectivity). This property allows the peered virtual network to use the virtual network gateway of the peered virtual network. When Kurrent Cloud creates an Azure peering link, this property is disabled. As the peering link is created from Kurrent Cloud side, there is no way for the option to be changed on the customer's side of the peering link. This means that when connecting to a Private KurrentDB cluster from a Azure Virtual Network, connections must originate from the peered virtual network. The two most common use cases that are impacted by this limitation are: 1. Users who are connecting to a Azure Virtual Network using a point-to-site or site-to-site VPN connection will not be able to connect directly to the managed KurrentDB instance. 2. Customers who are utilizing a hub and spoke network architecture will not be able to connect directly to the managed KurrentDB instance. We will be addressing this limitation in the future, and we have recommendations for how to work around this limitation. --- --- url: 'https://docs.kurrent.io/cloud/networking/public-network.md' --- # Public Networks Public Access Clusters are provisioned on a **Public Network** and have a public IP address assigned to each cluster node, with access to the cluster restricted to the IP addresses/ranges specified in an **IP Access List** assigned to each cluster. Clusters on the same network are isolated from each other, so you can create multiple clusters on the same public network without worrying about unauthorized access from one cluster and another. We previously discussed how to create a Public Managed Cluster in the [Public Access Getting Started](../getting-started/public.md) guide. So let's get into some of the details of how to manage Public Access Networks and IP Access Lists. ## Public Networks Overview You can create a **Public Network** as part of the cluster creation process, or as a standalone resource. When you create a **Public Network**, you specify the cloud provider and region where the network will be created. It is recommended to create the network in a region that is geographically close to your location to minimize latency. **Public Networks** are separate VPCs from **Private Networks** that require VPC peering to access **Private Clusters**. ### Creating a Public Network ::: tabs#way @tab Cloud Console To create a public network in the console, navigate to the Networks view and click on the `New network` button. On the new network page: 1. Provide a descriptive name for the network in the `Network name` field. 2. Select the network type as `Public` 3. Select the Cloud Provider 4. Select a region from the dropdown list. 5. Click on the `Create network` button. ![New network page](images/public-net-new-1.png) Once you have created the network, you will be taken to the network detail page, where you can see the network is in the `Provisioning` state. ![Network detail page](images/public-net-new-2.png) Once the network is provisioned, the network will be in the `Available` state. ![Network detail page](images/public-net-new-3.png) @tab esc Networks are created using the `infra networks create` sub-command of the `esc` CLI. To create a public network with the command line, use the `infra networks create` sub-command and specify the `--public-access` flag. For example, the following command creates a public network named `Public AWS us-west-2` in the AWS `us-west-2` region. ```bash esc infra networks create \ --description "Public AWS us-west-2" \ --provider aws \ --region us-west-2 \ --public-access ``` ::: ### Modifying a Public Network Once a Network resource has been created, you can only modify the name or delete the resource. First, let's see how to rename a Public Network. ::: tabs#way @tab Cloud Console To rename a Public Network in the console, navigate to the **Networks** view and click on the menu button for the network you want to rename, either in the Network list or in the Network detail section. Click on the `Rename ` menu item. ![Network detail](images/public-net-rename-1.png) You will be taken to the **Rename Network** view, where you can enter a new name for the network. When you are done, click on the `Rename network` button. ![Rename network](images/public-net-rename-2.png) You will be taken back to the **Networks** view, where you will see a notification that the network name has been updated. ![Network detail](images/public-net-rename-3.png) @tab esc To rename a Public Network using the `esc` CLI, use the `infra networks update` sub-command and specify the `--name` flag. For example, the following command renames a network named `Public AWS us-west-2` to `Public AWS us-west-2 Renamed`. ```bash esc infra networks update \ --id "cu3hlaiirsdmuidcjffg" \ --name "Public AWS us-west-2 Renamed" ``` ::: ### Deleting a Public Network Deleting a Public Network is done similarly to renaming a Network. ::: warning Resource Dependencies You will not be able to delete a network until all clusters provisioned on that network have been deleted. ::: ::: tabs#way @tab Cloud Console To delete a Public Network, navigate to the **Networks** view and click on the menu button for the network you want to delete, either in the Network list or in the Network detail section. Click on the `Delete ` menu item. ![Network detail](images/public-net-delete-1.png) You will be taken to the **Delete Network** view. You will be prompted to confirm the deletion by entering the name of the Network. Click on the `Delete network` button. ![Delete network](images/public-net-delete-2.png) If there are no clusters provisioned on the network, you will be taken back to the **Networks** view, where you will see a notification that the network deletion has been initiated. ![Network detail](images/public-net-delete-3.png) If there are clusters provisioned on the network, you will receive an error stating that the network cannot be deleted because there are clusters provisioned on it. ![Network detail](images/net-delete-with-cluster-error.png) Once the network deletion has been completed, the network will show as `Deleted` in the **Networks** view. After 24 hours, the network will be removed from the **Networks** view. ![Network detail](images/public-net-delete-4.png) @tab esc To delete a Public Network using the `esc` CLI, use the `infra networks delete` sub-command and specify the `--id` flag. For example, the following command deletes a network with the ID `cu3hlaiirsdmuidcjffg`. ```bash esc infra networks delete \ --id "cu3hlaiirsdmuidcjffg" ``` ::: ## IP Access Lists Public Access Clusters are protected by an **IP Access List**. An IP Access List is a list of IP addresses or IP address ranges specified in CIDR notation that are allowed to access the cluster. ::: tip It is common for corporate networks and VPNs to use a range of public IP addresses that outbound connections are translated to when connecting to the public internet. You should speak with your IT department or network administrator to get those IP address ranges. Not including all of the outbound IP addresses can result in intermittent connectivity issues that are otherwise difficult to diagnose. ::: Some key points about IP Access Lists: * IP Access Lists are distinct resources in Kurrent Cloud and can be created, modified, and deleted independently of Public Access Clusters * All Public Access Clusters must be assigned an IP Access List * Public Access Clusters can be assigned a single IP Access List * IP Access Lists can be applied to one or more Public Access Clusters * Updating an IP Access List will apply the changes to all clusters that are assigned that IP Access List, typically within seconds * You can change the IP Access List assigned to a cluster at any time * You cannot delete an IP Access List that is in use by any clusters ### Creating an IP Access List Besides creating an IP Access List as part of the cluster creation process, you can also create an IP Access List as a standalone resource. ::: tabs#way @tab Cloud Console To create an IP Access List, navigate to the **IP Access Lists** view and click on the `New IP Access List` button. ![New IP Access List](images/ip-access-list-create-1.png) You will be taken to the **New IP Access List** view, where you can enter a name for the IP Access List, as well as the IP Addresses or CIDR blocks you want to allow access to the cluster and an optional comment for each CIDR block. When you are done, click on the `Create IP Access List` button. ![New IP Access List](images/ip-access-list-create-2.png) @tab esc To create an IP Access List using the `esc` CLI, use the `infra acl create` sub-command and specify the `--cidr-blocks` flag. For brevity, IP Access Lists are referred to as ACLs (Access Control Lists) in the `esc` CLI. For example, the following command creates an IP Access List named `Production Access` with the CIDR blocks `12.23.56.0/24` and `12.23.57.0/24`. If you wish to add a comment to a specific CIDR block, you can do so by specifying the argument in the format `cidr-block,comment`. ```bash esc infra acl create \ --name "Production Access" \ --cidr-blocks "12.23.56.0/24" \ --cidr-blocks "12.23.57.0/24,Engineering" ``` ::: ### Modifying an IP Access List When are request to change the CIDR blocks in an IP Access List, the changes are applied asynchronously to all clusters that are assigned that IP Access List. This typically happens within seconds. If there are any issues applying the changes to any cluster, Cloud engineers are alerted to investigate. ::: tabs#way @tab Cloud Console To modify an IP Access List, navigate to the **IP Access Lists** view and click on the menu button for the IP Access List you want to modify, either in the IP Access List list or in the IP Access List detail section. Click the `Edit ` menu item. ![IP Access List detail](images/ip-access-list-edit-1.png) You will be taken to the **Edit IP Access List** view, where you can enter a new name for the IP Access List. When you are done, click the `Update IP Access List` button. ![Edit IP Access List](images/ip-access-list-edit-2.png) You will be taken back to the **IP Access Lists** view, where you will see a notification that the IP Access List has been updated. ![IP Access List detail](images/ip-access-list-edit-3.png) @tab esc To modify an IP Access List using the `esc` CLI, use the `infra acl update` sub-command. You must specify the `--id` flag, and you can use the `--cidr-blocks` flags one or more times to specify the CIDR blocks you wish to include in the list, or the `--description` flag to change the name of the IP Access List. For example, the following command updates an IP Access List with the ID `cu3hlaiirsdmuidcjffg` to include the CIDR blocks `12.23.56.0/24` and `12.23.57.0/24`. If you wish to add a comment to a specific CIDR block, you can do so by specifying the argument in the format `cidr-block,comment`. If you omit any CIDR blocks, they will be removed from the list. **Note**: When updating an IP Access List, you must specify all CIDR blocks you wish to include in the list. If you omit any CIDR blocks, they will be removed from the list. ```bash esc infra acl update \ --id "cu3hlaiirsdmuidcjffg" \ --cidr-blocks "12.23.56.0/24,Engineering" \ --cidr-blocks "12.23.57.0/24,Operations" ``` ::: --- --- url: 'https://docs.kurrent.io/cloud/ops/index.md' --- # Clusters ## Viewing clusters When you create a cluster, from the Clusters view, you will see a list of all your currently provisioned clusters, as well as any clusters that have been deleted within the last four hours. For each cluster, you will see the cluster name, type (public or private), cluster ID, provider, region, type, KurrentDB version, status, and date of creation, as well as a set of icons for performing common actions, and a menu with additional actions. ![Clusters list](./images/clusters-list.png) ### Details tab When you select a cluster from the Clusters list, you will see the cluster details page, which includes the cluster Name, ID, provisioning status, and date of creation. ![Cluster details - Details](./images/cluster-details.png) ### Provider tab The Provider tab contains details about the cloud provider, region, type (F1, C4, M8, etc), cluster topology (single node, three node multi zone), disk type, size, and any provider specific details. ![Cluster details - Provider](./images/cluster-details-provider.png) ### Network tab The Network tab contains details about the network configuration, including the Network ID, link to the Network details page, network type (public, private). ![Cluster details - Private Network](./images/cluster-details-network-private.png) For public clusters, the network tab also includes the IP Access List ID and a link to the IP Access List. ![Cluster details - Public Network](./images/cluster-details-network-public.png) ### Database tab The Database tab contains details about version of KurrentDB running on the cluster and the health status of the cluster. The `Version` field shows the major release version of KurrentDB, and `Tag` shows the specific patch version. ![Cluster details - Database](./images/cluster-details-database.png) ### Addresses tab The Addresses tab contains addresses for accessing the cluster UI, as well as URIs for gRPC and TCP clients. ![Cluster details - Addresses](./images/cluster-details-addresses.png) ### Issues tab The Issues tab contains a list of operational issues that have been detected for the cluster, such as if cluster nodes out of sync, high system load or memory usage, low disk space, etc. ![Cluster details - Issues](./images/cluster-details-issues.png) You can click on an issue to be taken to the specific issue details page. Additional information on issues can be found in the [Integrations](../integrations/README.md#issues) section. ### Notifications tab The Notifications tab contains details about provisioning errors, or errors encountered while upgrading or resizing a cluster. Additional information on notifications can be found in the [Integrations](../integrations/README.md#notifications) section. ![Cluster details - Notifications](./images/cluster-details-notifications.png) ## Connecting to a cluster As mentioned above, the **Addresses** tab of a cluster details section contains the addresses to use for accessing the cluster UI, as well as URIs for gRPC and TCP clients. You will also see a button labeled `Connect to `. When clicked, a modal will appear that will first check if the cluster is reachable. If it is, you will get a list of options for connecting to the cluster, including a link to the cluster UI, as well as links to the official KurrentDB client libraries for a variety of languages. ![Connect to cluster](./images/cluster-connect.png) If there are issues connecting to the cluster, you will see some diagnostic information indicating some possible reasons why the connection may be failing. For example, if the cluster is private and no peering has been established ![Connect to Private Cluster - Error](./images/connect-private-error-diagnostic.png) Another example of a public cluster that is not reachable ![Connect to Public Cluster - Error](./images/connect-public-error-diagnostic.png) If you are having trouble connecting to a cluster, see the [Troubleshooting](../faq.md#troubleshooting) section of the FAQ for more information, and contact our support team if you need assistance. ### DNS names The format for the DNS name of a cluster follows the pattern `.mesdb.eventstore.cloud`. This name resolves to IP addresses of all the cluster nodes or to the IP address of a single instance, depending on the deployment topology. For public clusters, the name resolves to the public IP addresses of the cluster nodes. For private clusters, the name resolves to the private IP addresses of the cluster nodes. Each cluster node has its own DNS name, which can be used for accessing individual nodes for node-specific operations like stats collection or scavenging. * **Public Clusters**: The node DNS names follow the pattern `-p.mesdb.eventstore.cloud`. The `p` suffix on the hostname is used for public cluster nodes to indicate that the node is a public node. * **Private Clusters**: The node DNS names follow the pattern `-.mesdb.eventstore.cloud`. ::: note Additional DNS names for Public Clusters Public cluster nodes also have a DNS name that follows the same pattern as private clusters, `-.mesdb.eventstore.cloud`. This name resolves to the private IP address of the node and is used for internal communication between the nodes. It is not accessible from the public internet. You can always check the `/gossip` endpoint from any node to see the list of names clients can use to connect to the individual nodes. ::: ### TLS certificates Kurrent Cloud provisions secure KurrentDB clusters with TLS enabled for HTTP and gRPC using certificates issued by Let's Encrypt. We automatically renew the certificates before they expire and replace the certificates on all cluster nodes. This is all done with zero impact to client connections or cluster availability. We do not support using third party certificates or offer private certificate authorities. ## Changing the default passwords Every KurrentDB cluster starts with two default users: `$admin` and `$ops`. When you create a cluster, those users have the default password `changeit`. It is **strongly recommended** that the first thing you do after creating a cluster is to change the passwords for these users, particularly for clusters that are publically accessible. See the [User Authentication](/server/v24.10/security/user-authentication.md#basic-authentication) section of the KurrentDB documentation for details of how to . ::: note Limitation While the enterprise version of KurrentDB supports authentications methods such as x509 certificates, LDAP, and OAuth, Kurrent Cloud Managed Clusters only support basic authentication. ::: ## Resizing cluster nodes Clusters can be scaled up or down on-demand to optimize for cost or performance through the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). When you resize a cluster, the compute instances that make up the cluster are resized. You can resize a cluster to a larger or smaller node size, but you cannot change the topology of the cluster. See also the [sizing guide](./sizing.md) for general guidance. ::: note Resizing restrictions Single-node clusters are not intended for production use. For this reason, the maximum size you can resize a single-node cluster to via the Cloud Console is `M8`. ::: ### Resize operation overview For multi-node clusters, resizes are done in a rolling fashion, meaning that the cluster is available throughout the resize operation. Before a resize starts, the cluster health is verified. If there are any issues with the cluster, such as a node is down or is too far out of sync with the cluster, the resize operation will not be allowed to begin. To minimize impact to clients, the two follower nodes are resized one at a time, then before resizing the leader node, the leader is resigned and a leader election is initiated to ensure the final node can be resized without disruption. Nodes are stopped one at a time, compute instance type is changed, and the node is started again. Once a node has been resized, before proceeding to the next node, the cluster must return to a consistent state before the next node is resized. If any cluster node does not return to a healthy state, the resize operation is aborted and the Cloud team is alerted. ::: note Client connections during resizes When a cluster node is resized, clients that are connected to that node will be disconnected and automatically reconnect to another node. If the `nodePreference` connection parameter is set to `leader`, the client will reconnect to the new leader once one has been elected. ::: For single-node instances, the resize operation requires downtime because the compute instance must be stopped. For larger databases, once the node is started again, KurrentDB may take several minutes or more to become available. ### How to resize a cluster ::: tabs#way @tab Cloud Console To resize a cluster in the console, navigate to the clusters view and select **Resize Cluster** for the cluster menu icon in the clusters list or from the menu in the cluster details section. ![cluster list](./images/cluster-resize-1.png) On the detail page, specify the new cluster size and click on **Resize Cluster**. ![cluster\_expand\_detail](./images/cluster-resize-2.png) If there a resize will incur downtime, you will receive a warning before the resize proceeds. ![Cluster Resize Warning](./images/cluster-resize-downtime-warning.png) In the cluster view, you can see that the resize is in progress. ![cluster\_expand\_detail](./images/cluster-resize-3.png) Once the resize operation is complete, the new cluster status will return to `Ok` and the new cluster size will be reflected in the **Type** column in the **Clusters List**, as well as under the **Provider** tab in the **Cluster Details** section. @tab esc To resize a cluster with the command line, use the `clusters resize` sub-command, where `--target_size` is the target instance size. Possible values are: `F1`, `C4`, `M8`, `M16`, `M32`, `M64`, `M128`. ```bash esc mesdb clusters resize \ --target-size C4 \ --id cn7dd2do0aekgb8nbf20 \ --project-id cn62uolo0aegb5icm0bg \ --org-id 9bsv0s4qu99g029v5560 ``` ::: ## Upgrading KurrentDB version The KurrentDB version of a cluster can be changed on-demand to any compatible version that is in support through the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). You can upgrade to the latest patch release or to a new major version. ::: tip If a cluster's current version of KurrentDB is compatible with an older version, you can use the upgrade process to downgrade to that older version. ::: ### Upgrade process overview When upgrading a three-node cluster, the upgrade is done in a rolling fashion, meaning that the cluster is available throughout the upgrade process. Before an upgrade is started, the cluster health is verified. If there are any issues with the cluster, such as a node is down or is too far out of sync with the cluster, the upgrade operation will not be allowed to begin. To minimize impact to clients, the two follower nodes are upgraded one at a time, then before upgrading the leader node, the leader is resigned and a leader election is initiated to ensure the final node can be upgraded without disruption. Nodes are stopped one at a time, compute instance type is changed, and the node is started again. Once a node has been upgraded, before proceeding to the next node, the cluster must return to a consistent state before the next node is upgraded. If any cluster node does not return to a healthy state, the upgrade operation is aborted and the Cloud team is alerted. ::: note Client connections during upgrades When a cluster node is upgraded, clients that are connected to that node will be disconnected and automatically reconnect to another node. If the `nodePreference` connection parameter is set to `leader`, the client will reconnect to the new leader once one has been elected. ::: Upgrading a single-node instance, on the other hand, does require downtime because the KurrentDB service must be restarted. For larger databases, KurrentDB may take several minutes or more to become available. ::: tabs#way @tab Cloud Console In the Clusters view, if there is a dot on the *Upgrade Cluster* icon for a cluster, this indicates that there is an upgrade available for the current version of the database. ![Upgrade available](./images/cluster-upgrade-1.png) To upgrade the cluster, select **Upgrade Cluster** in the cluster's menu icon in the clusters list, the **Upgrade Cluster** icon, or from the menu in the cluster details section. ![Cluster list](./images/cluster-upgrade-2.png) On the detail page, specify the new cluster version and click on **Upgrade Cluster**. ![Cluster Upgrade](./images/cluster-upgrade-3.png) In the cluster view, you can see that the upgrade is in progress. ![Cluster List - Upgrade in Progress](./images/cluster-upgrade-4.png) Once the upgrade operation is complete, the new cluster version will show in the cluster view. If the upgrade cannot be done online, a warning will be displayed and must be confirmed before the upgrade will be started. ![Cluster Upgrade Downtime Warning](./images/cluster-upgrade-downtime-warning.png) @tab esc To upgrade a cluster with the command line, use the `clusters upgrade` command, where `--target_tag` is the version you want to upgrade to. This must include the full version, e.g. 24.10.1. ```bash esc mesdb clusters upgrade \ --target-tag 24.10.1 \ --id cn7dd2do0aekgb8nbf20 \ --project-id cn62uolo0aegb5icm0bg \ --org-id 9bsv0s4qu99g029v5560 ``` ::: ## Expanding disks Disks can be expanded on-demand, to accommodate database growth, through the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc) See also the cloud [sizing guide](./sizing.md) for general guidance. ::: note Limitation After modifying a disk for a cluster in **AWS**, you must wait at least **six hours** before you can resize that volume again. See [here](https://docs.aws.amazon.com/ebs/latest/userguide/ebs-modify-volume.html#elastic-volumes-limitations) for more information. ::: ::: tabs#way @tab Cloud Console To expand disks in the console, navigate to the clusters view and click on **Expand Disk** in the cluster's menu icon in the clusters list or from the menu in the cluster details section. ![Expand Disk Menu](./images/expand-disk-1.png) On the **Expand Disk** page, specify the new disk size (as well as the disk IOPS and throughput when using the AWS GP3 disk type) and click on **Expand cluster disk**. ![Expand Disk Page](./images/expand-disk-2.png) You will be redirected to the Clusters view, where you can see that the disk expansion is in progress. ![Expand Disk Progress](./images/expand-disk-3.png) Once the disk expansion operation is complete, the new disk size will show under the **Provider** tab in the **Cluster Details** section. @tab esc To expand disks with the command line, use the `clusters expand` command, where `--id` is the cluster id. ```bash esc mesdb clusters expand \ --disk-size-in-gb 16 --id c3fi17to0aer9r834480 \ --project-id c3fhvdto0aepmg0789m0 \ --org-id bt77lfqrh41scaatc180 ``` ::: ## Protecting cluster from deletion Cluster can be protected from accidental deletion using the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). ### Enable Protection ::: tabs#way @tab Cloud Console To protect a cluster, navigate to the **Clusters view** and click on the **Protect Cluster** icon, the **Enable Protection** the cluster menu icon in the **Clusters list**, or from the menu in the **Cluster details** section. ![Enable Protection menu](./images/cluster-enable-protection-1.png) A confirmation dialog will appear. Click on **Enable Protection**. ![Enable Protection confirmat](./images/cluster-enable-protection-2.png) You will then see a notification that Protection has been enabled. ![Enable Protection success](./images/cluster-enable-protection-3.png) Now the option to delete the cluster is disabled. ![Delete inactive](./images/cluster-enable-protection-4.png) @tab esc To protect a cluster, you need to update a value of `protected` parameter to `true`: ```bash:no-line-numbers esc mesdb clusters update --id cis4pcid60b5q96r8hm0 --protected true ``` ::: ### Disable Protection To delete a protected cluster, you must first disable protection ::: tabs#way @tab Cloud Console To disable protection, navigate to the **Clusters view** and click on the **Disable Protection** icon, the **Disable Protection** the cluster menu icon in the **Clusters list**, or from the menu in the **Cluster details** section. ![Disable Protection menu](./images/cluster-disable-protection-1.png) A confirmation dialog will appear. Click on **Disable Protection**. ![Disable Protection confirmation](./images/cluster-disable-protection-2.png) You will then see a notification that Protection has been disabled. ![Disable Protection success](./images/cluster-disable-protection-3.png) Now the option to delete the cluster is available again. ![Delete available](./images/cluster-disable-protection-4.png) @tab esc ```bash:no-line-numbers esc mesdb clusters update --id cis4pcid60b5q96r8hm0 --protected false ``` ::: ## Quotas Kurrent Cloud has a quota system in place to prevent abuse of the service. Quotas are set at the organization level and currently limit the number of vCPUs that can be provisioned at any given time. To request a quota increase, please contact our support team. --- --- url: 'https://docs.kurrent.io/cloud/ops/account-security.md' --- # Account Security ## Multi-factor Authentication (MFA) Our MFA solution integrates exclusively with authenticator apps, offering a convenient and secure method for users to verify their identities. When setting up MFA, ensure your device is ready to use, as the process involves generating and entering authenticator codes. ### User MFA To enable Multi-factor Authentication (MFA) within the user interface, navigate to [Account Preferences](https://console.kurrent.cloud/preferences). Under the **Multi-Factor Authentication (MFA) Preferences** section you will see if your account has MFA enabled or disabled. To enable, click the **Enable** button. If it is currently enabled, you can click the **Disable** button to disable MFA. ![Preferences - MFA](./images/enable-mfa-1.png) When prompted to confirm the MFA setup, click on the **Enable** button. ![MFA Confirmation](./images/enable-mfa-2.png) You will then be taken to the MFA setup page, where you must first enter your password ![MFA Setup Process](./images/enable-mfa-3.png) Next, you will be presented with a QR code. Scan the QR code with your authenticator app. If you are unable to scan the QR code, you can click on **Trouble Scanning?**, which will provide you with the secret key in text format, which you can enter into your authenticator app. ![MFA Setup Process](./images/enable-mfa-4.png) Once you have scanned the QR code and entered the one-time password (OTP) generated by the authenticator, you will be given a recovery code. ::: warning Recovery Code It is vital you save the recovery code somewhere secure. If you lose access to the authenticator app that is setup to generate the OTP code for your account, your recovery code will be your only way to regain access to your account. ::: ![MFA Recovery Code](./images/enable-mfa-5.png) Once you have saved your recovery code, you should see a confirmation message that your MFA setup is complete. ![MFA Setup Complete](./images/enable-mfa-6.png) ### Re-generating tokens from `esc` with MFA Once MFA is enabled, tokens generated with the `esc` command-line tool are invalidated. To regain access, generate new tokens using the following command: ```bash esc access tokens create ``` You will be prompted for your password and an authenticator code as part of the process. Alternatively, if you prefer to obtain a token through the Cloud Console, navigate to the [Authentication Tokens](https://console.kurrent.cloud/authentication-tokens) page and click the **Request refresh token** button. ### Require MFA for all organization users To enable MFA for all organization users, navigate to the **Access Control** section for the organization. ![Enable MFA for Organization](./images/enable-mfa-org-1.png) Click on **Settings** to get to the Organization Access Control Settings. To enable MFA for the organization, check the *Enable Multi-Factor Authentication (MFA)* checkbox and click the **Update** button. To disable, uncheck the same checkbox and click **Update**. ![Enable MFA for Organization](./images/enable-mfa-org-2.png) ::: warning Enabling MFA for an organization only affects new users and prevents current users who have MFA enabled from disabling it for their account. It does not affect existing users who have MFA disabled. ::: ## Audit Log API The Audit Log API displays an organization's activities as a series of events. It provides access to an audit trail of changes made by the current user or for the entire organization. Audit logs for an organization are only accessible by organization admins. However, any user can view the changes that he/she made in the cloud. ### Get audit logs for the current user Usage: `esc audit user get` Optional Parameters: * `-o, --org-id`: The organization ID for which to retrieve audit logs. * `-a, --after`: Only get logs after this timestamp. * `-b, --before`: Only get logs before this timestamp. * `-l, --limit`: The maximum number of records to retrieve. ### Get audit logs for an entire organization Usage: `esc audit organization get -o {orgId}` Required Parameters: * `-o, --org-id`: The organization ID for which to retrieve audit logs. Optional Parameters: * `-a, --after`: Only get logs after this timestamp. * `-b, --before`: Only get logs before this timestamp. * `-l, --limit`: The maximum number of records to retrieve. --- --- url: 'https://docs.kurrent.io/cloud/ops/backups.md' --- # Backup and Restore It is important to take regular backups of your clusters. Besides providing a safety net in case of data corruption, backups can also used for various operations, such as creating copies of your data for testing, moving data from one Kurrent Cloud Project to another, or changing from one type of access from Public to Private, or vice versa. There are two types of backups: * **Manual Backups**: These are backups that you create on demand using the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). * **Scheduled Backups**: These are backups that are created automatically by a [Scheduled Job](./jobs.md) that defines the frequency and number of backups to keep at a time. Both types of backups can be viewed in the **Backups** view of the console. All backups created by the same job will be grouped together in one row, which can be expanded by clicking the down arrow icon on the right side of the row. ![Backups List](./images/backups-list.png) ## Manual Backup Backups can be created on demand using the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). You can customise the backup label using a combination of free-text and predefined variables: * **index—**: the auto-incremented value with the number of backups. You can format it as: * decimal: `index:decimal` (*default*), * hexadecimal: `index:hex`. * **cluster**: value from the cluster information: * description: `cluster:description` (*default*), * id: `cluster:id`, * cloud provider: `cluster:provider` * **datetime**: timestamp of when backup was made. You can format it as: * UTC time: `datetime:utc` (*default*), * [RFC 822](https://www.w3.org/Protocols/rfc822/#z28): `datetime:rfc822`, * [Unix](https://en.wikipedia.org/wiki/Unix_time): `datetime:unix`, * [JSON](https://en.m.wikipedia.org/wiki/ISO_8601): `datetime:json`, * [RFC3339](https://tools.ietf.org/html/rfc3339): `datetime:rfc3339`, ::: tabs#way @tab Cloud Console To create a backup in the console, navigate to the clusters view and click on the **Create backup** icon, or . In the popup, click the **Create one-off backup** button. ![Create One-Off Backup](./images/one-off-backup.png) @tab esc To create a backup, use the `backups create` command: ```bash esc mesdb backups create --description "on demand backup" \ --source-cluster-id c1eut65o0aeu6ojco7a0 \ --project-id btfjev2rh41scaatc1k0 BackupId("c1ev3l5o0aeu6ojco7b0") ``` To see the status of the backup use the `backups get` command: ```bash esc mesdb backups get --project-id btfjev2rh41scaatc1k0 \ --id c1ev3l5o0aeu6ojco7b0 Backup { id: BackupId("c1ev3l5o0aeu6ojco7b0"), project_id: ProjectId("btfjev2rh41scaatc1k0"), source_cluster_id: ClusterId("c1eut65o0aeu6ojco7a0"), source_cluster_description: "Demo-Backup", description: "on demand backup", size_gb: 8, provider: Aws, region: "eu-central-1", status: "available", created: "2021-03-26T14:38:12Z", linked_resource: None } ``` ::: ## Scheduled Backups Scheduled backups can be created through the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc) Scheduled backup jobs can be run as frequently as once an hour. After each successful backup, older backups created by the same job will be automatically deleted based on the provided configuration. ::: note Limitation: Only one backup can be created for a cluster at a time. ::: When a backup begins, a resource lock is placed on that cluster from other operations, such as modifying, deleting, and creating additional backups for the duration of that backup creation operation. Multiple scheduled backups can target the same cluster. However, if schedules overlap or a manual backup is in progress, any jobs that are attempting to start after the first backup job has started will fail as the first backup job to start will have locked the cluster. For example, you could create one scheduled backup that executes every hour, along with a second scheduled backup that executes once a week. Backups from these scheduled jobs are pruned independently regardless of their age, so if both saved a maximum of four backups, the oldest backup from the weekly job might be close to a month old, while the hourly job's backups would never be older than a fraction of a day. ::: tip To avoid this issue, you can offset scheduled backup jobs that might overlap by 15 to 30 minutes. Some providers can take longer to create backups, particularly for larger disks. You may need to adjust the offset if you encounter backup failures for this reason. ::: ### Backup Failures While backups failures are not common, there are several scenarios that can cause the creation of a backup to fail. The most likely causes are: * The cluster is locked by another operation, such as another backup is in progress, disk expansion, cluster resize, database upgrade, etc. * The cluster is not in a healthy state, such as if there is a node that is not responding or behind on replication. * The cloud provider is taking longer than expected to create a volume snapshot. If a backup fails, the cluster will be unlocked and the backup will be marked as `Defunct` in the console. If the backup was created by a schedule job, the defunct backup will be it deleted when it is scheduled to be pruned. One-off backups must be deleted manually. ::: warning If a backup becomes defunct due to a timeout waiting for the cloud provider, the snapshot may still have been created, and you will be charged for the snapshot storage. ::: ::: tip You can receive notifications of backup failures by setting up an [Integration](../integrations/README.md) to have an alert generated, notification sent to a chat channel or via email. ::: :::: tabs#way @tab Cloud Console To create a scheduled backup in the console, navigate to the **Clusters** view and click on the `create backup` icon and then on `Create backup schedule`. ![Create Scheduled Backup](./images/scheduled-backup-new-1.png) Choose a description, the frequency as well as the number of backups to keep before pruning. Finally, click the **Create backup schedule** button. ![Create Scheduled Backup](./images/scheduled-backup-new-2.png) Backups created this way appear in the console alongside backups created manually. To see the status on the scheduled backup jobs, navigate to the **Jobs** view of the console. ![Backup Jobs](./images/scheduled-backup-jobs.png) There you can see all backups created by a job, as well as their history and any backup jobs that may have failed. ::: note A backup might fail, for instance, if a cluster is locked by another operation when the backup tries to run. Such a locking operation could be the disk expand or manual backup. ::: @tab esc A scheduled backup can be created using the Kurrent Cloud CLI by using the `orchestrate` subcommand. The following call will create a new scheduled backup of the cluster with ID `c196ogto0aeqohe3ommq`: ```bash esc orchestrate jobs create \ --description 'My Hourly Backup' \ --schedule '0 */1 * * *' scheduled-backup \ --description '{cluster} Hourly Backup {datetime:RFC3339}' \ --max-backup-count 2 \ --cluster-id c196ogto0aeqohe3ommq ``` For details on the scheduled field, see [Job Schedules](./README.md). To list current jobs, run: ```bash esc orchestrate jobs list ``` To view the history of a job, run: ```bash esc orchestrate history list --job-id ``` :::: ## Restore from backup Restores can be done on demand using the [Cloud Console](https://console.kurrent.cloud/) and the [Kurrent Cloud CLI](https://github.com/kurrent-io/esc). The topology of the new cluster does not need to be the same as the source of the backup: you can restore a 3 nodes cluster backup to a single node one. Additionally, you can restore a backup to a different network in the same cloud provider and region in the same project or another ::: note Limitations: * Restoring a backup will create a new cluster * You cannot restore a backup in place, a restored cluster will have a different ID than the source cluster * You cannot restore a backup to a different cloud provider or different region within the same cloud provider * You can only restore a backup to a disk size that is the same size or greater than the source cluster ::: ::: tabs#way @tab Cloud Console To restore a backup, navigate to the `Backups` view of the [Cloud Console](https://console.kurrent.cloud/) and click on the `Restore` icon of the backup you want to restore. ![one off restore backup](./images/backup-restore-1.png) Backups are restored as new clusters. You will be then redirected to the **Restore Backup** page, which is nearly identical to the **Cluster Create** page, where you can choose your cluster parameters. Note that you are not limited to restoring the backup to exactly the same cluster configuration as the cluster for which the backup was taken. You can change the cluster topology, the database software version, and the instance size. You cannot restore between different cloud providers, regions, or to another organization. ![one off restore cluster backup](./images/backup-restore-2.png) @tab esc As you cannot restore to the existing cluster, you should use the `source-backup-id` option of the `mesdb clusters create` command. When the backup id is provided, the CLI tool will create a new cluster using the provided backup. Example: restoring the backup with ID `c10dvoarh41lb9otkdrg` to an C4 single node instance. ```bash $ esc mesdb clusters create \ --description "restore" \ --source-backup-id c10dvoarh41lb9otkdrg \ --instance-type C4 --disk-size-in-gb 10 \ --disk-type gp3 --network-id c10dr5qrh41lbabqa2j0 \ --projection-level off --server-version 24.10 \ --topology single-node --project-id c10d0h2rh41lba1v92k0 ``` The output will display the new cluster ID: ```bash:no-line-numbers ClusterId("c1mnqjdo0aembuk4ljo0") ``` You can then get the new cluster status with the following command: ```bash $ esc mesdb clusters get --id c1mnqjdo0aembuk4ljo0 \ --project-id c10d0h2rh41lba1v92k0 --json ``` The output will be similar to: ```json { "id": "c1mnqjdo0aembuk4ljo0", "organizationId": "bt77lfqrh41scaatc180", "projectId": "c10d0h2rh41lba1v92k0", "networkId": "c10dr5qrh41lbabqa2j0", "description": "restore", "provider": "aws", "region": "eu-west-2", "topology": "single-node", "instanceType": "f1", "diskSizeGb": 10, "diskType": "gp3", "serverVersion": "24.10", "projectionLevel": "off", "status": "available", "created": "2021-03-26T09:37:17Z", "addresses": { "tcp": [ "c1mnqjdo0aembuk4ljo0.mesdb.eventstore.cloud:1113" ], "grpc": "kurrentdb://c1mnqjdo0aembuk4ljo0.mesdb.eventstore.cloud:2113", "ui": "https://c1mnqjdo0aembuk4ljo0.mesdb.eventstore.cloud:2113" } } ``` ::: --- --- url: 'https://docs.kurrent.io/cloud/ops/events.md' --- # Events and notifications ## Events and notifications On the Event Console, you will find messages about issues with your provisioned resources in the `Issues` section and errors that happened during provisioning in the `Notifications` section. Examples of issues that are reported in the `Issues` section: * Available disk space is low * Cluster load is high * Cluster is not responding Examples of errors that are reported in the `Notifications` section: * Failed cluster creation or deletion * Failed backup creation or deletion * Failed cluster upgrade You can also get these notifications sent to you by setting up an [Integration](../integrations/README.md). ## Operational status Any known issues impacting the availability of Kurrent Cloud, as well as planned outages and maintenance updates, will be displayed in the sidebar of the Kurrent Cloud console. Should there be any outage of the Kurrent Cloud console, you can also check and subscribe to Kurrent Cloud's current status at [our status page](https://status.eventstore.cloud). --- --- url: 'https://docs.kurrent.io/cloud/ops/jobs.md' --- # Scheduled Jobs The Kurrent Cloud runs scheduled jobs on the user's behalf. All of these jobs consist of a human-readable description and a schedule. Currently, the following jobs are supported: * Scheduled backups ### Job Schedules The schedule format used by the Kurrent Cloud CLI tool and API is a simplified subset of [cron](https://en.wikipedia.org/wiki/Cron#Overview), in the future we may support more cron features. The supported subset is: * for the first field, minute: * a wildcard `*` * a number between `0` and `59` (inclusive). * a rate, written as a wildcard divided by a number. E.g: `*/15` * For the second field, hour: * a wildcard `*` * a number must be between `0` and `23` (inclusive) * a rate, written as a wildcard divided by a number. E.g: `*/1` * for the third field, day of month: * a wildcard `*` * for the fourth field, month: * a wildcard `*` * for the fifth field, day of the week: * a wildcard `*` * a number between `0` and `7` (inclusive), Sunday to Saturday ``` ┌───────────── Minute: wildcard, number (0 - 59), rate │ ┌─────────── Hour: wildcard, number (0 - 23), rate │ │ ┌───────── Day of the month: wildcard │ │ │ ┌─────── Month: wildcard │ │ │ │ ┌───── Day of the week: wildcard, number (0 - 7) │ │ │ │ │ * * * * * ``` ::: note Scheduled backups have a minimum frequency of 60 minutes. Currently, it is not possible to schedule backups more frequently. ::: #### Examples: `0 */1 * * *` runs a job once an hour, at minute 00. `0 12 * * 1` runs a job at 12:00 PM on Monday. `30 13 * * 0` runs a job at 13:30 PM on Sunday. --- --- url: 'https://docs.kurrent.io/cloud/ops/sizing.md' --- # Cloud instance sizing guide Use this guide to assess the needs of your application performance, compared with the capability of a given KurrentDB instance in Kurrent Cloud. ## Instance performance Performance of the managed KurrentDB cluster in ES Cloud depends primarily on the instance size for cluster members. In addition, the disk size could affect the instance IOPS limit when it comes to reading and writing events to the database. We rate instances based on the desired usage. Our current ratings include: * Development * Production ### Throughput A wide variety of factors impact total Event throughput on a cluster, including Events per batch and Event size and the active number of streams in the working set. We have profiled using single event transactions for the production rated clusters and confirmed they can operate at 20k tx/sec throughput (concurrent read and write) given the correct configuration for concurrent clients, a given disk size, and Working Set. It's possible to increase the throughput by using batched write operations, but the impact of batching heavily depends on the batch size in bytes. Before placing any application into production, it is vital to perform a performance test on the planned instance to assess how these factors apply to your application. ::: note The **Working Set** is the number of streams being read from and written to **concurrently**. It's essential to recognise that writing one million events into one million streams is a very different scenario than writing one million events into a single stream. ::: ### Cloud vs self-hosted and development setups When load-testing an application against Kurrent Cloud KurrentDB clusters, performance may differ from a self-hosted solution when utilizing a similar instance size. Kurrent Cloud utilizes [ZFS](https://en.wikipedia.org/wiki/ZFS) to ensure filesystem integrity/safety as well as block level compression, to ensure the minimization of IOPs and a lower storage cost for data hosted. ZFS requires additional CPU and memory to provide these capabilities. As such a self-hosted comparison should utilize a similar configuration in order to provide a relevant comparison. KurrentDB requires CPU cycles to maintain indexes, and for other maintenance processes. Underpopulated or underloaded databases (such as demo or development setups) require little CPU time and overperform compared to fully populated systems with consistent throughput. Expect performance to level off as workloads increase. ## Available instance sizes | Size | Rating | Working Set (streams) | Disk size (min) | Concurrent clients (max) | |:---------|:-------------------------------|:----------------------|:----------------|:-------------------------| | **C4** | Development | 500k | 100 GB | | | **M8** | Development | 1M | 250 GB | | | **M16** | Development / Light Production | 6M | 500 GB | 20 | | **M32** | Production | 12M | 1 TB | 250 | | **M64** | Production | 30M | 2 TB | 500 | | **M128** | Production | 62M | 4 TB | 500 | --- --- url: 'https://docs.kurrent.io/cloud/tutorials.md' --- # Tutorials ## Loan Application To get you started, we've created an example application packaged as a set of Docker containers that illustrates the flow of events being appended to streams in KurrentDB. After you have completed the following tutorial, you will have: * Familiarized yourself with the Kurrent Cloud UI * Connected an example application to Kurrent Cloud * Navigated the KurrentDB Web UI and investigated events appended to streams * Been provided with resources such as documentation, videos, e-learning, and sample code repositories, to take the next step in your Kurrent Cloud journey To get started, click [here](https://github.com/kurrent-io/samples/blob/main/LoanApplication/README.md). ## Code Samples We have a range of sample code written in various languages, please visit our [GitHub](https://github.com/kurrent-io/samples) repository to find out more. --- --- url: 'https://docs.kurrent.io/commercial-tools/cli-tool.md' --- # Node administration CLI tool This CLI tool is a command line interface for administering KurrentDB nodes. It allows you to run tasks similar to those available in the web admin interface and SDKs, including administrating users, projections and configuration. ## Download Visit our [customer portal](https://developers.eventstore.org) to download the tool for Linux, Windows, and macOS. ## Usage ```shell es-cli [options] [args] ``` Where `` is equal to the section, and `` the operation. For example, to shutdown a node: ```shell es-cli admin shutdown ``` ### Connecting and authentication Using the CLI tool requires specifying a KurrentDB node by passing the URL with the `--serveurl` option, and your admin credentials with the `--username` and `--password` options: ```bash es-cli --serverurl="http://localhost:2113" --username=admin --password=changeit ``` ## Configuration You can also create a file with configuration values at the correct file path for your operating system. ::: tabs#os @tab Windows ```powershell %AppData%/eventstore.rc ``` @tab Linux and macOS ```shell ~/.eventstorerc ``` ::: The configuration file may contain any of the following values: ``` serverurl="http://127.0.0.1:2113" username="admin" password="changeit" output="json" # Or XML verbose=true # Or false ``` ## Commands Usage: ```shell es-cli [] [] ``` ### Options | Option | Description | |---------------|----------------------------------------------------------| | `--version` | Get the version of KurrentDB CLI | | `--help` | Display help | | `--output` | How output should be formatted (`text` or `json`) | | `--password` | The admin password | | `--serverurl` | The url of the KurrentDB server | | `--username` | The admin username | | `--verbose` | Verbose output of requests sent to the KurrentDB node | ### Commands summary | Command | Sub commands | |---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [admin](#admin) | [scavenge](#admin-scavenge), [shutdown](#admin-shutdown), [merge\_indexes](#admin-merge_indexes), [calculate\_stream\_size](#admin-calculate_stream_size), [backup](#admin-backup), [restore](#admin-restore), [s3\_backup](#admin-s3_backup), [s3\_restore](#admin-s3_restore), [azure\_backup](#admin-azure_backup), [azure\_restore](#admin-azure_restore), [verify\_db](#admin-verify_db), [clear\_scavenge\_streams](#admin-clear_scavenge_streams), [delete\_streams](#admin-delete_streams) | | [user](#user) | [add](#user-add), [change\_password](#user-change_password), [delete](#user-delete), [disable](#user-disable), [update](#user-update), [enable](#user-enable), [list](#user-list), [reset\_password](#user-reset_password) | | [projections](#projections) | [delete](#projections-delete), [disable](#projections-disable), [enable](#projections-enable), [list](#projections-list), [new](#projections-new), [result](#projections-result), [state](#projections-state), [status](#projections-status), [restore\_checkpoint](#projections-restore_checkpoint), [has\_stalled](#projections-has_stalled) | | [competing](#competing) | [list](#competing-list), [create](#competing-create), [update](#competing-update) | | [config\_generator](#config_generator) | [create\_config](#config_generator-create_config) | ### Command details #### admin ```shell es-cli admin [--version] [--help] [] ``` | Command | Description | |---------------------------------------------------------|--------------------------------------------------------------------------------------------| | [azure\_backup](#admin-azure_backup) | Backs up the KurrentDB database to the specified container in Azure Blob Storage | | [azure\_restore](#admin-azure_restore) | Restores a KurrentDB database from a specified container in Azure Blob Storage | | [backup](#admin-backup) | Backs up the KurrentDB database to the destination directory | | [calculate\_stream\_size](#admin-calculate_stream_size) | Calculates the size on disk of the give stream | | [clear\_scavenge\_streams](#admin-clear_scavenge_streams) | Deletes all the scavenge history streams | | [delete\_streams](#admin-delete_streams) | Deletes streams matching the specified regular expression | | [merge\_indexes](#admin-merge_indexes) | Manually merge indexes of KurrentDB | | [restore](#admin-restore) | Restores the KurrentDB database from the provided location to the destination directory | | [s3\_backup](#admin-s3_backup) | Backs up the KurrentDB to an S3 bucket | | [s3\_restore](#admin-s3_restore) | Restores KurrentDB from an S3 bucket | | [scavenge](#admin-scavenge) | Schedule a scavenge on KurrentDB | | [shutdown](#admin-shutdown) | Shutdown the KurrentDB instance | | [verify\_db](#admin-verify_db) | Verify the integrity of an EventStore database | #### admin azure\_backup ```shell es-cli admin azure_backup [options] ``` Backs up the KurrentDB database to the specified container in Azure Blob Storage. | Option | Description | |-----------------------|-------------------------------------------------------------------------------------------| | `-databasesource` | The location of the KurrentDB database | | `-indexsource` | The location of the KurrentDB index (default: databasesource/index) | | `-differential` | Backup only new or changed files | | `-deleteextra` | Delete extraneous files from the destination (with -differential only) | | `-storageaccountname` | The name of the storage account | | `-storageaccountkey` | The account key of the storage account (found in **security** > **access keys** on azure) | | `-databasecontainer` | The destination container of the database backup | | `-indexcontainer` | The destination container of the index backup (default: databasecontainer-index) | | `-maxretrycount` | The number of times to retry an upload before cancelling the backup (default: 3) | | `-y` | Automatically confirm prompt to delete files from the destination directory | #### admin azure\_restore ```shell es-cli admin azure_restore [options] ``` Restores a KurrentDB database from a specified container in Azure Blob Storage. | Option | Description | |-----------------------|-------------------------------------------------------------------------------------------| | `-databasesource` | The location of the KurrentDB database | | `-indexsource` | The location of the KurrentDB index (default: databasesource/index) | | `-storageaccountname` | The name of the storage account | | `-storageaccountkey` | The account key of the storage account (found in **security** > **access keys** on azure) | | `-databasecontainer` | The container of the database backup | | `-indexcontainer` | The container of the index backup (default: databasecontainer-index) | | `-maxretrycount` | The number of times to retry a download before cancelling the restore (default: 3) | | `-y` | Automatically confirm prompt to delete the files in the destination folders if they exist | #### admin backup ```shell es-cli admin backup [options] ``` Backs up the KurrentDB database to the destination directory. | Option | Description | |------------------------|-----------------------------------------------------------------------------| | `-databasesource` | The location of the KurrentDB database | | `-databasedestination` | The backup destination | | `-indexsource` | The location of the Index files (default: databasesource/index) | | `-indexdestination` | The index backup destination (default: databasedestination/index) | | `-differential` | Backup only new or changed files | | `-deleteextra` | Delete extraneous files from the destination (with -differential only) | | `-y` | Automatically confirm prompt to delete files from the destination directory | #### admin calculate\_stream\_size ```shell es-cli admin calculate_stream_size [options] ``` Calculates the size on disk of the given stream. | Option | Description | |---------------|-------------| | `-streamname` | Stream Name | #### admin clear\_scavenge\_streams no options #### admin delete\_streams ```shell es-cli admin delete_streams [options] ``` Deletes streams matching the specified regular expression. | Option | Description | |----------------|----------------------------------------------------------------| | `-pattern` | Regular expression that the streams must match | | `-list` | Only list the streams matching the pattern, do not delete them | | `-fromall` | Force read stream names from $all | | `-fromstreams` | Force read stream names from $streams | | `-noverify` | Does not verify if the stream exists (faster) | | | USE THE FOLLOWING OPTIONS WITH CAUTION! | | `-hard` | Hard delete the streams (Default: soft delete) | | `-system` | Include system streams (starting with $) for deletion | | `-y` | Automatically confirm prompt to delete a stream | #### admin merge\_indexes No options. #### admin restore ```shell es-cli admin restore [options] ``` Restores the KurrentDB database from the provided location to the destination directory. | Option | Description | |------------------------|---------------------------------------------------------------------------| | `-databasesource` | The location of the backup to restore | | `-databasedestination` | The destination of the restore | | `-indexsource` | The location of the index backup (default: databasesource/index) | | `-indexdestination` | The destination of the index restore (default: databasedestination/index) | | `-y` | Automatic yes to prompts to delete files from destination directory | #### admin s3\_backup ```shell es-cli admin s3_backup [options] ``` Backs up a KurrentDB database to the destination S3 bucket. | Option | Description | |------------------------|-----------------------------------------------------------------------------| | `-databasesource` | The location of the KurrentDB database | | `-databasedestination` | The backup destination in the s3 bucket | | `-s3bucket` | The name of the S3 bucket | | `-indexsource` | The location of the Index files (default: databasesource/index) | | `-indexdestination` | The index backup destination (default: databasedestination/index) | | `-differential` | Backup only new or changed files | | `-deleteextra` | Delete extraneous files from the destination (with -differential only) | | `-awsregion` | The AWS region of the bucket | | `-awsaccesskeyid` | The AWS access key to use (if not provided, use environment variables) | | `-awssecretkey` | The AWS secret key to use (if not provided, use environment variables) | | `-awstoken` | The AWS token to use | | `-y` | Automatically confirm prompt to delete files from the destination directory | #### admin s3\_restore ```shell es-cli admin s3_restore [options] ``` Restores a KurrentDB database from an S3 bucket. | Option | Description | |------------------------|-----------------------------------------------------------------------------------| | `-databasesource` | The location in the S3 bucket of the backup to restore | | `-databasedestination` | The destination of the restore | | `-s3bucket` | The name of the S3 bucket | | `-indexsource` | The location in the S3 bucket of the index backup (default: databasesource/index) | | `-indexdestination` | The destination of the index restore (default: databasedestination/index) | | `-awsregion` | The AWS region of the bucket | | `-awsaccesskeyid` | The AWS access key to use (if not provided, use environment variables) | | `-awssecretkey` | The AWS secret key to use (if not provided, use environment variables) | | `-awstoken` | The AWS token to use | | `-y` | Automatically confirm prompt to delete all files from the destination folder | #### admin scavenge No options. #### admin shutdown No options. #### admin verify\_db ```shell es-cli admin verify_db [options] ``` Verify the integrity of a KurrentDB database. | Option | Description | |--------------------|-------------------------------------------------------------------------------------| | `-databasepath` | The path to the KurrentDB database directory on disk | | `-indexpath` | The path to the KurrentDB index directory on disk (default: databasepath/index) | | `-skipdbverify` | Skip database verification (default: false) | | `-skipindexverify` | Skip index verification (default: false) | An output file called 'verify\_db.error.log' will be written to the current working directory. #### user ```shell es-cli user [--version] [--help] [] ``` | Command | Description | |------------------------------------------|--------------------------| | [add](#user-add) | Add a user | | [change\_password](#user-change_password) | Change the user password | | [delete](#user-delete) | Delete a user | | [disable](#user-disable) | Disable a user | | [enable](#user-enable) | Enable a user | | [list](#user-list) | List all users | | [reset\_password](#user-reset_password) | Reset a user's password | | [update](#user-update) | Update a user | #### user add ```shell es-cli useradd [options] ``` Add a user. | Option | Description | |-----------------|------------------| | `-loginname` | Login Name | | `-password` | Password | | `-fullname` | Full Name | | `-isadmin` | Is Administrator | | `-isoperations` | Is Operations | #### user change\_password ```shell es-cli user change_password [options] ``` Change the password for the given user. | Option | Description | |--------------------|-----------------------------------------| | `-loginname` | The login name of the user to update | | `-newpassword` | The new password for the given user | | `-currentpassword` | The current password for the given user | #### user delete ```shell es-cli user delete [options] ``` Delete a user. | Option | Description | |--------------|--------------------------------------| | `-loginname` | The login name of the user to delete | #### user disable ```shell es-cli user disable [options] ``` Disable a user. | Option | Description | |--------------|---------------------------------------| | `-loginname` | The login name of the user to disable | #### user update ```shell es-cli user update [options] ``` Update a user. | Option | Description | |-----------------|------------------| | `-loginname` | Login Name | | `-fullname` | Full Name | | `-isadmin` | Is Administrator | | `-isoperations` | Is Operations | #### user enable ```shell es-cli user enable [options] ``` Enable a user. | Option | Description | |--------------|--------------------------------------| | `-loginname` | The login name of the user to enable | #### user list ```shell es-cli user list [options] ``` List all users. #### user reset\_password ```shell es-cli user reset_password [options] ``` Reset a user's password. | Option | Description | |----------------|-------------------------------------| | `-loginname` | The login name of the user | | `-newpassword` | The new password for the given user | #### projections ```shell es-cli projections [--version] [--help] [] ``` | Command | Description | |-------------------------------------------------------|------------------------------------------------------| | [delete](#projections-delete) | Delete a projection | | [disable](#projections-disable) | Disable a projection | | [enable](#projections-enable) | Enable a projection | | [has\_stalled](#projections-has_stalled) | Determines whether a projection has possibly stalled | | [list](#projections-list) | List all of the projections | | [new](#projections-new) | Create a new projection | | [restore\_checkpoint](#projections-restore_checkpoint) | Restore a previous checkpoint for a projection | | [result](#projections-result) | Get the result of a projection | | [state](#projections-state) | Get the state of a projection | | [status](#projections-status) | Get the status of a projection | #### projections delete ```shell es-cli projections delete [options] ``` Delete a projection. | Option | Description | |---------------------------|------------------------------| | `-name` | The projection name | | `-deletestatestream` | Delete the State Stream | | `-deletecheckpointstream` | Delete the Checkpoint Stream | #### projections disable ```shell es-cli projections disable [options] ``` Disable a projection. | Option | Description | |---------|---------------------| | `-name` | The projection name | #### projections enable ```shell es-cli projections enable [options] ``` Enable a projection. | Option | Description | |---------|---------------------| | `-name` | The projection name | #### projections list ```shell es-cli projections list [options] ``` List all the projections. #### projections new ```shell es-cli projections new [options] ``` Create a new projection. | Option | Description | |----------|------------------------------------------------------------------------------------| | `-name` | The projection name | | `-type` | Type of projection (\[o]netime, \[c]ontinuous, \[t]ransient) | | `-query` | The projection query (Prefix with @ to load from file. e.g. @projectionquery.json) | #### projections result ```shell es-cli projections result [options] ``` Get the result of a projection. | Option | Description | |--------------|--------------------------------------------------| | `-name` | The projection name | | `-partition` | The partition of the result you are querying for | #### projections state ```shell es-cli projections state [options] ``` Get the state of a projection. | Option | Description | |--------------|-------------------------------------------------| | `-name` | The projection name | | `-partition` | The partition of the state you are querying for | #### projections status ```shell es-cli projections status [options] ``` Get the status of a projection. | Option | Description | |---------|---------------------| | `-name` | The projection name | #### projections restore\_checkpoint ```shell es-cli projections restore_checkpoint [options] ``` Restore a previous checkpoint for a projection. | Option | Description | |---------|---------------------| | `-name` | The projection name | #### projections has\_stalled ```shell es-cli projections has_stalled [options] ``` Determines whether a projection has stalled. | Option | Description | |---------|---------------------| | `-name` | The projection name | #### competing ```shell es-cli competing [--version] [--help] [] ``` | Command | Description | |-----------------------------|---------------------------------| | [create](#competing-create) | Create a new subscription | | delete | Delete an existing subscription | | [list](#competing-list) | List the subscriptions | | [update](#competing-update) | Update an existing subscription | #### competing list ```shell es-cli competing list [options] ``` List the subscriptions. | Option | Description | |----------------------|--------------------------------------------------| | `-streamid` | The name of the stream to list subscriptions for | | `-subscription_name` | The name of the subscription to list | #### competing create ```shell es-cli competing create [options] ``` Create a new subscription. | Option | Description | |----------------------|---------------------------------------------------------------------------------------------------------------| | `-streamid` | The stream to subscribe to | | `-subscription_name` | The name of the subscription group | | `-config` | The settings to create the subscription with (Prefix with @ to load from file. e.g. @subscriptionConfig.json) | #### competing update ```shell es-cli competing update [options] ``` Update an existing subscription. | Option | Description | |----------------------|---------------------------------------------------------------------------------------------------------------| | `-streamid` | The stream to update the subscription on | | `-subscription_name` | The name of the subscription group to update | | `-config` | The settings to update the subscription with (Prefix with @ to load from file. e.g. @subscriptionConfig.json) | #### config\_generator ```shell es-cli config_generator [--version] [--help] [] ``` | Command | Description | |--------------------------------------------------|---------------------------------------------------------------------------------| | [create\_config](#config_generator-create_config) | Generates config files with recommended settings based on the provided options. | #### config\_generator create\_config ```shell es-cli config_generator createconfig [options] ``` Generates config files with recommended settings based on the provided options. Output options: | Option | Description | |-----------|------------------------------------------------------------------------------------| | `-output` | The destination for the generated config files (if nothing, just prints to STDOUT) | General options: | Option | Description | |--------------------|-----------------------------------------------------| | `-db` | The location of the KurrentDB database | | `-index` | The location of the KurrentDB index | | `-log` | The location of the KurrentDB logs | | `-run_projections` | The level of projections to run (none, system, all) | Cluster options: | Option | Description | |---------------------|---------------------------------------------------------------------| | `-node_count` | The number of database nodes in the cluster (default: 1) | | `-gossip_discovery` | How nodes in the cluster will discover each other (gossipseed, dns) | | `-dns_name` | The DNS name to use for gossip discovery | Network options: | Option | Description | |-----------------|-------------------------------------------------------------------------------------------------------------------| | `-network_type` | The type of network the nodes will be running in (lan, cloud) | | `-internal_ips` | The internal IP addresses of the nodes, separated by a comma (node order should be the same as other ip arguments | | `-external_ips` | The external IP addresses of the nodes, separated by a comma (node order should be the same as other ip arguments | ## Further help For further information you can use the `--help` option at the `es-cli` top level, the section level, or for individual operations. ## Troubleshooting The CLI tools uses the HTTP API and you can output the requests and responses from the calls by setting the `--verbose` configuration value to `true`. If you encounter any issues, please open a ticket on freshdesk. ## Changelog ### 1.7.0 * Added `verify_db` command under `admin` section to verify integrity of chunks, indexes and checkpoint files. ### 1.6.0 * Added `merge_indexes` command under `admin` section to trigger manual index merges when setting `MaxAutoMergeIndexLevel` * Added `db_stats` command under `admin` section which processes chunk files locally and outputs stream statistics in CSV format ### 1.5.0 * Added -deleteextra option to differential backup (for local, s3 and azure) to allow deleting of extraneous files from the destination (useful do delete previous versions of scavenged chunks from the destination) * Bugfix: Copy last chunk file to a temporary location before backing it up * Miscellaneous bug fixes ### 1.4.0 * Added support for differential backup (for local, s3 and azure) * Improved backup UI ### 1.3.0 * Added a command to calculate the size on disk of a stream * Added a command to delete streams matching a specific pattern * Fixed the name of one of the S3 backup command options * Added backup/restore commands for Microsoft Azure * Added a config generator command that generates sample configuration based on provided criteria ### 1.2.0 * Added backup/restore commands for Amazon S3 as well as local disk * Added Restore Checkpoint for Projections * Added the ability for the CLI to check if a projection has potentially stalled ### 1.1.0 * There was an issue with versions prior to 3.7.0 of KurrentDB where bad events could be appended to $scavenge-{id} and the $scavenges streams. * With the release of version 1.1.0 of the CLI we have included a clear\_scavenge\_streams command under the admin section which will read through the $all stream and delete the $scavenges-{id} and $scavenges streams. --- --- url: 'https://docs.kurrent.io/commercial-tools/indexmap-migrate.md' --- # Indexmap migration From EventStoreDB 5 onwards, the format of the indexmap file changed. The indexmap-migrate tool converts EventStoreDB 5 indexmap files to a format compatible with previous EventStoreDB versions. You will need this if you roll back from EventStoreDB 5 to an earlier version. ## Download ## Usage ```shell indexmap-migrate [options] ``` ### Options | Option | Description | | ---------------- | ------------------------------------------------------------ | | -h, -help | Display help | | -s, -source-file | Path to the indexmap file in your EventStoreDB 5 installation | --- --- url: 'https://docs.kurrent.io/commercial-tools/ldap-plugin.md' --- # LDAP Authentication Plugin for KurrentDB This plugin allows any LDAP protocol based directory services the ability to act as the authentication authority for KurrentDB. ::: tip The LDAP plugin is included as part of the commercial builds. ::: To configure KurrentDB to use the LDAP authentication plugin, make the following changes to [the configuration file of a database node](@server/configuration/README.md). You can make these changes after installation, but you need to stop the service, change the configuration and restart the service. Set the authentication type to `ldaps`, and configure the plugin with an `LdapsAuth` section.. An example configuration file in YAML: ```yaml AuthenticationType: ldaps LdapsAuth: Host: 13.88.9.49 Port: 636 #to use plaintext protocol, set Port to 389 and UseSSL to false UseSSL: true ValidateServerCertificate: false #set this to true to validate the certificate chain AnonymousBind: false BindUser: cn=binduser,dc=mycompany,dc=local BindPassword: p@ssw0rd! BaseDn: ou=Lab,dc=mycompany,dc=local ObjectClass: organizationalPerson Filter: sAMAccountName RequireGroupMembership: false #set this to true to allow authentication only if the user is a member of the group specified by RequiredGroupDn GroupMembershipAttribute: memberOf RequiredGroupDn: cn=ES-Users,dc=mycompany,dc=local PrincipalCacheDurationSec: 60 LdapGroupRoles: 'cn=ES-Accounting,ou=Staff,dc=mycompany,dc=local': accounting 'cn=ES-Operations,ou=Staff,dc=mycompany,dc=local': it 'cn=ES-Admins,ou=Staff,dc=mycompany,dc=local': '$admins' ``` When a user authenticates against the LDAP server by attempting a bind using the provided username/password the user is assigned roles by using the active domain groups the user is part of. You can see this in the `LdapGroupRoles` section. KurrentDB has 2 built-in roles (`$admins` and `$ops`) which you can assign users. Users who belong to the `$admins` group can perform any operation in a non-restrictive manner. ## Troubleshooting common problems If there is a misconfiguration an error is logged to the server's log file in most cases. ### Invalid bind credentials specified * Verify the `BindUser` and `BindPassword` parameters ### Exception during search - 'No such Object' or 'The object does not exist' * Verify the `BaseDn` parameter ### 'Server certificate error' or 'Connect Error - The authentication or decryption has failed' * Verify that the server certificate is valid. If it is a self-signed certificate, set `ValidateServerCertificate` to `false`. ### The LDAP server is unavailable. * Verify connectivity to the LDAP server from a KurrentDB node (e.g. using `netcat` or `telnet`) * Verify the `Host` and `Port` parameters * Verify that the server certificate is valid. If it is a self-signed certificate, set `ValidateServerCertificate` to `false`. ### Error authenticating with LDAPS server. System.AggregateException: One or more errors occurred. ---> System.NullReferenceException: Object reference not set to an instance of an object. at Novell.Directory.Ldap.Connection.connect(String host, Int32 port, Int32 semaphoreId) * Due to a packaging bug, this error may be thrown when setting `UseSSL: true` on Windows. The workaround is to extract Mono.Security.dll to the *EventStore* folder (where *EventStore.ClusterNode.exe* is located) ### No errors in server logs but cannot login * Verify the `ObjectClass` and `Filter` parameters * If you have set `RequireGroupMembership` to `true`, verify that the user is part of the group specified by `RequiredGroupDn` and that the LDAP record has the `memberOf` attribute (specified by `GroupMembershipAttribute`) --- --- url: 'https://docs.kurrent.io/dev-center/index.md' --- # Tutorials ### Kurrent Tutorials Here you'll find tutorials, use cases, and best practices to help you get the most out of your Kurrent experience. --- --- url: 'https://docs.kurrent.io/dev-center/tutorials/Auto-Scavenge.md' --- # Auto-Scavenge ## Tutorial: Using Auto-Scavenge This step-by-step tutorial guides you through using, scheduling and managing **Auto-Scavenge** in KurrentDB. The Auto-scavenge feature is designed to manage and automate cluster scavenges across multiple nodes efficiently. It operates on a scheduled basis using CRON expressions, ensuring that only one node scavenge occurs simultaneously to minimize disruption. To reduce impact on cluster performance the feature prioritizes non-leader nodes for scavenging, with the leader node resigning before its own scavenge is executed. Auto-scavenge restarts or cancels scavenges if a node is lost, and allows you to pause and resume cluster scavenges as needed. With this comprehensive, automated approach, you can maintain cluster health and performance more effectively. #### Prerequisites: * [KurrentDB 25.0 or later, or EventStoreDB 24.10 LTS installed and running](http://@server/quick-start/installation.md) * [License key](http://@server/quick-start/installation.md#license-keys) for Auto-Scavenge (a valid license is required to use this feature) * Requires `$ops` or `$admin` role ### Step 1: Verify license key Ensure you have a valid **license key** to utilize Auto-Scavenge. Without the license, this feature will not function. ### Step 2: Confirm Auto-Scavenge availability By default, Auto-scavenge is bundled with KurrentDB 25.0 or later, or EventStoreDB 24.10 LTS. You can confirm its availability in the KurrentDB logs. Look for the following log message: ``` [INF] "AutoScavenge" "24.10.1" plugin enabled. ``` For instructions on accessing KurrentDB logs, refer to the [log documentation](http://@server/diagnostics/logs.md). If you do not see the "plugin enabled" entry in the log, check the configuration file to see if it has been disabled. ```yaml AutoScavenge: Enabled: false ``` If Auto-scavenge has been disabled, enable it and restart the cluster. ```yaml AutoScavenge: Enabled: true ``` ::: note You cannot enable Auto-Scavenge while `dev mode` or `mem-db` is active. ::: ### Step 3: Schedule Auto-Scavenging To schedule Auto-scavenge use the http endpoint `/auto-scavenge/configure` and post a JSON object containing a schedule key with a value in Linux crontab format. See for an explanation of the format. #### Example: Configure to scavenge at 03:00 every day ``` POST https://127.0.0.1:2113/auto-scavenge/configure Content-Type: application/json Authorization: Basic { "schedule": "0 3 * * *" } ``` Replace `` with your credentials, and `` with the correct address for your server. ### Step 4: Checking Auto-Scavenge status Submit a `GET` request to the http endpoint `/auto-scavenge/status` to return the Auto-scavenge status: ``` GET https://127.0.0.1:2113/auto-scavenge/status Authorization: Basic admin:changeit ``` The response below shows that Auto-scavenge has been scheduled and will begin running in 13 hours, 14 minutes and 32.6999115 seconds. ```json { "state": "Waiting", "schedule": "0 3 * * *", "timeUntilNextCycle": "0.13:14:32.6299115" } ``` See the [Auto-Scavenge documentation](https://docs.kurrent.io/server/v24.10/operations/auto-scavenge.html#http-endpoints) for more information on Auto-scavenge states. ### Step 5 (optional): Pausing Auto-Scavenge If the status endpoint returns `"state": "InProgress"` and you want to pause Auto-Scavenge, submit a POST request to the `/auto-scavenge/pause` endpoint: ``` POST https://127.0.0.1:2113/auto-scavenge/pause Authorization: Basic admin:changeit ``` ### Step 6 (optional): Resume Auto-Scavenge If the status endpoint returns `"state": "Paused"` and you want to resume Auto-Scavenge, submit a POST request to the `/auto-scavenge/resume` endpoint: ``` POST https://127.0.0.1:2113/auto-scavenge/resume Authorization: Basic admin:changeit ``` ### Summary By following this tutorial, you should have successfully: * Scheduled Auto-scavenge to run on KurrentDB * Verified Auto-scavenge status from KurrentDB * (Optional) Paused Auto-scavenge * (Optional) Resumed Auto-scavenge By configuring Auto-scavenge to operate on a scheduled basis, you can ensure that only one node is engaged in scavenging at any given time, and that the leader node is excluded from the process. Additionally, this setup allows for real-time status monitoring and provides the flexibility to pause and resume the Auto-scavenge operation. --- --- url: 'https://docs.kurrent.io/dev-center/tutorials/Encryption-At-Rest.md' --- # Encryption-At-Rest ## Tutorial: Setting up and using Encryption-At-Rest in KurrentDB The **Encryption-At-Rest** feature provides encryption for KurrentDB to secure database chunk files, ensuring data protection even if an attacker gains access to the physical disk. This step-by-step guide will walk you through the process of enabling and configuring this feature, including generating master keys and applying encryption. #### Prerequisites: * [KurrentDB 25.0 or later, or EventStoreDB 24.10 installed and running.](@server/quick-start/installation.md) * [License key](@server/quick-start/installation.md#license-keys) for Encryption-At-Rest (a valid license is required to use this feature). * Basic understanding of KurrentDB, encryption principles, and key management. ### Step 1: Verify license key Ensure you have a valid **license key** to use the Encryption-At-Rest feature. Without this license, the feature will not be functional. ### Step 2 (optional): Confirm Encryption-At-Rest availability By default, Encryption-At-Rest is bundled with KurrentDB 25.0 or later, or EventStoreDB 24.10 LTS. You can confirm its availability in the KurrentDB logs. Look for the following log message: ```text:no-line-numbers [INF] ClusterVNodeHostedService Loaded SubsystemsPlugin plugin: encryption-at-rest 24.10.0.1316 ``` Refer to the [log documentation](@server/diagnostics/logs.md) for instructions on accessing KurrentDB logs. ### Step 3: Generate a master key To encrypt the data, you must first generate a **master key** using the `es-cli` tool ([available to be downloaded in Cloudsmith](https://cloudsmith.io/~eventstore/repos/eventstore/packages/?q=es-cli)). This key is used to derive the data keys that will encrypt chunk files. Run the following command to generate a master key: ```bash:no-line-numbers es-cli encryption generate-master-key ``` Place the generated master key in a secure directory on a **separate drive** from the database files, ensuring enhanced security. The key should be stored in the directory you will configure in the next step, for example, `/secure/keys/`. ### Step 4: Configure and enable Encryption-At-Rest #### Step 4.1: Add the configuration file 1. Navigate to the `config` directory within the KurrentDB installation.\ There are multiple [configuration mechanisms](@server/configuration/) available. Since options set in a JSON configuration override those set in a YAML configuration, here is an example using JSON. You can also find a [YAML configuration file example in the documentation](@server/security/#configuration). Create a new JSON configuration file (e.g., `encryption-config.json`) with the following content: ```json { "EventStore": { "Plugins": { "EncryptionAtRest": { "Enabled": true, "MasterKey": { "File": { "KeyPath": "/secure/keys/" // Update with the actual path to your keys } }, "Encryption": { "AesGcm": { "Enabled": true, "KeySize": 256 // Optional: 128, 192, 256 bits (default is 256) } } } } } } ``` 2. Make sure to replace `/secure/keys/` with the actual path where the generated master key is stored. #### Step 4.2: Set the encryption algorithm In the main KurrentDB configuration file, add the following line to specify the encryption transformation: ```bash:no-line-numbers Transform: aes-gcm ``` This ensures that the **AES Galois Counter Mode (AES-GCM)** encryption algorithm is applied to new chunks. ### Step 5: Confirm Encryption-At-Rest activation After configuring Encryption-At-Rest, you can confirm it is enabled and active by checking the log file. You should see the following log messages: ```text:no-line-numbers ... [141828, 1,11:42:45.325,INF] Encryption-At-Rest: Loaded master key source: "File" [141828, 1,11:42:45.340,INF] Encryption-At-Rest: (File) Loaded master key: 1 (256 bits) [141828, 1,11:42:45.345,INF] Encryption-At-Rest: Active master key ID: 1 [141828, 1,11:42:45.345,INF] Encryption-At-Rest: Loaded encryption algorithm: "AesGcm" [141828, 1,11:42:45.347,INF] Encryption-At-Rest: (AesGcm) Using key size: 256 bits [141828, 1,11:42:45.401,INF] Loaded the following transforms: Identity, Encryption_AesGcm [141828, 1,11:42:45.402,INF] Active transform set to: Encryption_AesGcm ... ``` ### Step 6 (optional): Verify encryption Once Encryption-At-Rest is enabled, new chunk files created or scavenged will be encrypted using the configured encryption algorithm. You can verify encryption with the following tests: * **New chunks test**: * Create or scavenge chunk files and verify by checking the logs to confirm that new chunks have been encrypted. * Example log message that indicates the active encryption mechanism is operational:\ `[141828, 1,11:42:45.401,INF] Active transform set to: Encryption_AesGcm` * **Unauthorized access test**: * Attempt to access encrypted files without proper keys to confirm they cannot be read. * For example, you can attempt to read encrypted chunk files directly using a text editor, hexadecimal viewer, or unsupported tool. Without the proper master key, the contents should appear garbled or unreadable. ### Step 7: Important considerations * **Irreversible encryption:** Once encryption is enabled, you **cannot revert** to an unencrypted state if any new chunk file has been created or scavenged. To decrypt the encrypted chunks, special tooling would be required (currently unavailable). * **Master key management:** * If your master key is compromised, the entire database can be decrypted. Always secure the key on a different drive than your database files. * If your master key is lost or corrupted, you’ll lose access to your data. Always keep a backup of your master key in a safe place. * **Scope of protection**: Encryption-At-Rest encrypts chunk files but does not encrypt index files. Additionally, this feature does not protect against memory-dump-based attacks, which require separate mitigation strategies. ### Summary By following this tutorial, you have successfully configured and enabled Encryption-At-Rest in KurrentDB. This encryption setup ensures that your chunk files are securely encrypted, protecting your data from potential disk-based attacks. --- --- url: >- https://docs.kurrent.io/dev-center/tutorials/eventstore-to-kurrent-migration-java-client.md description: >- Automatically migrate your Java applications from the EventStore client to the new KurrentDB client using OpenRewrite. --- Following the [Event Store rebrand to Kurrent](https://www.kurrent.io/blog/event-store-is-evolving-to-kurrent), all client libraries have been updated with new package names and improved APIs. This guide shows you how to automatically migrate your Java application from `com.eventstore.db-client-java` to the new `io.kurrent.kurrentdb-client`. ## Why Migrate? * **Latest features and improvements**: The KurrentDB client includes performance enhancements and new capabilities * **Automated migration**: Use [OpenRewrite](https://docs.openrewrite.org/) to handle the migration automatically with minimal manual effort ## Prerequisites * Java 8 or above * Maven or Gradle build tool * Existing application using `com.eventstore.db-client-java` dependency ## What Gets Updated This migration will automatically update: * **Dependencies**: `com.eventstore:db-client-java` → `io.kurrent:kurrentdb-client` * **Package imports**: `com.eventstore.dbclient` → `io.kurrent.dbclient` * **Class names**: `EventStoreDBClient` → `KurrentDBClient` * **Connection strings**: `esdb://` → `kurrent://` * **Method names**: `expectedRevision()` → `streamRevision()` ## Step 1: Create Migration Recipe Create a file named `rewrite.yml` in the root directory of your Java project: ::::: details Click to show rewrite.yml contents ```yaml --- type: specs.openrewrite.org/v1beta/recipe name: io.kurrent.java.UpgradeClient displayName: Migrate to Kurrent Client description: > Migrate applications to the latest KurrentDB from EventStoreDB client. This recipe will modify an application's pom files, migrate to the new names of classes, update package information and change the connection strings. tags: - KurrentDB - Kurrent # Update dependencies recipeList: - org.openrewrite.java.dependencies.ChangeDependency: oldGroupId: com.eventstore oldArtifactId: db-client-java newGroupId: io.kurrent newArtifactId: kurrentdb-client newVersion: 1.0.2 # Update class names - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBClientBase newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBClientBase - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBClient newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBClient - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBClientSettings newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBClientSettings - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBConnectionString newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBConnectionString - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBProjectionManagementClient newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBProjectionManagementClient - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.EventStoreDBPersistentSubscriptionsClient newFullyQualifiedTypeName: io.kurrent.dbclient.KurrentDBPersistentSubscriptionsClient - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: com.eventstore.dbclient.ExpectedRevision newFullyQualifiedTypeName: io.kurrent.dbclient.StreamState # Update package names - org.openrewrite.java.ChangePackage: oldPackageName: com.eventstore.dbclient newPackageName: io.kurrent.dbclient # Update method names and connection strings - org.openrewrite.text.FindAndReplace: find: expectedRevision( replace: streamRevision( caseSensitive: true - org.openrewrite.text.FindAndReplace: find: esdb:// replace: kurrent:// caseSensitive: true - org.openrewrite.text.FindAndReplace: find: esdb+discover:// replace: kurrent+discover:// caseSensitive: true --- ``` ::::: ## Step 2: Run the Migration Choose the command based on your build tool: ::: tabs @tab Maven ```bash # Run the migration (this will scan and update your code automatically) ./mvnw -U org.openrewrite.maven:rewrite-maven-plugin:run \ -Drewrite.activeRecipes=io.kurrent.java.UpgradeClient \ -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-java-dependencies:1.29.0,org.openrewrite.recipe:rewrite-migrate-java:3.3.0 ``` If you're not using the Maven wrapper, replace `./mvnw` with `mvn`. @tab Gradle First, create an `init.gradle` file with the following contents: ```kotlin initscript { repositories { maven { url "https://plugins.gradle.org/m2" } } dependencies { classpath("org.openrewrite:plugin:latest.release") } } rootProject { plugins.apply(org.openrewrite.gradle.RewritePlugin) dependencies { rewrite("org.openrewrite.recipe:rewrite-java-dependencies:latest.release") rewrite("org.openrewrite.recipe:rewrite-migrate-java:latest.release") } afterEvaluate { if (repositories.isEmpty()) { repositories { mavenCentral() } } } } ``` Then run: ```bash # Run the migration gradle rewriteRun --init-script init.gradle -Drewrite.activeRecipe=io.kurrent.java.UpgradeClient ``` ::: ## Step 3: Review Changes The migration will show output similar to this: ```bash [INFO] Using active recipe(s) [io.kurrent.java.UpgradeClient] [WARNING] Changes have been made to pom.xml by: [WARNING] org.openrewrite.java.dependencies.ChangeDependency: {oldGroupId=com.eventstore, oldArtifactId=db-client-java, newGroupId=io.kurrent, newArtifactId=kurrentdb-client, newVersion=1.0.2} [WARNING] Changes have been made to LoanApplicationDemo/Java/src/main/java/com/example/CreditController.java by: [WARNING] org.openrewrite.java.ChangeType: {oldFullyQualifiedTypeName=com.eventstore.dbclient.EventStoreDBClient, newFullyQualifiedTypeName=io.kurrent.dbclient.KurrentDBClient} ... [WARNING] Please review and commit the results. [WARNING] Estimate time saved: 1h 20m ``` ## Important: Manual Review Required ### ExpectedRevision Changes The migration automatically converts `expectedRevision()` calls to `streamRevision()`, but this assumes you're passing a `long` parameter. If you're passing a `StreamState` object, you'll need to manually change these to `streamState()`: **Before:** ```java // This will be auto-converted correctly appendOptions.expectedRevision(5L); // This will need manual fixing after migration appendOptions.expectedRevision(StreamState.NO_STREAM); ``` **After migration:** ```java // Auto-converted (correct) appendOptions.streamRevision(5L); // Needs manual fix - change to: appendOptions.streamState(StreamState.NO_STREAM); ``` ## After Migration 1. **Test your application**: Run your test suite to ensure everything works correctly. 2. **Fix any StreamState calls**: Review compilation errors for `streamState()` vs `streamRevision()` usage. 3. **Verify connection strings**: Ensure all `kurrent://` connections work as expected. 4. **Update documentation**: Update any internal docs referencing the old client. 5. **Clean up**: Remove the `rewrite.yml` and `init.gradle` files if no longer needed. ## Need Help? If you encounter issues during migration: * Check the [KurrentDB Java client documentation](@clients/grpc/getting-started.md). * Review the [OpenRewrite documentation](https://docs.openrewrite.org) for advanced configuration. * Open an issue in the [KurrentDB Java client repository](https://github.com/kurrent-io/KurrentDB-Client-Java). The migration should handle 95% of the changes automatically, saving you significant time while ensuring consistency across your codebase. --- --- url: 'https://docs.kurrent.io/dev-center/tutorials/HTTP_Connector.md' --- ## Tutorial: Setting up and using an HTTP Sink in KurrentDB Connectors simplify the integration of KurrentDB data into other systems. Each connector runs on the server-side and uses a catch-up subscription to receive events, filter or transform them, and push them to an external system via a sink. The following are the available Kurrent sinks: 1. [Kafka Sink](/server/v24.10/features/connectors/sinks/kafka.html) 2. [MongoDB Sink](/server/v24.10/features/connectors/sinks/mongo.html) 3. [RabbitMQ Sink](/server/v24.10/features/connectors/sinks/rabbitmq.html) 4. [HTTP Sink](/server/v24.10/features/connectors/sinks/http.html) 5. [Serilog Sink](/server/v24.10/features/connectors/sinks/serilog.html) This step-by-step tutorial guides you through setting up a connector using the **HTTP Sink** in KurrentDB. This feature allows KurrentDB to push event data to an http endpoint. #### Prerequisites * [KurrentDB 25.0 or later, or EventStoreDB 24.10 installed and running](/server/v24.10/quick-start/installation.html) ### Step 1: Set up an HTTP endpoint using PostBin In this tutorial, you will use PostBin to create an HTTP endpoint that consumes events from KurrentDB. PostBin supports creating a unique URL to collect requests from KurrentDB (GET, POST, PUT, PATCH, DELETE, etc.). **1.1** Navigate to and click **Create Bin**. **1.2** You will be directed to a page with the endpoint URLs for curl, wget, and echo. Copy the bin URL for curl. *Note: If you are testing with an endpoint from your custom application, ensure no Cross-Origin Resource Sharing (CORS) issues prevent the data from being transmitted from KurrentDB.* ### Step 2: Create the connector Create a connector by sending a `POST` request to `connectors/{connector_id}`, where `{connector_id}` is a unique identifier of your choice for the connector. **2.1:** Create a file named `create_connector.sh`. **2.2:** Add the following content to the file. Ensure you **replace the value for “Url”** with the curl URL you copied from Postb.in. ::: tabs @tab Bash ```bash:no-line-numbers #!/bin/bash curl -i -X POST \ http://localhost:2113/connectors/test-app \ -H "Content-Type: application/json" \ -u "admin:changeit" \ -d '{ "settings": { "InstanceTypeName": "http-sink", "Url": "https://www.postb.in/1736471171412-2404703341890", "Subscription:Filter:Expression": "order-.*?" } }' ``` ::: The following section provides additional information about the file contents: * `http://localhost:2113/connectors/test-app` provides a **unique name identifier** for the connector ("test-app" in this case). * `"InstanceTypeName": "http-sink"` provides the type of connector sink (Http Sink in this case). * `"Url": "https://www.postb.in/1736471171412-2404703341890"` provides the HTTP URL endpoint to which the KurrentDB will send the data. * `"Subscription:Filter:Expression": "order-.*?"` filters the results for streams starting with "order-". **2.3:** Run the `create_connector.sh` script. After running the script and creating the connector, you should receive an `HTTP 200 OK` message similar to the one below: ```text:no-line-numbers HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Mon, 20 Jan 2025 18:20:43 GMT Server: Kestrel Transfer-Encoding: chunked ``` ### Step 3: Check the connector status Now that you’ve created a connector, you can check its status and settings. This process is helpful when troubleshooting. **3.1:** Create a file named `status.sh`. **3.2:** Add the following content to the file: ::: tabs @tab Bash ```bash:no-line-numbers #!/bin/bash JSON=$(cat <- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/introduction.md --- # Overview ![](./images/use-the-right-database-for-the-job.png#light) ![](./images/use-the-right-database-for-the-job-dark.png#dark) Modern applications often deal with diverse data access patterns that a single database can’t efficiently handle. That’s where you can leverage a database mix-and-match approach where different types of databases for different parts of your system can be used based on their strengths. For example: * **PostgreSQL** for general queries. * **Redis** for fast lookups. * **MongoDB** for web frontends. This strategy is beneficial because no single database is perfect for every use case. Selecting the most optimal database for each specific task enhances performance, scalability, and developer efficiency. ::: info This is also popularly known as **Polyglot Persistence**. ::: ## Traditional Approach Traditionally, systems use a single relational database for all operations, and pushing updates to other data stores is not reliable or straightforward. To distribute data across multiple databases, developers typically implement synchronization mechanisms like: * **Manual synchronization and ETL jobs** to periodically replicate data. * **Message brokers** (e.g., Kafka, RabbitMQ) that propagate updates to various services. However, these methods can have drawbacks: * Risk of synchronization failure. * Noticeable delays due to batch processing. * Difficulty in recovery after failures. * Issues with message loss, duplication, or ordering. Such complexities increase the developers' workload to maintain synchronization and consistency. ## Mix-and-Match Database Effectively with KurrentDB KurrentDB captures all changes (events) immutably within your system, which can be consumed by any number of downstream systems or databases in real time. This means you can reliably synchronize a variety of databases or data models, each optimized for a specific use case, without the complexity, risks, or delays of traditional batch jobs or external messaging systems. ### Benefits include: * **Reliable and Simple Synchronization**: * Events stored immutably in sequential order help detect and deal with typical synchronization issues like message loss or duplication. * **Real-time Updates**: * KurrentDB allows real-time event stream subscriptions, enabling instant updates across data stores like Redis and MongoDB. * **Efficient Error Recovery**: * Easily fix synchronization issues by replaying events to rebuild accurate read models. * **Future-Proof Architecture**: * Instantly populate new specialized databases for unforeseen needs without modifying existing components. ## How to Mix-and-Match Databases with KurrentDB Implementing this strategy with KurrentDB involves a few simple steps: #### 1. Capture Events in KurrentDB Persist each event immutably, creating a reliable event log as the authoritative data source. #### 2. Build Optimized Read Models Project or transform events into specialized databases: * **MongoDB** for frontend queries. * **Redis** for caching and rapid lookups. * **Relational DBs** for complex analytics and reporting. #### 3. Real-time Synchronization Subscribe databases directly to KurrentDB's event streams, ensuring automatic real-time updates without manual syncing. #### 4. Easy Error Recovery Quickly recover from synchronization issues by replaying events to rebuild accurate read models. --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/tutorial/tutorial-1.md --- # Part 1: Setup and Initialize KurrentDB In this part, you will start a GitHub Codespaces session in your browser. ::: info GitHub Codespaces provides an instant and preconfigured development environment all within your browser. This environment contains all the tools and code to complete this tutorial. To learn more about Github Codespaces, [click here](https://github.com/features/codespaces). ::: You will then initialize KurrentDB by appending sample events that mimic an e-commerce application. The events are appended using a data generator program. ## Step 1: Set up Your Codespaces 1. Click the button below to initiate Codespaces and ensure following values are selected: [![](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true\&ref=main\&repo=951198039\&skip_quickstart=true) | Configuration Option | Selection | |-----------------------------|--------------------------| | Branch | `main` | | Dev container configuration | `Mix-and-Match Database` | | Region | Any value | | Machine type | Any value | Log in to GitHub if required. 2. Wait for your Codespace to build. This can take up to a few minutes. ::: tip For this quickstart, you can safely ignore and close any Codespaces notifications that appear on the bottom right of the page. ::: ## Step 2: Start and Initialize KurrentDB with Sample Events 1. Once your Codespace is loaded, run the following command in the terminal to append sample events to KurrentDB: ```sh ./scripts/1-init-data.sh ``` This is a custom script for this quickstart to help start KurrentDB in Docker. 2. You will see the following message printed in the terminal: ``` 🚀 KurrentDB Server has started!! 🚀 URL to the KurrentDB Admin UI 👉: https://XXXXXXXXX.XXX Appended sample data to KurrentDB ``` 3. Copy the URL printed in the terminal from the last step. 4. Open a new browser tab. 5. In the address bar of the new tab, paste the URL and navigate to it. 6. This will display the KurrentDB Admin UI. !\[KurrentDB Admin UI Dashboard]\(../images/admin-ui.png =300x) ## Step 3: Browse Sample Events in KurrentDB Admin UI 1. Click the `Stream Browser` link from the top navigation bar. 2. Under `Recently Changed Streams`, click the `$ce-cart` link. ::: info Understanding Category System Projection The `$ce-cart` stream contains events from all the carts in KurrentDB. This uses the category system projection stream feature. For more information, see [System Projections](https://docs.kurrent.io/server/v25.0/features/projections/system.html#by-category). ::: 3. You should see an ordered list of the appended events associated with two distinct, virtual shopping carts. ::: info Introducing shopping cart events In KurrentDB, events for each shopping cart are appended to a stream like `cart-2fbe05d1dcf043d782ea24923298ae3a`, where `2fbeone05d1dcf043d782ea24923298ae3a` is the cart's unique ID. The cart will contain events like these: | Event | Description | |----------------------------|-------------------------------------------------------------------------------------| | `VisitorStartedShopping` | When a visitor starts shopping and a cart is created. | | `CustomerStartedShopping` | When a known customer starts shopping and a cart is associated with their ID. | | `CartShopperGotIdentified` | When a customer's identity is linked to a cart (e.g., a visitor logged in) | | `ItemGotAdded` | When an item is added to the cart, including details like quantity, price, and tax. | | `ItemGotRemoved` | When an item is removed from the cart, including the quantity removed. | | `CartGotCheckedOut` | When a cart is checked out and converted into an order. | | `CartGotAbandoned` | When a cart is abandoned after being idle for a specified duration. | ::: ::: info Quick Quiz What were the quantities of each product in the shopping carts? ::: --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/tutorial/tutorial-2.md --- # Part 2: Project KurrentDB Events to Postgres Now that KurrentDB is filled with shopping cart events, you will learn how to project these events to other databases as read models. You will do this by executing a few sample applications written for this part. #### Introducing the Postgres Projection Application This application projects KurrentDB events to Postgres relational tables. The tables can be queried for reporting using standard SQL queries. To do this, the application subscribes to the shopping cart events. For each event it receives, it inserts or updates a record to a cart and cart\_item table in Postgres. The schemas of the tables are as follows: #### `carts` Table ```sql CREATE TABLE IF NOT EXISTS carts ( cart_id TEXT PRIMARY KEY, customer_id TEXT NULL, status TEXT NOT NULL DEFAULT 'STARTED', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL ) ``` #### `cart_items` Table ```sql CREATE TABLE IF NOT EXISTS cart_items ( cart_id TEXT NOT NULL, product_id TEXT NOT NULL, product_name TEXT NOT NULL, quantity INTEGER NOT NULL, currency TEXT NULL, price_per_unit DECIMAL(10,2) NOT NULL, tax_rate DECIMAL(5,2) NOT NULL, updated_at TIMESTAMP NOT NULL, PRIMARY KEY (cart_id, product_id), FOREIGN KEY (cart_id) REFERENCES carts(cart_id) ON DELETE CASCADE ) ``` ## Step 4: Execute Projection Application 1. Run the following command in the terminal to execute the projection applications: ```sh ./scripts/2-start-projections.sh ``` ## Step 5. Review the Projected Read Models in Postgres In this step, you will review the records in the `carts` and `cart_items` tables that were created from executing the applications in the previous step: 1. Run the following command in the terminal to start Postgres CLI: ```sh docker exec -it postgres psql -U postgres ``` You will receive a message, like below, printed in the terminal: ``` psql (16.8 (Debian 16.8-1.pgdg120+1)) Type "help" for help. postgres=# ``` 2. Run the following command in Postgres CLI to list the shopping carts projected from the events: ```sql select * from carts; ``` You should see two carts in the table. ::: info Quick Quiz The carts' status should be checked out or abandoned. Do the cart statuses match those you saw when manually calculating the number of events in each cart during step 3 in the last part? ::: ```sql select * from cart_items; ``` You will see a few items in each cart. ::: tip If you're stuck with the output and can't exit, press `q` to exit. You're likely in paging mode because the output has overflowed. ::: ::: info Quick Quiz Do the products and their quantities in the carts match the totals calculated in step 3 in the last part? ::: 3. Exit Postgres CLI by running the command: ``` exit ``` ## Step 6. Examine the Postgres Projection Application Codebase Projecting KurrentDB events to read models in another database often adheres to the following pattern: 1. Retrieve the last checkpoint 2. Subscribe to events from a stream from the checkpoint 3. Process each event by updating the read model and checkpoint in the database You will examine how this pattern is applied to the Postgres projection application. 1. Run the following command in the terminal to open the main program for the Postgres projection application: ```sql code ./PostgresProjection/Program.cs ``` Most of the code snippets leveraged in this step can be found within this file in Codespaces. 2. Locate and examine the code that retrieves the last checkpoint: ```cs var checkpointValue = postgres.QueryFirstOrDefault( // Get the checkpoint value from PostgreSQL checkpoint table "SELECT checkpoint " + "FROM checkpoints " + "WHERE read_model_name = 'carts'"); var streamPosition = checkpointValue.HasValue // Check if the checkpoint exists.. ? FromStream.After(StreamPosition.FromInt64(checkpointValue.Value)) // if so, subscribe from stream after checkpoint.. : FromStream.Start; // otherwise, subscribe from the start of the stream ``` A `SELECT` is statement used retrieve the checkpoint. If no checkpoint is found or it is the first time the application is executed, we can retrieve the default start position. ::: info Understanding Checkpoint A projection often uses a checkpoint to recover the position of the last processed event. This way, when an application unexpectedly crashes mid-process, the projection does not have to process all the previously processed events. ::: ::: info Storing Checkpoints in Relational Databases Checkpoints for database projections can often be saved to a separate checkpoint table similar to this: ```sql CREATE TABLE IF NOT EXISTS checkpoints ( read_model_name TEXT PRIMARY KEY, checkpoint BIGINT NOT NULL ) ``` ::: 3. Locate and examine the code that subscribes to stream: ```cs await using var subscription = esdb.SubscribeToStream( // Subscribe events.. "$ce-cart", // from the cart category system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) ``` A subscription is created that subscribes to events from the `$ce-carts` stream. The subscription will only retrieve events starting from `streamPosition` in the stream (i.e., the checkpoint retrieved from the previous step). ::: info Different Types of Subscriptions This sample uses catch-up subscriptions to subscribe to events. You can also use persistent subscriptions or connectors to achieve a similar result. For more information about catch-up subscriptions, [click here](https://docs.kurrent.io/clients/subscriptions.html). For more information about persistent subscriptions, [click here](https://docs.kurrent.io/clients/persistent-subscriptions.html). For more information about connectors, [click here](https://docs.kurrent.io/server/v24.10/features/connectors/) ::: 4. Locate and examine the code that processes each event: ```cs await foreach (var message in subscription.Messages) // Iterate through the messages in the subscription { if (message is not StreamMessage.Event(var e)) continue; // Skip this message if it is not an event postgres.BeginTransaction(); // Begin a transaction for Postgres postgres.Execute(CartProjection.Project(e)); // Update the Postgres read model based on the event being processed postgres.Execute( "INSERT INTO checkpoints (read_model_name, checkpoint) " + // Insert checkpoint into the checkpoint table "VALUES (@ReadModelName, @Checkpoint) " + "ON CONFLICT (read_model_name) DO " + // If the read model name already exists.. "UPDATE SET checkpoint = @Checkpoint", // then update the checkpoint value new { ReadModelName = "carts", Checkpoint = e.OriginalEventNumber.ToInt64() // Get the stream position from the event }); postgres.Commit(); // Commit the transaction only if the read model and checkpoint are updated successfully Console.WriteLine($"Projected event " + $"#{e.OriginalEventNumber.ToInt64()} " + $"{e.Event.EventType}"); } ``` For each event, the projection will: * Start a database transaction, * Update the `carts`, `cart_items` tables in the database, * Update the `checkpoint` table in the database, * Commit the database transaction ::: tip To ensure atomicity and consistency, the updates to the read model and checkpoint tables should be committed within the same transaction. This guarantees that both updates succeed or fail together, preventing data inconsistencies like outdated read models or incorrect checkpoint positions. It also simplifies error recovery and ensures the system remains in sync. ::: ::: info Exactly-once processing This implementation ensures exactly-once processing by using KurrentDB for reliable persistence, idempotent projection logic, and transactional updates. The read model and checkpoint are updated atomically, preventing duplicates or inconsistencies, unlike traditional message brokers that rely on at-least-once or at-most-once delivery. ::: The `CartProjection.Project(e)` function above returns a SQL command that updates the read model depending on the event. 5. Run the following command in the terminal to open the code that performs for the Postgres projection: ```sql code ./PostgresProjection/CartProjection.cs ``` 6. Locate and examine the code that handles the projection for the `CustomerStartedShopping` event: ```cs private static IEnumerable? Project(CustomerStartedShopping evt) { var sql = @"INSERT INTO carts(cart_id, customer_id, status, created_at, updated_at) VALUES(@CartId, @CustomerId, @Status, @Timestamp, @Timestamp) ON CONFLICT(cart_id) DO NOTHING"; var parameters = new { CartId = evt.cartId, CustomerId = evt.customerId, Status = "STARTED", Timestamp = evt.at }; yield return new CommandDefinition(sql, parameters); } ``` This returns a sql command that inserts a cart if `CustomerStartedShopping` event is received. 7. Locate and examine the code that handles the projection for the `CartGotCheckedOut` event: ```cs private static IEnumerable? Project(CartGotCheckedOut evt) { var sql = @"UPDATE carts SET status = @Status, updated_at = @Timestamp WHERE cart_id = @CartId"; var parameters = new { CartId = evt.cartId, Status = "CHECKED_OUT", Timestamp = evt.at }; yield return new CommandDefinition(sql, parameters); } ``` This returns a command that updates a cart's status to `CHECKED_OUT` if the `CartGotCheckedOut` event is received. ::: info Quick Quick What does `CartProjection.Project()` return when `ItemGotRemoved` is received? ::: --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/tutorial/tutorial-3.md --- # Part 3: Project KurrentDB Events to Redis #### Introducing the Redis Projection Application This application projects KurrentDB events to Redis sorted sets to calculate the top 10 products across all carts over the past 24 hours. To do this, it subscribes to the shopping cart events. Each item added or removed event the application receives will increment/decrement the product's quantity in a Redis sorted set for the current hour. ## Step 7: Review the Projected Read Models in Redis In this step, you will review the top 10 products that were recorded in Redis from executing the applications in a previous step: 1. Run the following command in the terminal to start Redis CLI: ```sh docker exec -it redis redis-cli ``` You will see a message, like below, printed in the terminal: ``` 127.0.0.1:6379> ``` 2. Run the following command in the Redis CLI to list all keys in Redis: ``` KEYS * ``` You will see a list that is similar to this: ``` 1) "checkpoint" 2) "product-names" 3) "top-10-products:2025041508" ``` 3. Run the following command in the Redis CLI to list the most popular products added to a cart. Replace top-10-product:YYYYMMDDHH with the actual top-10-products key listed in the previous step. ``` ZREVRANGE ***REPLACES THIS WITH top-10-products:YYYYMMDDHH KEY FOUND ABOVE*** 0 9 WITHSCORES ``` You will see a list that is similar to this: ``` 1) "5449310139799" 2) "9" 3) "4291118428480" 4) "6" 5) "0563658703704" 6) "4" 7) "2256276792349" 8) "1" ``` The 13-digit number is the product ID, followed by its quantity across all shopping carts. In this case, `5449310139799` is the most popular product with 9 of them across all carts. ::: info Quick Quiz Given that the quantity for a product above is the total added minus the total removed from a cart, pick one of the products above and confirm it matches what the events in step 3 from the previous part indicate. ::: 4. Exit the Redis CLI by running the command: ``` exit ``` ## Step 8. Examine the Redis Projection Application Codebase Similar to step 6, projecting KurrentDB events to read models in another database like Redis can also follow the same pattern: 1. Retrieve the last checkpoint 2. Subscribe to events in a stream from the checkpoint 3. Process each event by updating the read model and checkpoint in the database You will examine how this pattern is applied to the Redis projection application. 1. Run the following command in the terminal to open the main program for the Postgres projection application: ```sql code ./RedisProjection/Program.cs ``` Most of the code snippets included in this step can be found in this file. 2. Locate and examine the code that retrieves the last checkpoint: ```cs var checkpointValue = redis.StringGet("checkpoint"); // Get the checkpoint value from redis var streamPosition = long.TryParse(checkpointValue, out var checkpoint) // Check if it exists and convertible to long ? FromStream.After(StreamPosition.FromInt64(checkpoint)) // If so, set var to subscribe events from stream after checkpoint : FromStream.Start; // Otherwise, set var to subscribe to events from the stream from the start. ``` The `redis.StringGet()` statement can retrieve the checkpoint. If no checkpoint is found or it is the first time the application is executed, we can retrieve the default start position. 3. Locate and examine the code that subscribes to stream: ```cs await using var subscription = esdb.SubscribeToStream( // Subscribe events.. "$ce-cart", // from the cart category system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) ``` A catch-up subscription is created that subscribes to events from the `$ce-carts` stream. The subscription will only retrieve events starting from `streamPosition` in the stream (i.e., the checkpoint retrieved from the previous step). 4. Locate and examine the code that processes each event: ```cs await foreach (var message in subscription.Messages) // Iterate through the messages in the subscription { if (message is not StreamMessage.Event(var e)) continue; // Skip if message is not an event var txn = redis.CreateTransaction(); // Create a transaction for Redis if (!CartProjection.TryProject(txn, e)) continue; // Project the event into Redis txn.StringSetAsync("checkpoint", e.OriginalEventNumber.ToInt64()); // Set the checkpoint to the current event number txn.Execute(); // Execute the transaction } ``` For each event, the projection will: * Start a Redis transaction, * Save the appropriate key-value pairs in the database, * Update the `checkpoint` key in the database, * Commit the Redis transaction The `CartProjection.TryProject()` function above will try to project the event into the appropriate key-value pair in Redis. 5. Run the following command in the terminal to open the code that performs the Redis projection: ```sql code ./RedisProjection/CartProjection.cs ``` 6. Locate and examine the code that handles the projection for the `ItemGotAdded` event: ```cs public static void Project(ITransaction txn, ItemGotAdded addedEvent) { var hourKey = $"top-10-products:{addedEvent.at:yyyyMMddHH}"; // Create a key for the current hour var productKey = addedEvent.productId; // Use the product ID as the member in the sorted set var productName = addedEvent.productName; // Assuming `productName` is part of the event txn.SortedSetIncrementAsync(hourKey, productKey, addedEvent.quantity); // Increment the quantity of the product in the sorted set txn.HashSetAsync("product-names", productKey, productName); // Store product name in a hash; Console.WriteLine($"Incremented product {addedEvent.productId} in " + $"{hourKey} by {addedEvent.quantity}"); } ``` If the event is `ItemGotAdded`, then a Redis sort set is incremented with the product key for that particular hour. A hash set is also used to map product IDs to product names (this is used later in the Demo Web Page to construct a table of the top 10 products). 7. Locate and examine the code that handles the projection for the `ItemGotRemoved` event: ```cs public static void Project(ITransaction txn, ItemGotRemoved removedEvent) { var hourKey = $"top-10-products:{removedEvent.at:yyyyMMddHH}"; // Create a key for the current hour var productKey = removedEvent.productId; // Use the product ID as the member in the sorted set txn.SortedSetDecrementAsync(hourKey, productKey, // Decrement the quantity of the product in the sorted set removedEvent.quantity); Console.WriteLine($"Decremented product {removedEvent.productId} in " + $"{hourKey} by {removedEvent.quantity}"); } ``` If the event is `ItemGotRemoved`, then a Redis sort set is decremented with the product key for that particular hour. --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/tutorial/tutorial-4.md --- # Part 4: Project KurrentDB Events in Real-Time Now that the read models on the databases are synchronized with events on KurrentDB, you will learn how to synchronize the read models in real time. ## Step 9: Browse the Demo Web Page 1. Run the following command in the terminal to start the Demo Web Page application: ```sh ./scripts/3-start-demo-web-page.sh ``` You will see the following message printed in the terminal: ``` URL to the Demo web UI 👉: https://XXXXXXXXX.XXX ``` 2. Open a new browser tab. 3. In the address bar of the new tab, paste the URL and navigate to it. 4. This will display a demo web app for this sample. This page displays the top 10 products added to carts in the past 24 hours. This table is retrieved with data from Redis generated from the Redis projection. 5. Click `Carts Table (Postgres)` in the header. 6. This page displays the contents of the cart and items tables in Postgres, which were generated from the Postgres projection. ::: info Quick Quiz Do the products and quantity in the carts match what you calculated and queried in previous Quick Quizzes? ::: ::: info Quick Quiz Are the contents of the Top 10 Products (Redis) table and the Carts Table (Postgres) in sync? ::: ## Step 10: Start the Live Data Generator 1. Run the following command in the terminal to start a live data generator: ```sh ./scripts/4-start-live-data-gen.sh ``` You will see the following message printed in the terminal: ``` URL to the KurrentDB Admin UI 👉: https://XXXXXXXXX.XXX URL to the Demo Web Page 👉: https://XXXXXXXXX.XXX 10:10:33 info: edb-commerce[0] Executing command 'live-data-set' with settings {"ConfigurationFile":"./data/datagen.live.config","ConnectionString":"esdb://localhost:2113?tls=false"} 10:10:34 info: edb-commerce[0] Generating 12 products 10:10:35 info: edb-commerce[0] Generating 640 carts 10:10:35 info: edb-commerce[0] With 399 carts concurrently ``` The tool is now running in the background. ## Step 11: Watch the Read Models Update in Real-Time 1. Navigate to the KurrentDB Admin UI 2. Click the `Stream Browser` link from the top navigation bar. 3. Under `Recently Changed Streams`, click `$ce-cart` link. 4. Notice how new events are being appended to the stream in real time 5. Return to the Demo Web Page and click on the `Top 10 Products` link from the top navigation bar. Notice how the Top 10 products are being updated in real-time. 6. Click on the `Carts Table` link from the top navigation bar. Notice how the data is updated and that new carts are available in the Postgres tables. Click the `Refresh` button to see the most recent data. 7. Return to the terminal and stop the live data generator tool by typing Ctrl + C. ## Step 12: Understanding Catch-up Subscription and Real-Time Processing 1. Run the following command in the terminal to open the main program for the Postgres projection application: ```sql code ./PostgresProjection/Program.cs ``` 2. Locate and examine the code that subscribes to stream ```cs await using var subscription = esdb.SubscribeToStream( // Subscribe events.. "$ce-cart", // from the cart category system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) ``` The subscription will only retrieve events starting from `streamPosition` in the stream. If `streamPosition` is not at the end of the stream, the subscription will first return all the events from that position to the end of the stream. Afterwards, it will listen for any new events appended in real time. If `streamPosition` is at the end of the stream, then the subscription will automatically listen to new events in real time. ::: info For more info on subscribing to a stream for live updates, [click here](https://docs.kurrent.io/clients/subscriptions.html#subscribing-to-a-stream-for-live-updates) ::: --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/mix-and-match-database/tutorial/tutorial-intro.md --- # Introduction This tutorial will guide you through the Mix-and-Match Database sample with KurrentDB using GitHub Codespaces. ## Objectives In this tutorial, you will: * Learn how to project events from KurrentDB into read models in different databases * Experience how to update read models catch-up subscription and checkpoints * Understand how to update read models in real time ## Prerequisites Before starting, ensure you have the following: * A GitHub account to use GitHub Codespaces * Basic knowledge of one of the development languages/platforms below * Familiarity with command-line operations ## Tutorial Overview This tutorial consists of the following steps: ### [Part 1: Setup and Initialize KurrentDB](tutorial-1.md) 1. **[Set up your Codespaces](tutorial-1.md#step-1-set-up-your-codespaces)**: Starts up an interactive coding environment in your browser where all tools and database are installed 2. **[Start and Initialize KurrentDB with Sample Events](tutorial-1.md#step-2-start-and-initialize-kurrentdb-with-sample-events)**: Start up KurrentDB and initialize it with sample events 3. **[Browse the Sample Events in KurrentDB Admin UI](tutorial-1.md#step-3-browse-sample-events-in-kurrentdb-admin-ui)**: Access the Admin UI to browse the appended events ### [Part 2: Project KurrentDB Events to Postgres](tutorial-2.md) 4. **[Execute Projection Applications](tutorial-2.md#step-4-execute-projection-application)**: Starts up the projection sample applications that transform KurrentDB events into read models in Postgres and Redis 5. **[Review the Projected Read Models in Postgres](tutorial-2.md#step-5-review-the-projected-read-models-in-postgres)**: Run the PostgreSQL command line tool to review the newly inserted records 6. **[Examine the Postgres Projection Application Codebase](tutorial-2.md#step-6-examine-the-postgres-projection-application-codebase)**: Examine the PostgreSQL projection application codebase to see how events are transformed to read models in the tables ### [Part 3: Project KurrentDB Events to Redis](tutorial-3.md) 7. **[Review the Projected Read Models in Redis](tutorial-3.md#step-7-review-the-projected-read-models-in-redis)**: Run the Redis command line tool to review the newly added entries 8. **[Examine the Redis Projection Application Codebase](tutorial-3.md#step-8-examine-the-redis-projection-application-codebase)**: Examine the Redis projection application codebase to see how events are transformed into read models in Redis ### [Part 4: Project KurrentDB Events in Real-Time](tutorial-4.md) 9. **[Browse the Demo Web Page](tutorial-4.md#step-9-browse-the-demo-web-page)**: Navigate to the Demo Web Page to see a sample of how the read models in Postgres and Redis are used 10. **[Start the Live Data Generator](tutorial-4.md#step-10-start-the-live-data-generator)**: Start a live data generator program that continuously appends events into KurrentDB 11. **[Watch the Read Models Update in Real-Time](tutorial-4.md#step-11-watch-the-read-models-update-in-real-time)**: See how the read models are updated in real-time in the Demo Web Page 12. **[Understanding catch-up subscription and real-time processing](tutorial-4.md#step-12-understanding-catch-up-subscription-and-real-time-processing)**: Understand how the code projects events to the read models in real-time --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/introduction.md' --- # Overview ![Solving Dual Writes with KurrentDB](./images/outbox-hero.png) ## Dual Write Problem Without distributed transactions, operations that write to multiple resources are not atomic, potentially leading to inconsistencies in the system. This issue is commonly known as the dual write problem. Although named "Dual Write," this pattern can involve writing to more than two resources. A common use case is updating a relational database and simultaneously sending a notification message via a message queue to another system. ![Dual Write Problem](./images/dual-write-problem.png#light) ![Dual Write Problem](./images/dual-write-problem-dark.png#dark) Failure to write to one resource introduces inconsistency with the other. For instance, if the database update succeeds but message delivery fails, downstream systems remain uninformed. ![Out of sync when database updated but messaging failed](./images/dual-write-problem-failed-messaging.png#light) ![Out of sync when database updated but messaging failed](./images/dual-write-problem-failed-messaging-dark.png#dark) Conversely, a failed database update paired with a successful message dispatch leads to downstream systems acting on non-existent data. ## Transactional Outbox Pattern The transactional outbox pattern ensures consistency by writing business data and outgoing messages atomically within the same database transaction. A separate process later dispatches these messages to downstream systems, mitigating the dual write problem. The outbox pattern promotes reducing the number of resources we write to, preferably to just one, and technically, to one transaction. This implies that the effect on the other resources gets deferred. Practically, the outbox acts as a persistent queue holding messages until they're dispatched asynchronously. This trades immediate consistency for eventual consistency, reducing complexity and the risk associated with simultaneous writes. There are several common implementation approaches to this pattern. #### Using a Relational Outbox Table ![Outbox with relational table](./images/outbox-with-database-table.png#light) ![Outbox with relational table](./images/outbox-with-database-table-dark.png#dark) In a relational database, the outbox could manifest itself as one or more tables that keep track of how the other resources need to be affected. In the canonical example, the outbox would be a table containing messages to be sent to the other system. The salient point is to atomically commit regular database changes and outbox messages as part of a single transaction. A separate process (outbox relay) can now pick up the messages from the outbox and send them out. Once a message has been sent, it can be marked as sent or removed from the outbox, whichever option is preferred. While straightforward and intuitive, this approach introduces latency because messages are delivered via periodic polling rather than immediate notification. Additionally, the outbox pattern inherently depends on the database’s scalability, meaning throughput and performance are constrained by database capacity and resource contention. #### Using Change Data Capture (CDC) ![Outbox with change data capture](./images/outbox-with-cdc.png#light) ![Outbox with change data capture](./images/outbox-with-cdc-dark.png#dark) Instead of directly managing an outbox table, another implementation of the outbox pattern involves generating a CDC feed from the affected data table and transforming these changes into messages for external systems. Streaming platforms commonly favor this method. This approach avoids polling overhead and propagates updates with significantly lower latency. However, it relies on additional CDC tooling, increasing complexity if such tools aren't already part of the existing infrastructure. It also risks exposing the internal data model of the source tables, potentially creating undesirable coupling with other systems. ## Outbox Out-of-the-Box with KurrentDB ![Outbox with KurrentDB](./images/outbox-with-kurrentdb.png#light) ![Outbox with KurrentDB](./images/outbox-with-kurrentdb-dark.png#dark) When working with streams and events, an important shift tends to happen. That is, the events written to a stream turn out to be triggers for the messages we want to send to the other system with minimal translation. This occurs because when KurrentDB is used as an event store for event sourcing, an event stream behaves like a hybrid between a database table and a message queue. Like a database table, the stream provides atomic, durable, and immediately consistent operations. And like a message queue, it offers subscription mechanisms to propagate updates to multiple systems in an eventually consistent way. In effect, the stream is the outbox, out of the box. The native subscription capabilities, such as persistent and catch-up subscriptions and connectors, act as cheap mechanisms for writing the glue code that sends messages to the other system. ## How to Approach the Dual Write Problem with KurrentDB 1. Record each business change as an event and append it to a stream in KurrentDB, using the stream as the definitive source of truth. 2. Do not update other systems or read models directly as part of the same append operation. 3. Set up subscriptions to listen for new events in the stream. 4. Process these events asynchronously by triggering actions on external systems. --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial-summary.md' --- # Summary In this tutorial, you’ve explored how KurrentDB offers a powerful, event-driven approach to address the dual write problem — a core challenge in distributed systems where writes to multiple data stores risk inconsistency due to the lack of atomicity. Through practical exercises, you learned how to reliably propagate changes from KurrentDB to external systems like PostgreSQL without needing distributed transactions. Here are the key takeaways: * **Understand the Dual Write Problem** Traditional systems risk inconsistencies when writing to multiple data stores (e.g., a database and a message queue) because partial failures can leave data out of sync. * **Leverage KurrentDB’s Native Event Model** KurrentDB simplifies this pattern by treating event streams as a hybrid between a database and a message queue: * Atomic and durable like a database * Subscribable and reactive like a queue * **Implement Persistent Subscriptions** You set up a consumer group to process `OrderPlaced` events and write to PostgreSQL, achieving eventual consistency with real-time responsiveness. * **Ensure Idempotent Event Handling** Your handler gracefully handles duplicates and out-of-order messages, using unique constraints and error catching to ensure safe, repeatable operations. * **Recover from Application Outages with Checkpoints** Persistent subscriptions track the last successfully processed event using server-side checkpoints. On restart, the application resumes from the checkpoint, avoiding unnecessary reprocessing and data duplication. * **Retry Transient Errors Automatically** Temporary issues like database downtime or network glitches are automatically retried. By identifying transient exceptions and sending a Nack with a retry flag, your application remains fault-tolerant without human intervention. * **Gracefully Skip Permanent Errors** For unrecoverable issues (e.g., corrupted or malformed events), you used the Nack method with a skip action. This prevents one bad event from blocking the entire stream, allowing processing to continue smoothly. By using KurrentDB as your **"outbox out of the box"**, you’ve implemented a clean, scalable, and fault-tolerant system that avoids the traditional pitfalls of dual writes. --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial/tutorial-1.md' --- # Part 1: Set up Codespaces In this part, you will start a GitHub Codespaces session in your browser. ::: info GitHub Codespaces provides an instant and preconfigured development environment all within your browser. This environment contains all the tools and code to complete this tutorial. To learn more about Github Codespaces, [click here](https://github.com/features/codespaces). ::: ## Step 1: Set up Your Codespaces 1. Click the button below to initiate Codespaces and ensure following values are selected: [![](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true\&ref=main\&repo=951198039\&skip_quickstart=true\&devcontainer_path=.devcontainer%2Foutbox%2Fdevcontainer.json) | Configuration Option | Selection | |--------------------------------|----------------------| | Branch | `main` | | Dev container configuration | `Outbox` | | Region | Any value | | Machine type | Any value | Log in to GitHub if required. 2. Wait for your Codespace to build. This can take up to a few minutes. ::: tip For this quickstart, you can safely ignore and close any Codespaces notifications that appear on the bottom right of the page. ::: --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial/tutorial-2.md' --- # Part 2: Trigger Writes to External Data Stores With KurrentDB, eventually consistent update to multiple resources often begin with an event that triggers the entire process. Subsequently, each downstream system subscribes to this event and updates its data store on its own without the need for a distributed transaction. In this tutorial, an `OrderPlaced` event will trigger the start of an order fulfillment process in the fulfillment system. For the purpose of this tutorial, how this event is created is not important. For simplicity, it will be created by a data generator based on checkout-related events. ::: note How Triggering Events are Created In general, just like any event, a triggering event can be created in many ways. For example, a `CouponUsed` event may need transactional mechanisms and patterns such as aggregate and deciders to ensure race condition from multiple actors don't violate business rules (e.g. a coupon can only be used 100 times). On the other hand, an event like `OrderPlaced` in this tutorial may not require these practices since it is only a summary event that collects information from the relevant shopping cart and checkout events. ::: ## Step 2: Start Databases and Append OrderPlaced Event to KurrentDB 1. Once your Codespace is loaded, run this command in the terminal to start KurrentDB and PostgreSQL, and append sample events to KurrentDB: ```sh ./scripts/1-start-dbs-and-generate-data.sh ``` 2. You will see the following message printed in the terminal: ``` Appended data to KurrentDB 🚀 KurrentDB Server has started!! 🚀 URL to the KurrentDB Admin UI 👉: https://XXXXXXXXX.XXX ``` 3. Copy the URL printed in the terminal from the last step. 4. Open a new browser tab. 5. In the address bar of the new tab, paste the URL and navigate to it. 6. This will display the KurrentDB Admin UI. !\[KurrentDB Admin UI Dashboard]\(../images/admin-ui.png =300x) ## Step 3: Browse OrderPlaced Events in KurrentDB Admin UI 1. Click the `Stream Browser` link from the top navigation bar. 2. Under `Recently Changed Streams`, click the `$ce-order` link. ::: info Understanding Category System Projection The `$ce-order` stream contains events from all the carts in KurrentDB. This uses KurrentDB's "by category" system projection stream feature. For more information, see [System Projections](https://docs.kurrent.io/server/v25.0/features/projections/system.html#by-category). ::: 3. You should see a sequenced list of the appended events associated with the two distinct orders. 4. Click on one of them to see the details of the order. ::: details Sample detail of an `OrderPlaced` event ```json { "orderId": "order-b0d1a15a21d24ffa97785ce7b345a87e", "customerId": "customer-185176238", "checkoutOfCart": "cart-631dd4d51e6b4f4d9f9e26e55f1cd587@9", "lineItems": [ { "productId": "3906362089844", "productName": "Glamorise Women's Plus Size MagicLift Natural Shape Bra Wirefree #1210", "quantity": 5, "pricePerUnit": "USD601.05", "taxRate": 0.21 }, { "productId": "4579864912959", "productName": "Simple Designs LT3039-PRP 14.17” Contemporary Mosaic Tiled Glass Genie Standard Table Lamp with Matching Fabric Shade for Home Décor, Bedroom, Living Room, Foyer, Office, Purple", "quantity": 3, "pricePerUnit": "USD392.81", "taxRate": 0.06 } ], "shipping": { "recipient": { "title": "Ms.", "fullName": "Beulah Schmidt", "emailAddress": "Beulah.Schmidt@yahoo.com", "phoneNumber": "1-819-847-8206 x80714" }, "address": { "country": "IR", "lines": [ "Clementine Mountain 445", "75096-8505 North Chadport", "Bedfordshire" ] }, "instructions": "", "method": "express" }, "billing": { "recipient": { "title": "Ms.", "fullName": "Beulah Schmidt", "emailAddress": "Beulah.Schmidt@yahoo.com", "phoneNumber": "1-819-847-8206 x80714" }, "address": { "country": "IR", "lines": [ "Clementine Mountain 445", "75096-8505 North Chadport", "Bedfordshire" ] }, "paymentMethod": "wireTransfer" }, "at": "2025-01-01T01:11:30.976938+00:00" } ``` ::: ::: tip You may have noticed other streams and events in KurrentDB. You can safetly ignore them for the purpose of this tutorial. ::: --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial/tutorial-3.md' --- # Part 3: Write to a External Data Store with Persistent Subscription Now that the event that triggers multiple writes is appended to KurrentDB, you will learn how to handle this event to update a external data store (Postgres) in an eventually consistent way. In this tutorial you will achieve this by running the order processor application. This is a sample application within the order fulfillment system that listens for `OrderPlaced` events with a KurrentDB persistent subscription. Whenever it receives an event, it will kickstart an order fulfillment process simulated by inserting a record in a PostgreSQL table. ## Step 4: Create a KurrentDB Persistent Subscription Consumer Group A persistent subscription consumer group is created on KurrentDB to handle the triggering event. This allows client applications to subscribe to events from KurrentDB. ::: info Persistent Subscription and Consumer Group Persistent subscriptions in KurrentDB are maintained by the server rather than the client. Keeping their position (checkpoint) on the server side, allows them to continue from where they left off if the connection is closed or interrupted. Consumer groups are a concept used with persistent subscriptions that enable the competing consumers pattern, where each clients belonging to a single consumer group receive a portion of events, allowing for load balancing and parallel processing. Multiple consumer groups can be created for the same stream, each operating independently with its own checkpoint position maintained by the server. [Click here](https://docs.kurrent.io/server/v25.0/features/persistent-subscriptions.html) for more information about persistent subscription and consumer groups. ::: 1. Run this command in the terminal to create the persistent subscription: ```sh curl -X PUT -d $'{ "minCheckPointCount": 0, "maxCheckPointCount": 0, "resolveLinktos": true, "maxRetryCount": 100 }' \ http://localhost:2113/subscriptions/%24ce-order/fulfillment \ -H "Content-Type: application/json" ``` You will see the following message: ```json { "correlationId": "f7aac1a3-b5ea-415c-9e3d-d54c0b465d29", "reason": "", "result": "Success", "label": "ClientMessageCompleted" } ``` This creates a consumer group called `fulfillment` that subscribes to the `$ce-order` stream you reviewed in the previous step. :::: warning Review and adjust these settings before applying to production The checkpoint and retry configuration values above are for demonstration only and may not be suitable for a production use. To avoid performance bottlenecks or unexpected behavior, be sure to test and tune them based on your expected workload. ::: details Checkpoint and Retry Configurations Explained The configurations above determine how your consumer group processes events and manages checkpoints: | Configuration Option | Explanation | |--------------------------------|----------------------| | `minCheckPointCount` | The minimum number of messages that must be processed before a checkpoint may be written | | `maxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint, preventing excessive event reprocessing after failures | | `maxRetryCount` | The maximum number of retries (due to timeout) before a message is considered to be parked, preventing infinite retry loops for problematic events | ::: [Click here](https://docs.kurrent.io/server/v25.0/features/persistent-subscriptions.html#checkpointing) for more information about checkpoints. [Click here](https://docs.kurrent.io/server/v25.0/features/persistent-subscriptions.html#acknowledging-messages) for more information about retries. :::: ## Step 5. Review the Consumer Group from the KurrentDB Admin UI 1. Navigate to the KurrentDB Admin. ::: tip If you are unsure what its URL is, you can execute the following script in your codespaces terminal to find out: ```sh ./scripts/get-kurrentdb-ui-url.sh ``` ::: 2. Click the `Persistent Subscriptions` link from the top navigation bar. 3. Click on `$ce-order` directly below the `Stream/Group(s)` column header in the dashboard. The `fulfillment` consumer group should be listed under `$ce-order`. ## Step 6. Start the Order Processor Application When the order processor application is started, it will connect to the `fulfillment` subscription created during the previous step and begin to process any events in the `$ce-order` stream. 1. Run this command in the terminal to start the order processor application: ```sh ./scripts/start-app.sh ``` You will receive a message, like below, printed in the terminal: ``` All apps are running. ``` 2. Run this command in the terminal to view the application log of the order processor application in follow mode: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see messages that indicate two order fulfillment processes have been started: ``` orderprocessor | OrderProcessor started orderprocessor | Subscribing events from stream orderprocessor | Received event #0 in $ce-order stream orderprocessor | Order fulfillment for order-b0d1a15a21d24ffa97785ce7b345a87e started. orderprocessor | Received event #1 in $ce-order stream orderprocessor | Order fulfillment for order-f16847c9a2e44b23bdacc5c92b6dbb25 started. ``` 3. Press `ctrl + c` to exit follow mode. 4. Run this command in the terminal to start PostgreSQL CLI: ```sh docker exec -it postgres psql -U postgres ``` You will receive a message, like below, printed in the terminal: ``` psql (16.8 (Debian 16.8-1.pgdg120+1)) Type "help" for help. postgres=# ``` 5. Run this command in Postgres CLI to list the orders that have started the order fulfillment process: ```sql select orderid from OrderFulfillment; ``` You should see two orders in the table that match the orders listed in the application log. ::: tip If you're stuck with the output and can't exit, press `q` to exit. You're likely in paging mode because the output has overflowed. ::: 6. Exit Postgres CLI by running the command: ``` exit ``` ::: info Alternative Ways to Kickstart the Order Fulfillment Process Inserting a record into a relational database is just one of the many ways to kickstart the order fulfillment process. Alternatively, the order processor could have also triggered this by: * Making a REST call to the order fulfillment API * Publishing a message to a message broker * Sending an email to a the fulfillment department to manually kickstart the process * Appending an event to a OrderFufillment stream in KurrentDB * etc. For demonstration purposes in this tutorial, an insert into a table would suffice. ::: ## Step 7. Examine the Order Processor Application Codebase 1. Run this command in the terminal to open the main program for the order processor application: ```sql code ./OrderProcessor/Program.cs ``` Most of the code snippets leveraged in this step can be found within this file. 2. Locate and examine the code that subscribes to stream: ```cs await using var subscription = kurrentdb.SubscribeToStream( // Subscribe to the $ce-order stream in KurrentDB "$ce-order", "fulfillment"); ``` A subscription is created that subscribes to events from the `$ce-order` stream via the `fulfillment` consumer group. ::: info Different Types of Subscriptions This sample uses persistent subscriptions to subscribe to events. You can also use catch-up subscriptions or connectors to achieve a similar result. [Click here](https://docs.kurrent.io/clients/subscriptions.html) for more information about catch-up subscriptions [Click here](https://docs.kurrent.io/server/v25.0/features/connectors/) for more information about connectors ::: 3. Locate and examine the code that processes each event: ```cs await foreach (var message in subscription.Messages) // Iterate through the messages in the subscription { if (message is PersistentSubscriptionMessage.NotFound) // Skip this message if the subscription is not found { Console.WriteLine("Persistent subscription consumer group not found." + "Please recreate it."); continue; } if (message is not PersistentSubscriptionMessage.Event(var e, _)) // Skip this message if it is not an event continue; try { Console.WriteLine($"Received event #{e.Link.EventNumber} in " + // Log the event number of the event in the $ce-order stream $"{e.Link.EventStreamId} stream"); if (EventEncoder.Decode(e.Event.Data, "order-placed") // Try to deserialize the event to an OrderPlaced event is not OrderPlaced orderPlaced) // Skip this message if it is not an OrderPlaced event continue; repository.StartOrderFulfillment(orderPlaced.orderId); // Process the OrderPlaced event by inserting an order fulfillment record into Postgres await subscription.Ack(e); // Send an acknowledge message to the consumer group so that it will send the next event } ``` The code shows a key part of an event processing pipeline that: * Processes subscription messages from KurrentDB event store's `$ce-order` stream * Performs filtering by: * Skipping non-event messages * Skipping messages where the subscription isn't found * Only processing "order-placed" events * Handles order events by: * Deserializing the event data into an `OrderPlaced` object * Starting order fulfillment by calling `repository.StartOrderFulfillment()` * Acknowledges successful processing with `subscription.Ack()` to tell the consumer group to send the next event ::: info It is important to send an acknowledge message to the consumer group via `subscription.Ack()` after the event is processed successfully. Otherwise KurrentDB will assume the consumer has not received it yet and will retry. ::: 4. Run this command in the terminal to open the OrderFulfillmentRepository class used to insert an order fulfillment record in PostgreSQL: ```sql code ./OrderProcessor/OrderFulfillmentRepository.cs ``` 5. Locate and examine the code that processes each event: ```cs public void StartOrderFulfillment(string? orderId) { if (string.IsNullOrEmpty(orderId)) throw new ArgumentException("Order ID cannot be null or empty", nameof(orderId)); var sql = @" INSERT INTO OrderFulfillment (OrderId, Status) VALUES (@OrderId, 'Started')"; try { _dataAccess.Execute(sql, new { OrderId = orderId }); Console.WriteLine($"Order fulfillment for {orderId} started."); } catch (PostgresException ex) when (ex.SqlState == "23505") // If the error is a unique violation (duplicate key).. { // then it means the order fulfillment already exists. Console.WriteLine($"Order fulfillment for {orderId} " + // Ignore the error and log a message "already started. Start request ignored."); } } ``` This method inserts a new fulfillment record with 'Started' status into the database and handles duplicates by catching and ignoring PostgreSQL unique constraint violations. ::: warning Ensuring Idempotency in Persistent Subscriptions Functions used within persistent subscription processing loops must be idempotent to prevent data duplication. Persistent subscriptions in KurrentDB provide at-least-once delivery guarantees, which means: * Events may be delivered and processed multiple times * The same event might be retried after failures or connection issues * Your processing logic must handle duplicate events gracefully For example, the `StartOrderFulfillment()` function demonstrates proper idempotent design because: * It attempts to insert a record with a unique orderId constraint * If the same event is processed again, the database rejects the duplicate * The function gracefully handles this case without error or side effects This approach ensures that the same order fulfillment record is not inserted multiple times, even when the same `OrderPlaced` event is processed repeatedly. ::: ## Step 8. Process New Events in Real-Time In this step, you will learn how persistent subscriptions can process new events in real time. 1. Run this command in the terminal to append 2 more new `OrderPlaced` events in KurrentDB: ```sh ./scripts/2-generate-data-to-test-real-time.sh ``` You will receive a message, like below, printed in the terminal: ``` Appended data to KurrentDB ``` 2. Run this command in the terminal to view the application log of the order processor application in follow mode: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see messages that indicate four order fulfillment processes have been started: ``` orderprocessor | OrderProcessor started orderprocessor | Subscribing events from stream orderprocessor | Received event #0 in $ce-order stream orderprocessor | Order fulfillment for order-b0d1a15a21d24ffa97785ce7b345a87e started. orderprocessor | Received event #1 in $ce-order stream orderprocessor | Order fulfillment for order-f16847c9a2e44b23bdacc5c92b6dbb25 started. orderprocessor | Received event #2 in $ce-order stream orderprocessor | Order fulfillment for order-44c1c762ca1d440bb2e03a2655ba7edb started. orderprocessor | Received event #3 in $ce-order stream orderprocessor | Order fulfillment for order-c49064f930344a72bd6173db57e43f78 started. ``` 3. Press `ctrl + c` to exit follow mode. 4. Run this command in the terminal to start PostgreSQL CLI: ```sh docker exec -it postgres psql -U postgres ``` 5. Run this command in Postgres CLI to list the orders that have started the order fulfillment process: ```sql select orderid from OrderFulfillment; ``` You should see four orders in the table that should match those listed in the application log. ::: tip Built-in Real-time Processing with Persistent Subscription Persistent subscriptions deliver events in real-time to subscribers after catching up with historical events. ::: 6. Exit Postgres CLI by running the command: ``` exit ``` --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial/tutorial-4.md' --- # Part 4: Error Handling for Writes to External Data Stores Writing to multiple data stores consistently is a critical challenge in distributed systems. Without proper handling, your data can become: * **Lost** - when failures occur during processing * **Duplicated** - when the same operation is performed multiple times * **Corrupted** - when partial updates leave data in an inconsistent state Traditional solutions often rely on distributed transactions, which add complexity and can impact performance. KurrentDB provides built-in features specifically designed to address these challenges more elegantly. In this section, you'll explore practical failure scenarios that commonly occur when writing to external data stores like PostgreSQL, and how they can be handled with a combination of KurrentDB features and application code. ## Step 9: Handle Application Outage with Checkpoints Event processing applications like the order processor can sometimes go down while processing historic or live events from a persistent subscription. When this happens, we typically want to avoid reprocessing all previous messages for two main reasons: * **Data duplication risks**: Reprocessing messages can cause duplicate data in external systems, especially those that don't support idempotency or deduplication (such as email sending operations). * **Performance concerns**: Reprocessing large volumes of events can significantly impact system performance, particularly when the stream contains many events. KurrentDB addresses this challenge using checkpoints in persistent subscriptions. In this step, we'll examine how event processing applications can recover from and outage, and how KurrentDB's checkpoint mechanism minimizes the number of events that need to be reprocessed when it happens. ::: info Checkpoint in Persistent Subscription Checkpoints in persistent subscriptions are records of the last successfully processed event position, stored as events with in system streams. They enable subscriptions to resume from their last position after interruptions (like server restarts or leader changes), though this may result in some events being received multiple times by consumers since checkpoints are written periodically rather than after every event. [Click here](https://docs.kurrent.io/server/v25.0/features/persistent-subscriptions.html#checkpointing) for more information about checkpoints in persistent subscriptions. ::: 1. Run this command in the terminal to display the current information of the consumer group: ```sh curl -s http://localhost:2113/subscriptions/%24ce-order/fulfillment/info | \ jq '{totalItemsProcessed, lastCheckpointedEventPosition, lastKnownEventNumber}' ``` You should see that four items have been processed by the consumer group so far: ``` "totalItemsProcessed": 4, ``` This means the consumer group has four acks - one for each order queried previously. You should also see that the last checkpointed event position is 3: ``` "lastCheckpointedEventPosition": "3", ``` This means that the consumer group will resume from position 3 even if there is an application outage. Finally, you should also see that the last known event number is 3: ``` "lastKnownEventNumber": 3, ``` This is the event number of the last and fourth `OrderPlaced` event in `$ce-order`. ("3" is displayed since event numbers are zero-based). 2. Run this command in the terminal to stop the order processor application. This simulates an application outage: ```sh docker compose --profile app stop ``` 3. Run this command in the terminal to append two more new `OrderPlaced` events in KurrentDB while the application is down: ```sh ./scripts/3-generate-data-during-app-outage.sh ``` 4. Run this command in the terminal to display the current information of the consumer group: ```sh curl -s http://localhost:2113/subscriptions/%24ce-order/fulfillment/info | \ jq '{totalItemsProcessed, lastCheckpointedEventPosition, lastKnownEventNumber}' ``` You should see that the `totalItemsProcessed` is still "4": ``` "totalItemsProcessed": 4, ``` This is expected even though two new `OrderPlaced` events were appended. This is because the application is currently down. You should also see that the last checkpointed event position is still 3: ``` "lastCheckpointedEventPosition": "3", ``` This is because no new events have been processed yet, so the checkpoint has not been updated. However, you should see that the `lastKnownEventNumber` is 5 instead of 3: ``` "lastKnownEventNumber": 5, ``` This means the consumer group is aware that two more events were appended. 5. Run this command in the terminal to stop the order processor application. This simulates an application recovery: ```sh docker compose --profile app start ``` 6. Run this command in the terminal to view the application log after the application has restarted: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see messages that indicate that the two new events are created: ``` orderprocessor | OrderProcessor started orderprocessor | Subscribing events from stream orderprocessor | Received event #4 in $ce-order stream orderprocessor | Order fulfillment for order-36bf75fe641e453b906946ba4b5288c5 created. orderprocessor | Received event #5 in $ce-order stream orderprocessor | Order fulfillment for order-babc43583617421a90d4c7d039900142 created. ``` Notice how the processor received events starting from event #4 because of the previously saved checkpoint. 7. Press `ctrl + c` to exit follow mode. :::: info How often are Checkpoints Saved? The frequency at which checkpoints are saved depends on three key configuration parameters: * **minCheckpointCount** - Minimum number of events before a checkpoint may be written * **maxCheckpointCount** - Maximum number of events before forcing a checkpoint * **checkPointAfterMilliseconds** - Time-based threshold for checkpoint creation For this tutorial, we've configured these parameters to their minimum values to ensure checkpoints are saved after each processed event. This makes the behavior more predictable and easier to observe. ::: warning Performance Note While frequent checkpointing provides better recovery guarantees, it's not necessarily the best practice for production environments. Each checkpoint operation triggers a disk write, so excessive checkpointing can introduce significant performance overhead. In production, you should balance recovery needs with performance considerations. See [Step 4](tutorial-3.md#step-4-create-a-kurrentdb-persistent-subscription-consumer-group) for more information. ::: :::: 8. Run this command in the terminal to display the current information of the consumer group: ```sh curl -s http://localhost:2113/subscriptions/%24ce-order/fulfillment/info | \ jq '{totalItemsProcessed, lastCheckpointedEventPosition, lastKnownEventNumber}' ``` You should see that the `totalItemsProcessed` is now 6 instead of 4: ``` "totalItemsProcessed": 6, ``` And the checkpoint has been updated to 5: ``` "lastCheckpointedEventPosition": "5", ``` But the `lastKnownEventNumber` is still 5: ``` "lastKnownEventNumber": 5, ``` 9. Run this command in the terminal to start PostgreSQL CLI: ```sh docker exec -it postgres psql -U postgres ``` 10. Run this command in Postgres CLI to list the orders that have started the order fulfillment process: ```sql select orderid from OrderFulfillment; ``` You should now see a total of six orders: ``` orderid ---------------------------------------- order-b0d1a15a21d24ffa97785ce7b345a87e order-f16847c9a2e44b23bdacc5c92b6dbb25 order-44c1c762ca1d440bb2e03a2655ba7edb order-c49064f930344a72bd6173db57e43f78 order-36bf75fe641e453b906946ba4b5288c5 order-babc43583617421a90d4c7d039900142 ``` 11. Exit Postgres CLI by running the command: ``` exit ``` ## Step 10. Handle Transient Errors by Retrying Events Transient errors are temporary failures that resolve themselves over time. Examples include database disconnections, network issues, or service restarts. When these errors occur, the best strategy is often to retry processing rather than failing permanently. For example, if PostgreSQL becomes unavailable while the order processor is running: * The database connection fails * The OrderPlaced event processing throws an exception * Without retry logic, this event would be lost * With retry logic, processing resumes once the database recovers Most transient errors resolve within seconds or minutes. KurrentDB's persistent subscriptions provide built-in retry capabilities, helping your system maintain data consistency during temporary outages. In this step, you'll see how these retries prevent data loss when database connectivity is interrupted. 1. Run this command in the terminal to stop PostgreSQL to simulate a database outage: ```sh docker compose --profile db stop postgres ``` 2. Run this command in the terminal to append 2 new `OrderPlaced` events in KurrentDB: ```sh ./scripts/4-generate-data-during-db-outage.sh ``` 3. Run this command in the terminal to view the application log in follow mode: ```sh docker compose --profile app logs -f ``` 4. Wait for a few seconds and you will start to notice messages that indicate a transient error is detected and the application will retry the event: ``` orderprocessor | Detected DB transient error Name or service not known. Retrying. orderprocessor | Received event #4 in $ce-order stream orderprocessor | Detected DB transient error Name or service not known. Retrying. orderprocessor | Received event #5 in $ce-order stream ``` Notice that the application retries this continuously for a while. 5. Press `ctrl + c` to exit follow mode. 6. Run this command in the terminal to stop PostgreSQL to simulate database recovery: ```sh docker compose --profile db start postgres ``` 7. Run this command in the terminal to view the application log in follow mode again: ```sh docker compose --profile app logs -f ``` Wait for a while and notice that the event processing that have been retrying continuously has now been processed. ``` orderprocessor | Received event #6 in $ce-order stream orderprocessor | Order fulfillment for order-3d268df88f9c451eae9cae49d10656d5 created. orderprocessor | Received event #7 in $ce-order stream orderprocessor | Order fulfillment for order-ad53653936ff469ea208cce8726906eb created. ``` 8. Press `ctrl + c` to exit follow mode. ::: warning Use Idempotency to Handle Out-of-Order and Duplicate Events Depending on how the persistent subscription is configured and when a transient error is recovered, you may notice events are being retried multiple times and in a different order than you expect. Because of this, you should always design your event handling logic to be idempotent. In other words, processing the same event more than once—or receiving it out of order—should not break your application or result in inconsistent data. See [Step 7](tutorial-3.md#step-7-examine-the-order-processor-application-codebase) for more information. ::: 9. Run this command in the terminal to start PostgreSQL CLI: ```sh docker exec -it postgres psql -U postgres ``` 10. Run this command in Postgres CLI to list the orders that have started the order fulfillment process: ```sql select orderid from OrderFulfillment; ``` You should now see the eight orders in total: ``` orderid ---------------------------------------- order-b0d1a15a21d24ffa97785ce7b345a87e order-f16847c9a2e44b23bdacc5c92b6dbb25 order-44c1c762ca1d440bb2e03a2655ba7edb order-c49064f930344a72bd6173db57e43f78 order-36bf75fe641e453b906946ba4b5288c5 order-babc43583617421a90d4c7d039900142 order-3d268df88f9c451eae9cae49d10656d5 order-ad53653936ff469ea208cce8726906eb ``` 11. Exit Postgres CLI by running the command: ``` exit ``` ## Step 11. Examine How Transient Errors are Handled in the Codebase 1. Run this command in the terminal to open the main program for the order processor application: ```sql code ./OrderProcessor/Program.cs ``` 2. Locate and examine the code that handles transient errors: ```cs catch (Exception ex) { // ------------------------------------------------------------- // // Warning: This is just one example of a transient error check // // You should to add more checks based on your needs // // ------------------------------------------------------------- // var exceptionIsTransient = // Exception is transient if it matches one of the following patterns: ex is SocketException || // SocketException indicating a network error (https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socketexception?view=dotnet-plat-ext-7.0) ex is NpgsqlException { IsTransient: true }; // Postgres exception indicating the error is transient (https://www.npgsql.org/doc/api/Npgsql.NpgsqlException.html#Npgsql_NpgsqlException_IsTransient) if (exceptionIsTransient) // If exception is transient.. { Console.WriteLine($"Detected DB transient error {ex.Message}. Retrying."); await subscription.Nack(PersistentSubscriptionNakEventAction.Retry, // Send a not acknowledge message to the consumer group and request it to retry "Detected DB transient error", e); Thread.Sleep(1000); // Wait for a second before retrying to avoid overwhelming the database } ``` To handle transient errors, you can: * Identify a list of errors that are recoverable * Catch these errors and call `subscription.Nack()` to send a not acknowledge message to KurrentDB with the `Retry` flag With this approach, KurrentDB will re-send the event again for a configured number of time (defined by `maxRetryCount`). If the error is recovered before this, then the processor will successfully handle this error. Otherwise, KurrentDB will park this event. ::: warning Customize Transient Error Handling The list of transient errors provided in our example is not exhaustive and may not fully reflect the conditions in your environment. You should treat it as a starting point and customize it based on your infrastructure, observed failure modes, and testing. Be sure to evaluate and expand this list to include any additional error types that are specific to your setup or likely to occur in your system. ::: ::: info Parking Events in Persistent Subscription An event can be saved, or "parked," in a special stream dedicated to persistent subscriptions. This parked event can later be reviewed for debugging or troubleshooting, such as checking if it contains a specific error. If the underlying issue is fixed, the event can also be replayed. This process is commonly referred to as "dead-lettering." [Click here](https://docs.kurrent.io/server/v25.0/features/persistent-subscriptions.html#parked-messages) for more information about parking. ::: ::: warning Dangers of Setting a High `maxRetryCount` Configuration The `maxRetryCount` configuration of the consumer group sets the number of times it should retry an event when it is instructed. While a high `maxRetryCount` may increase the chance for a transient error to recover while it waits for server to recover, it can also increase the load on the server that may already be under distress with high load, making it more difficult to recover. You should ensure that `maxRetryCount` is set appropriately so that it does not potentially overload a recovering server. ::: ## Step 12. Handle Permanent Errors by Skipping Events Event processors sometimes encounter permanent errors that cannot be resolved through retries. These unrecoverable errors can sometimes result from say a malformed events with structural or syntactical problems. When these permanent errors occur, continuous retrying is futile and blocks subsequent events in the stream from being processed. One solution is to skip the problematic event, allowing the processor to continue with other events in the stream. In this step, you will find out how to detect and skip events that trigger permanent errors. 1. Run this command in the terminal to generate an invalid `OrderPlaced` event: ```sh curl -X POST \ -H "Content-Type: application/vnd.eventstore.events+json" \ http://localhost:2113/streams/order-b3f2d72c-e008-44ec-a612-5f7fbe9c9240 \ -d ' [ { "eventId": "fbf4a1a1-b4a3-4dfe-a01f-ec52c34e16e4", "eventType": "order-placed", "data": { "thisEvent": "is invalid" } } ]' ``` 2. Run this command in the terminal to view the application log of the order processor application: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see a new log message indicating a permanent error has been detected and the event has been skipped. ``` orderprocessor | Received event #8 in $ce-order stream orderprocessor | Detected permanent error Order ID cannot be null or empty (Parameter 'orderId'). Skipping. ``` 3. Press `ctrl + c` to exit follow mode. ## Step 13. Examine How Permanent Errors are Handled in the Codebase 1. Run this command in the terminal to open the main program for the order processor application: ```sql code ./OrderProcessor/Program.cs ``` 2. Locate and examine the code that handles permanent errors: ```cs catch (Exception ex) { if (exceptionIsTransient) // If exception is transient.. { ... } else // If exception is not transient (i.e. permanent).. { Console.WriteLine($"Detected permanent error {ex}. Skipping."); await subscription.Nack(PersistentSubscriptionNakEventAction.Skip, // Send a not acknowledge message to the consumer group and request it to skip "Detected permanent error", e); } ``` Errors not classified as transient are considered permanent errors. To handle these unrecoverable situations, call `subscription.Nack()` with the `PersistentSubscriptionNakEventAction.Skip` flag. This instructs the consumer group to skip processing the problematic event and deliver the next available event in the stream. --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/outbox/tutorial/tutorial-intro.md' --- # Introduction This tutorial will guide you through the Outbox Out of the Box sample with KurrentDB using GitHub Codespaces. ## Objectives In this tutorial, you will: * Understand how triggering events in KurrentDB can initiate updates to external data stores * Implement and observe real-time processing with KurrentDB persistent subscriptions * Create and configure consumer groups for reliable event processing * Develop idempotent event handlers to safely process events with at-least-once delivery guarantees * Implement proper error handling strategies for both transient and permanent errors * Work with checkpoints to ensure events are properly processed after system failures * Learn how to handle problematic events by using techniques like retrying and skipping ## Prerequisites Before starting, ensure you have the following: * A GitHub account to use GitHub Codespaces * Basic knowledge of C# * Familiarity with command-line operations ## Tutorial Overview This tutorial consists of the following steps: ### [Part 1: Set up Codespaces](tutorial-1.md) 1. **[Set up your Codespaces](tutorial-1.md#step-1-set-up-your-codespaces)**: Start an interactive coding environment in your browser where all tools and databases are installed ### [Part 2: Trigger Writes to External Data Stores](tutorial-2.md) 2. **[Start Databases and Append OrderPlaced Event to KurrentDB](tutorial-2.md#step-2-start-databases-and-append-orderplaced-event-to-kurrentdb)**: Start KurrentDB and PostgreSQL, and append sample OrderPlaced events 3. **[Browse OrderPlaced Events in KurrentDB Admin UI](tutorial-2.md#step-3-browse-orderplaced-events-in-kurrentdb-admin-ui)**: Access the Admin UI to explore the triggering events ### [Part 3: Write to a External Data Store with Persistent Subscription](tutorial-3.md) 4. **[Create a KurrentDB Persistent Subscription Consumer Group](tutorial-3.md#step-4-create-a-kurrentdb-persistent-subscription-consumer-group)**: Set up a persistent subscription to process events 5. **[Review the Consumer Group from the KurrentDB Admin UI](tutorial-3.md#step-5-review-the-consumer-group-from-the-kurrentdb-admin-ui)**: Examine the created consumer group 6. **[Start the Order Processor Application](tutorial-3.md#step-6-start-the-order-processor-application)**: Run the application that processes OrderPlaced events 7. **[Examine the Order Processor Application Codebase](tutorial-3.md#step-7-examine-the-order-processor-application-codebase)**: Understand how idempotent event processing works in KurrentDB 8. **[Process New Events in Real-Time](tutorial-3.md#step-8-process-new-events-in-real-time)**: Observe real-time event processing with persistent subscriptions ### [Part 4: Error Handling for Writes to External Data Stores](tutorial-4.md) 9. **[Handle Application Outage with Checkpoints](tutorial-4.md#step-9-handle-application-outage-with-checkpoints)**: Learn how checkpoints ensure event processing after system failures 10. **[Handle Transient Errors by Retrying Events](tutorial-4.md#step-10-handle-transient-errors-by-retrying-events)**: Implement retry logic for temporary failures 11. **[Examine How Transient Errors are Handled in the Codebase](tutorial-4.md#step-11-examine-how-transient-errors-are-handled-in-the-codebase)**: Understand the error handling implementation 12. **[Handle Permanent Errors by Skipping Events](tutorial-4.md#step-12-handle-permanent-errors-by-skipping-events)**: Learn to handle unrecoverable errors by skipping problematic events 13. **[Examine How Permanent Errors are Handled in the Codebase](tutorial-4.md#step-13-examine-how-permanent-errors-are-handled-in-the-codebase)**: Review the permanent error handling implementation --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/time-travel/introduction.md' --- # Overview ![Time Travel with KurrentDB](./images/time-travel-hero.png) ## What is Time Traveling? Time traveling in data systems means being able to query or reconstruct your data as it existed at any point in the past. This is useful for many scenarios, such as auditing, debugging, compliance, and analytics. | Use Case | Example | |--------------------------------------|---------| | Business process reconstruction | See the full lifecycle of a loan application, including every approval, rejection, and amendment, to resolve disputes or meet regulations | | Point-in-time state diffing | Show exactly what changed in a customer’s profile or account settings between two points in time for support or compliance reviews | | Simulation and what-if analysis | Simulate the impact of a pricing change or business rule adjustment by replaying all related product and sales events to see how outcomes would differ | | Business event tracing | Audit the sequence of business events for a transaction (order placed, payment received, shipment sent, delivery confirmed) to verify SLAs or investigate complaints | | Timeline-Based State Inspection | Let users view a timeline of how their account or order changed, with each step tied to a business event (like address updated, item added, return requested) | | Temporal aggregate calculation | Track how total sales for a product changed over time by calculating monthly sales at the end of each month | ## Time Traveling with Traditional Approaches Traditional databases struggle with historical queries because they are designed to store only the current state. Retrieving or reconstructing previous versions often requires extra mechanisms like audit logs, snapshots, or CDC with time-partitioned data lakes. These approaches add complexity, slow down queries, and may not always provide a complete or reliable view of historical data. #### Audit Logs Audit logs are a common approach in traditional databases for tracking changes. They record each modification as a new entry in a separate log table, typically including details such as who made the change, when it occurred, and what operation was performed. **Advantages:** * Provide a detailed record of changes. * Can answer questions about who changed what and when. * Useful for meeting regulatory requirements for tracking changes. **Limitations:** * Audit logs are separate from main data, risking inconsistencies if not commited together. * Often capture only high-level operations. * Gaps or missed events make it difficult to reconstruct past states. #### Snapshots Snapshots are another traditional method for preserving historical data. A snapshot captures the entire state of a table or dataset at a specific point in time, often on a scheduled basis (e.g., nightly or weekly), and stores it as a separate copy. **Advantages:** * Provide a complete view of data. * Useful for restoring data after accident. * Easy to implement without major changes to existing applications. **Limitations:** * Snapshots are taken infrequently. * Storage requirements can be high. * Snapshots lack a detailed audit trail of individual changes. #### Change Data Capture (CDC) and Data Lake Change Data Capture (CDC) is a technique that tracks and records changes (inserts, updates, deletes) in source databases and delivers them to downstream systems, often storing them in time-partitioned data lakes for analytics and historical queries. **Advantages:** * Captures detailed change events, enabling near real-time replication and analytics. * Supports integration with data warehouses and lakes for large-scale historical analysis. * Can be used to build audit trails and reconstruct state changes over time. **Limitations:** * CDC pipelines add operational complexity and require many moving parts * Do not persist the actual change from the source, so lost events mean state can't be reliably reconstructed. * Captured changes are usually low-level and technical, making business intent unclear. ## Time Traveling with KurrentDB KurrentDB makes time travel simple by recording every change as an immutable, ordered event. This lets you accurately reconstruct any previous state, making historical queries, audits, and analysis straightforward without complex workarounds. KurrentDB makes this possible because: * It stores every change as an immutable event, preserving a complete and accurate history. * It allows event streams to be replayed at any time for simulation, testing, and debugging—even far into the future. * Its historical event log is naturally suited for regulatory and compliance needs, making audits and reporting straightforward. * It supports outbox pattern out of the box, ensuring reliable delivery of historical events to downstream systems. * It promotes events are modeled around business intent, providing clear context for historical queries. * Events are small and focused, making them efficient to store and process. * It handles storage, indexing, and pushing events natively, reducing the need for extra components in your time travel solution. ## How to Time Travel with KurrentDB * **Store events in ordered streams:** Record every relevant change as an event in KurrentDB, ensuring events are stored in the order they occurred. * **Replay events on-demand:** Replay all the relevant events from stream up to the desired timestamp or version. * **Pre-compute state for efficiency:** For objects with long histories or frequent queries, you can periodically store snapshots of state. This reduces the number of events that need to be replayed for common queries. KurrentDB’s approach makes time travel queries reliable, flexible, and efficient, supporting both detailed investigations and high-performance analytics. --- --- url: 'https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial-summary.md' --- # Summary In this tutorial, you’ve explored how KurrentDB enables robust snapshot-based and on-demand time-travel capabilities by leveraging its ordered event streams. Time-travel allows you to reconstruct and query the state of your system at any point in history, an essential feature for auditing, debugging, and understanding how your data evolves. Here are the key takeaways: * **Understand Time-Travel in Event-Sourced Systems**\ By storing every change as an immutable event, KurrentDB makes it possible to rebuild the state of your application as it existed at any moment in the past. * **Implement Snapshot-Based Time-Travel**\ You created projections that periodically store snapshots of your data, enabling fast queries for historical states without replaying all events from the beginning. * **Enable On-Demand State Reconstruction**\ You examined APIs that can replay events up to a specific point in time, reconstructing the exact state as it was, even if no snapshot exists for that moment. * **Balance Performance and Accuracy**\ You learned when to use pre-computed snapshots for performance and when to use on-demand event replay for the most accurate, up-to-date historical views. * **Filter and Process Events Efficiently**\ Your solution filters events by relevant criteria (such as date or entity) and processes only what’s needed to reconstruct the requested state. * **Build Interactive Time-Travel Interfaces**\ You implemented a UI that lets users “travel” through time, visualizing how data changed by selecting different historical points. * **Audit and Trace Data Lineage**\ You enabled users to trace which events contributed to a particular state, providing transparency and accountability for every data change. By using KurrentDB’s event storage and processing features, you’ve built a powerful, flexible time-travel system that delivers deep insight into your data’s history while maintaining simplicity and performance. --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-1.md --- # Part 1: Set up Codespaces In this part, you will start a GitHub Codespaces session in your browser. ::: info GitHub Codespaces provides an instant and preconfigured development environment all within your browser. This environment contains all the tools and code to complete this tutorial. To learn more about Github Codespaces, [click here](https://github.com/features/codespaces). ::: ## Step 1: Set up Your Codespaces 1. Click the button below to initiate Codespaces and ensure following values are selected: [![](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true\&ref=time-travel\&repo=951198039\&skip_quickstart=true\&devcontainer_path=.devcontainer%2Ftime-travel%2Fdevcontainer.json) | Configuration Option | Selection | |--------------------------------|----------------------| | Branch | `main` | | Dev container configuration | `Time Travel` | | Region | Any value | | Machine type | Any value | Log in to GitHub if required. 2. Wait for your Codespace to build. This can take up to a few minutes. ::: tip For this quickstart, you can safely ignore and close any Codespaces notifications that appear on the bottom right of the page. ::: --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-2.md --- # Part 2: Initialize KurrentDB with Sample Orders for Sales Report In this part, you will start a KurrentDB instance and initialize it with a few hundred order events. These orders will be used to construct sales reports in part 3. ## Step 2: Start Databases and Append OrderPlaced Event to KurrentDB 1. Once your Codespace is loaded, run this command in the terminal to start KurrentDB, and append sample events to it: ```sh ./scripts/1-start-dbs-and-generate-data.sh ``` This can take a minute or so. 2. After a while, you will see the following message printed in the terminal: ``` KurrentDB has started. Seeding data to KurrentDB... this may take a while. 17:44:00 info: edb-commerce[0] Executing command 'seed-data-set' with settings {"InputPath":"/workspaces/edu-samples/time-travel/data/data.few-hundred-orders.json","ConnectionString":"esdb://localhost:2113?tls=false"} Appended data to KurrentDB 🚀 KurrentDB Server has started!! 🚀 URL to the KurrentDB Admin UI 👉: https://XXXXXXXXX.XXX ``` 3. Copy the URL printed in the terminal from the last step. 4. Open a new browser tab. 5. In the address bar of the new tab, paste the URL and navigate to it. 6. This will display the KurrentDB Admin UI. !\[KurrentDB Admin UI Dashboard]\(../images/admin-ui.png =300x) ## Step 3: Browse OrderPlaced Events in KurrentDB Admin UI 1. Click the `Stream Browser` link from the top navigation bar. 2. Under `Recently Changed Streams`, click the `$et-order-placed` link. ::: tip You may have noticed other streams and events in KurrentDB. You can safely ignore them for the purpose of this tutorial. ::: ::: info Understanding Event Type System Projection The `$et-order-placed` system stream contains all `order-placed` events across all streams in KurrentDB. This uses KurrentDB's "by event type" system projection stream feature. For more information, see [System Projections](https://docs.kurrent.io/server/v25.0/features/projections/system.html#by-event-type). ::: 3. You should see a sequenced list of the appended `order-placed` events associated to a few hundred orders. 4. Click on one of the events to see the order details. ::: note Sample detail of an `OrderPlaced` event ```json { "orderId": "order-defeb24c5f314600a92e33a26e414d49", "customerId": "customer-189867765", "checkoutOfCart": "cart-be7c5d744ba7490a9888d21a5b484095@5", "store": { "url": "https://www.amazon.pl", "countryCode": "POL", "geographicRegion": "Europe" }, "lineItems": [ { "productId": "8142578988976", "productName": "SAURA LIFE SCIENCE Adivasi Ayurvedic Neelgiri Hair growth Hair Oil-250ML (2)", "category": "Beauty", "quantity": 2, "pricePerUnit": "USD579.17", "taxRate": 0.21 }, { "productId": "3811624999109", "productName": "PULOTE 100PCS Pink Plastic Plates - Heavy Duty Pink Disposable Plates - Pink and Gold Plastic Plates Include 50PCS Pink Dinner Plates, 50PCS Pink Dessert Plates for Party\u0026Wedding", "category": "Health \u0026 Household", "quantity": 3, "pricePerUnit": "USD421.82", "taxRate": 0.21 } ], "shipping": { "recipient": { "title": "Miss", "fullName": "Dwayne Romaguera", "emailAddress": "Dwayne.Romaguera28@yahoo.com", "phoneNumber": "(385) 893-8263" }, "address": { "country": "IE", "lines": [ "Prudence Land 1777", "25151-4282 Lake Kathryn", "Borders" ] }, "instructions": "", "method": "standard" }, "billing": { "recipient": { "title": "Miss", "fullName": "Dwayne Romaguera", "emailAddress": "Dwayne.Romaguera28@yahoo.com", "phoneNumber": "(385) 893-8263" }, "address": { "country": "IE", "lines": [ "Prudence Land 1777", "25151-4282 Lake Kathryn", "Borders" ] }, "paymentMethod": "wireTransfer" }, "at": "2025-01-01T01:41:54.0643996\u002B00:00" } ``` ::: ::: info Key Fields in `OrderPlaced` Event | Field | Description | |-------------------------|-----------------------------------------------------------------------------------------------| | `orderId` | Unique identifier for the order (e.g., "order-defeb24c5f314600a92e33a26e414d49") | | `store` | Object containing store information such as URL and country code | | `store.geographicRegion`| Geographic region where the store is located (e.g., "Europe"). | | `lineItems` | Array of products in the order, each with details like ID, name, quantity, and price | | `lineItems.category` | Product category classification (e.g., "Beauty", "Health & Household") | | `shipping` | Object containing shipping details, recipient info, address, and shipping method | | `billing` | Object containing billing information, recipient details, address, and payment method | | `at` | Timestamp indicating when the order was placed | ::: --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-3.md --- # Part 3: Project Events to Sales Report Read Model Now that KurrentDB is initialized with a few hundred order events, you will project the events into a JSON file-read model and render it as a sales report on a web page. ### Introducing the Month End Sales Report The month-end sales report summarizes total revenue from orders placed across all e-commerce stores worldwide. Sales figures are broken down by product category and region. The report also includes the monthly sales target to support forecasting and performance management. The report also includes a month end sales target, which helps with forecasting and performance management. ![Month End Sales Report](../images/month-end-sales-report.png) ## Step 4: Start the Report Projection Application The Report Projection Application listens for `OrderPlaced` events and updates a JSON read model that represents the sales report. This read model is saved to disk and used to display the sales report on a web page. ::: tip Read Model: Flexibility to Choose the Right Data Model and Store Read models built from events can be stored in many ways, such as relational or document databases, but you are not limited to these options. In this example, a JSON file on local disk is used to show that any type of persistent storage is acceptable. For instance, the read model could also be saved as a PDF for emailing to management, or as a rendered HTML file that can be cached and served directly to browsers. You have complete flexibility in choosing the data model and storage method for your read model. [Click here](../introduction.md) for more information about this. ::: 1. Run this command in the terminal to start the report projection application: ```sh ./scripts/start-app.sh ``` You will receive a message, like below, printed in the terminal: ``` All apps are running. ``` 2. Run this command in the terminal to view the application log of the report projection application in follow mode: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see many messages that indicate the read model is being updated: ``` reportprojection | Projected event #XXX order-placed ``` 3. Press `ctrl + c` to exit follow mode. 4. Run this command in the terminal to open the main program for the order processor application: ```sh code ./data/report-read-model.json ``` You should see a json structure similar to this: ```json { "checkpoint": 534, "salesReports": { "2025-01-31": { "categories": { "Arts, Crafts \u0026 Sewing": { "regions": { "Asia": { "dailySales": 0, "monthEndSalesTarget": 54600, "totalMonthlySales": 56214.91, "targetHitRate": 1.03 }, "Europe": { "dailySales": 868.85, "monthEndSalesTarget": 83500, "totalMonthlySales": 82157.26, "targetHitRate": 0.98 }, // ..... } }, "Automotive": { "regions": { "Asia": { "dailySales": 0, "monthEndSalesTarget": 6500, "totalMonthlySales": 6532.68, "targetHitRate": 1.01 }, "Europe": { "dailySales": 1166.55, "monthEndSalesTarget": 8700, "totalMonthlySales": 10965.57, "targetHitRate": 1.26 }, // ..... ``` ::: details Details of the Read Model | Field | Description | |----------------------------|-----------------------------------------------------------------------------------------------| | `checkpoint` | The last processed event number (used for resuming projections). | | `salesReports` | Maps report dates (YYYY-MM-DD) to their sales data. | | └─ *date* | Contains sales data for the given date. | |   └─ `categories`| Maps product categories to their data (e.g. Automotive, Beauty, Electronics). | |     └─ *category* | Contains sales data for the category. | |       └─ `regions` | Maps regions to their sales data (e.g. Asia, Europe, North-America). | |         └─ *region* | Contains sales metrics for the region. | |           └─ `dailySales` | Sales for the reported day. | |           └─ `monthEndSalesTarget`| The sales target for the month. | |           └─ `totalMonthlySales` | The total sales for the month. | |           └─ `targetHitRate` | Ratio of actual sales to target. | ::: The structure of `report-read-model.json` is intentionally **denormalized**. This means the data is organized and pre-built for direct use, in this case, by the web page. ::: info What Does "Denormalized" Mean? A denormalized read model stores related and pre-computed data together, such as sales totals by category and region, in a format optimized for fast querying and display. Unlike normalized data structures, which separate data into multiple related models, a denormalized model keeps all the information needed for a report or user interface in one place. This approach eliminates the need for complex joins or calculations at query time, allowing front-end applications to use the data directly. Denormalization is especially common for read models generated from KurrentDB events, as it enables applications to efficiently present and analyze event data without requiring additional processing at runtime. Denormalization is a common for read models generated from KurrentDB events, since it allows applications to efficiently present and analyze event data without extra processing. ::: ## Step 5: Start and Browse the Report Web Application 1. Run this command in the terminal to start the sales report web application: ```sh ./scripts/start-web.sh ``` You will receive a message, like below, printed in the terminal: ``` All apps are running. URL to Demo Web Application 👉 http://XXXXXXXX ``` 2. Copy the URL printed in the terminal and navigate to it in a browser. You should see the sales report with structure and figures that match the read model generated previously. ::: info Quick Quiz Do the numbers in the report match the ones in `report-read-model.json`? ::: ## Step 6: Examine the Report Projection Application The Report Projection Application performs the following steps: * Connects to KurrentDB and subscribes to the `$et-order-placed` stream * Loads the existing sales report read model from disk, or creates a new one if none exists * Reads the last processed event position (checkpoint) from the read model * Processes each `OrderPlaced` event by updating sales totals by date, category, and region * Updates the checkpoint after each event * Saves the updated read model as a JSON file after every event 1. Run this command in the terminal to open the main program for the projection application: ```sh code ./ReportProjection/Program.cs ``` Most of the code snippets leveraged in this step can be found within this file. 2. Locate and examine the code that deserializes the JSON file to read model: ```cs // --------------------------------- // // Deserialize JSON to report object // // --------------------------------- // var readModelPath = // Get the path to the report read model from an environment variable Environment.GetEnvironmentVariable("OUTPUT_FILEPATH") ?? "data/report-read-model.json"; var hasExistingReadModel = File.Exists(readModelPath); var readModel = LoadOrCreateReadModel(readModelPath); // Deserialize the JSON file into a ReportReadModel object // ... ReportReadModel LoadOrCreateReadModel(string readModelPath) { return File.Exists(readModelPath) // Check if the read model file exists ? JsonSerializer.Deserialize( // If it does, deserialize it File.ReadAllText(readModelPath)) // By reading the file contents and deserialize to ReportReadModel ?? new ReportReadModel() // If file is not found or deserialization fails : new ReportReadModel(); // create a new empty ReportReadModel object } ``` The read model is retrieved by reading the JSON file from a location specified by an environment variable. If the file exists and can be read successfully, it is deserialized into a `ReportReadModel` object. If the file doesn't exist or deserialization fails, a new empty `ReportReadModel` object is created instead. 3. Locate and examine the code that retrieves the last checkpoint: ```cs // ---------------------------------------------------------- // // Retrieve the last checkpoint position from JSON Read Model // // ---------------------------------------------------------- // var streamPosition = hasExistingReadModel // Check if the checkpoint exists.. ? FromStream.After(StreamPosition.FromInt64(readModel.Checkpoint)) // if so, subscribe from stream after checkpoint.. : FromStream.Start; // otherwise, subscribe from the start of the stream ``` The checkpoint is retrieved from the deserialized read model. If no read model/checkpoint is found or it is the first time the application is executed, we can retrieve the default start position. ::: info Understanding Checkpoints A projection often uses a checkpoint to recover the position of the last processed event. This way, when an application unexpectedly crashes mid-process, the projection does not have to process all the previously processed events. ::: ::: info Storing Checkpoints for Read Models Stored as File Checkpoints for read models that are stored as files can often be conveniently saved within the file along with the data structure. This way both the checkpoint and read model can be atomically saved together: ```json { "checkpoint": 42, // <--- CHECKPOINT "salesReports": { // <--- READ MODEL "2025-01-31": { "categories": { "Electronics": { "regions": { "Asia": { "dailySales": 5200.25, "monthEndSalesTarget": 6000.00, "totalMonthlySales": 60200.90, "targetHitRate": 86.71 }, // ... ``` ::: 4. Locate and examine the code that subscribes to stream: ```cs await using var subscription = kurrentdb.SubscribeToStream( // Subscribe to events.. "$et-order-placed", // from the order placed event type system projection.. streamPosition, // from this position.. true); // with linked events automatically resolved (required for system projections) ``` A subscription is created that subscribes to events from the `$et-order-placed` stream. The subscription will only retrieve events starting from `streamPosition` in the stream (i.e., the checkpoint retrieved from the previous step). ::: info Different Types of Subscriptions This sample uses catch-up subscriptions to subscribe to events. You can also use persistent subscriptions or connectors to achieve a similar result. For more information about catch-up subscriptions, [click here](/clients/subscriptions.html). For more information about persistent subscriptions, [click here](/clients/persistent-subscriptions.html). For more information about connectors, [click here](/server/v24.10/features/connectors/). ::: 5. Locate and examine the code that processes each event: ```cs await foreach (var message in subscription.Messages) // Iterate through the messages in the subscription { if (message is not StreamMessage.Event(var e)) continue; // Skip this message if it is not an event if (EventEncoder.Decode(e.Event.Data, "order-placed") // Try to deserialize the event to an OrderPlaced event is not OrderPlaced orderPlaced) continue; // Skip this message if it is not an OrderPlaced event ReportProjection.ReportProjection.ProjectOrderToReadModel( // Project the event to the read model orderPlaced, readModel); readModel.Checkpoint = e.OriginalEventNumber.ToInt64(); // Set the read model checkpoint to the event number of the event we just read SaveReadModel(readModel, readModelPath); // Save the read model to the JSON file Console.WriteLine($"Projected event " + $"#{e.OriginalEventNumber.ToInt64()} " + $"{e.Event.EventType}"); } ``` For each event, the previous code: * Filters for valid event messages and order-placed events only * Updates the read model with each order via `ProjectOrderToReadModel` * Tracks progress by updating the checkpoint in the read model after each event * Persists changes by saving the updated model to JSON * Runs continuously to handle the event stream ::: warning Updating Read Model and Checkpoints Atomically To ensure atomicity and consistency, the updates to the read model and checkpoint should be committed within the same transaction. This guarantees that the update to both will succeed or fail together, preventing data inconsistencies like outdated read models or incorrect checkpoint positions. It also simplifies error recovery and ensures the system remains in sync. ::: ::: info Exactly-once processing with Catch-up Subscription This implementation ensures exactly-once processing by using KurrentDB for reliable persistence, idempotent projection logic, and transactional updates. The read model and checkpoint are updated atomically, preventing duplicates or inconsistencies, unlike traditional message brokers that rely on at-least-once or at-most-once delivery. ::: ## Step 7: Examine the Report Projection Logic In this step, you will explore the report projection logic that transforms the order events into the month-end sales reports. The code located in `./ReportProject/ReportProjection.cs`: * Takes an OrderPlaced event and a ReportReadModel as inputs * Extracts key information like order date and geographic region * Updates two sales metrics in the report for each product category and region: * Monthly sales totals * Daily sales 1. Run this command in the terminal to open the main program for the projection application: ```sh code ./ReportProjection/ReportProjection.cs ``` This file is responsible for transforming `ReportReadModel` based on an `OrderPlaced` event. 2. Locate and examine `ProjectOrderToReadModel()`: ```cs public static void ProjectOrderToReadModel(OrderPlaced orderPlaced, ReportReadModel readModel) { var orderDate = DateOnly.FromDateTime(orderPlaced.At!.Value.DateTime); // Convert the order date to DateOnly without time var orderRegion = orderPlaced.Store!.geographicRegion!; // Get the order region from the store object var finalDayOfTheMonth = // Get the last day of the month for the order date new DateOnly(orderDate.Year, orderDate.Month, DateTime.DaysInMonth(orderDate.Year, orderDate.Month)); ProjectToMonthEndReport(); // Project the order to the month-end report //... } ``` `ProjectOrderToReadModel()` is the main method of the projection which takes the `OrderPlaced` event and updates the `ReportReadModel` that is passed in. The method prepares several variables for the projection and calls `ProjectToMonthEndReport()`. 3. Locate and examine `ProjectToMonthEndReport()`: ```cs void ProjectToMonthEndReport() // Project the order to the monthly sales for the last day of the month { ProjectToMonthlySales(finalDayOfTheMonth); // Project to monthly sales for the last day of the month if (orderDate == finalDayOfTheMonth) // If the order date is the last day of the month ProjectToDailySales(finalDayOfTheMonth); // Project the order's total to daily sales for the last day of the month } ``` `ProjectToMonthEndReport()` adds the order’s total to the monthly sales for the month-end report. If the order is placed on the last day of the month, it also adds the total to the daily sales for that day. These updates are performed using the `ProjectToMonthlySales()` and `ProjectToDailySales()` methods. 4. Locate and examine `ProjectToMonthlySales()` and `ProjectToDailysales()`: ```cs // Project the order's totals to monthly sales void ProjectToMonthlySales(DateOnly reportDate) { var report = GetReportAsOf(reportDate); // Get the report as of the requested date foreach (var lineItem in orderPlaced.LineItems!) // Iterate through each line item in the order { report.IncrementMonthlySales(lineItem.category, orderRegion, // Increment the monthly sales by line item's total lineItem.total); } } // Project the order's totals to daily sales void ProjectToDailySales(DateOnly reportDate) { var report = GetReportAsOf(reportDate); // Get the report as of the requested date foreach (var lineItem in orderPlaced.LineItems!) // Iterate through each line item in the order { report.IncrementDailySales(lineItem.category, orderRegion, // Increment the daily sales by line item's total lineItem.total); } } ``` Both methods first retrieve the month-end report and then increment the daily or monthly sales for the appropriate category and region. This is done for each line item in the order. --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-4.md --- # Part 4: Time Travel with Pre-computed Read Model In the previous section, the projection generated and stored a read model of the month end sales report that only showed its latest state. In this part, you will modify the projection to also record historical snapshots of the report. This will allow you to time travel to previous states of the report. ![Time Traveling Report](../images/time-travel-report.gif) ## Step 8: Add Time Travel Support to Report Projection In this step, you will modify the projection so that the read model includes sales data for every day of the month, not just the most recent day. For example, the read model will look like this: ```json { "checkpoint": 42, "salesReports": { "2025-01-31": { "categorySalesReports": { "Electronics": { "regionSalesReports": { "Asia": { "dailySales": 5200.25, "targetSales": 6000.00, "totalMonthlySales": 60200.90, "targetHitRate": 86.71 } } } } }, "2025-01-30": { "categorySalesReports": { "Electronics": { "regionSalesReports": { "Asia": { "dailySales": 5000.50, "targetSales": 6000.00, "totalMonthlySales": 55000.75, "targetHitRate": 83.34 } } } } } // Capture state of the read model for rest of the month // "2025-01-29": {} // "2025-01-28": {} } } ``` ::: info Pre-computed Read Model is not Required for Time Traveling Notice how the read model is again denormalized with all sales figures precalculated beforehand. This increases performance and decreases load time when the sales report is rendered. However, time traveling does not require a read model that is pre-generated and saved to disk. The following are alternate implementation examples: * Provide a more normalized read model that only contains the events throughout the month * Instead of providing a read model that is pre-computed, supply an API instead that will construct the read model on-demand Which approach you choose depends on your requirements and what you and your team are comfortable with. ::: ::: tip Granularity of Snapshot In this example, snapshots of the report are captured daily. However, you can choose a different level of granularity for your snapshots. The most detailed option is to create a snapshot for every change (by stream revision number), while less frequent options include weekly, monthly, or even annual snapshots. The optimal granularity for your system depends on your specific requirements. ::: 1. Run this command in the terminal to stop the projection app: ```sh ./scripts/stop-app.sh ``` 2. Run this command in the terminal to delete the existing read model: ```sh rm ./data/report-read-model.json ``` ::: info Is it Safe to Delete Read Models that were Persisted? It is generally safe to delete read models because they are not the source of truth; they are materialized views derived from replaying events stored in the event store. As long as you retain all historical events, you can always rebuild any deleted read model by replaying those events. Pre-computed read models are often considered a type of cache: they provide fast, query-optimized access to data, but can always be regenerated from the underlying event store if lost or corrupted. Their main purpose is to speed up queries and improve performance, not to serve as the primary source of truth. However, ensure that no unique or irreproducible data is stored only in your read models, and be aware that rebuilding can be time-consuming if your event store is very large. ::: 3. Run this command in the terminal to open the report projection code file: ```sh code ./ReportProjection/ReportProjection.cs ``` 4. Implement the empty `ProjectToMonthEndReportSnapshots()` method. ::: tip You should be able to achieve this by using the variables and methods found in the file such as: * orderDate * finalDayOfTheMonth * ProjectToMonthlySales() * ProjectToDailySales() You should also be able to do so without modifying any other method or classes in this project. ::: :::: tip Solution to the Problem You can find the solution to the problem below, or in the file: `./ReportProjection/SolutionToProjectToMonthEndReportSnapshots.txt` ::: details Expand to see the solution ```cs void ProjectToMonthEndReportSnapshots() { for (var i = orderDate.Day; i <= finalDayOfTheMonth.Day - 1; i++) // For each subsequent day in the month after the order date { var snapShotDate = new DateOnly(orderDate.Year, orderDate.Month, i); ProjectToMonthlySales(snapShotDate); // Project the monthly sales to a snapshot of the report } if (orderDate != finalDayOfTheMonth) // Don't project daily sales if the order date { // is the final day of the month ProjectToDailySales(orderDate); // since it is already projected } // by ProjectToMonthEndReport } ``` ::: :::: 5. Run this command in the terminal to start the projection app: ```sh ./scripts/start-app.sh ``` 6. Run this command in the terminal to display the log (in follow mode) to check if the app ran properly: ```sh docker compose --profile app logs -f ``` Within a few seconds, you should see many messages that indicate the read model is being updated: ``` reportprojection | Projected event #XXX order-placed ``` 7. Press `ctrl + c` to exit follow mode. 8. Run this command in the terminal to check the resulting read model: ```sh code ./data/report-read-model.json ``` 9. Run this command in the terminal to check if it matches the expected read model: ```sh code ./data/report-read-model-expected.json ``` 10. Repeat 1. to 9. until the results match ## Step 9: Explore Time Travel Capability in the Report Web Application 1. Run this command in the terminal to start the sales report web application: ```sh ./scripts/start-web.sh ``` You will receive a message, like below, printed in the terminal: ``` All apps are running. URL to Demo Web Application 👉 http://XXXXXXXX ``` 2. Copy the URL printed in the terminal and navigate to it in a browser. The page shows a snapshot of the month end report for a particular day. 3. Move the slider at the top to show snapshots of the report for different days. --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-5.md --- # Part 5: Time Travel with On-demand Event Replay In the previous section, you learned how to enable time-traveling functionality in the report web app by using projections to add historical snapshots of the report to the read model for quick querying. Time-traveling can be implemented in various ways, and pre-computing read model is just one approach. Another simpler method is to replay events on-demand from a stream up to a specific point in time. ::: tip Constructing Read Models: Pre-computed vs. On-demand Approaches Read models are generally constructed in two ways: **Pre-computed Read Models:** * Events are processed in the background and projected into pre-computed views * Results are stored in a database or file (like the JSON report in the previous part) * Deliver optimized query performance through pre-computation, ideal for read-heavy applications with frequent access patterns that require minimal query-time processing * This was demonstrated in the last part where the read model is stored as a JSON file **On-demand Read Models:** * Events are read and processed at query time directly from the event store * Read models are constructed on-the-fly when requested * Offer always-current data with no staleness concerns while maintaining architectural simplicity and eliminating the need for duplicate storage * This is demonstrated in this part where the read model is returned to the caller immediately You should always choose the approach that best fits your specific use case requirements. ::: In this section, you will explore how to enable time-traveling by leveraging KurrentDB's ability to read events on-demand up to a certain point. ::: tip Why KurrentDB Makes On-Demand Time Travel Easy In many traditional systems, reconstructing historical data can be challenging. This is because such systems often require you to create periodic snapshots, or they use message brokers that delete messages once they are consumed. As a result, it becomes difficult or even impossible to revisit the exact state of your data at a specific point in the past. KurrentDB takes a different approach. It immutably stores every event, so no event is ever lost or overwritten. This lets you replay events up to any point in time, on demand, to reconstruct the exact state of your data, even years after the original events occurred. This enables powerful auditing, debugging, and compliance, ensuring your historical data is always accessible when needed. ::: ## Step 10: Discover the Auditing Capabilities in the Report Web Application The report web application includes auditing features that demonstrate time-traveling through on-demand event replay. These capabilities allow you to examine which events contributed to specific sales figures and at different points in time. 1. Run this command in the terminal to start the sales report web application: ```sh ./scripts/start-web.sh ``` You will receive a message, like below, printed in the terminal: ``` All apps are running. URL to Demo Web Application 👉 http://XXXXXXXX ``` 2. Copy the URL printed in the terminal and navigate to it in a browser. 3. Click on the following cell: * **Metric**: Daily Sales * **Category**: Beauty * **Region**: Oceania Notice the events displayed on the right. These events represent the contributions to the daily sales. ![Events for Daily Sales](../images/events-for-daily-sales.png) ::: info Quick Quiz Do the totals of the events add up to the daily sales? ::: 4. Click on the link `Event #531 in $et-order-placed` This opens up KurrentDB Admin UI to show the details of the event. ::: info Quick Quiz Do the prices and amounts in the line items add up to the total shown in the event summary in the report? ::: 5. Click on the following cell: * **Metric**: Total Monthly Sales * **Category**: Beauty * **Region**: Oceania The events on the right now displays events that contributed to the total monthly sales. ![Events for Total Monthly Sales](../images/events-for-total-monthly-sales.png) ::: info Quick Quiz Do the totals of the events add up to the total monthly sales? ::: 6. Adjust the slider to time travel back in time. Observe how the events on the right were updated to show only the events relevant to the selected time period. ## Step 11: Examine the Event Audit API The Event Audit API demonstrates on-demand time traveling by reading events directly from KurrentDB up to a specific checkpoint. This approach constructs historical views without requiring pre-computed snapshots. 1. Run this command in the terminal to open the report web app: ```sh code ./DemoWeb/Program.cs ``` 2. Locate and examine the declaration for the `/api/events` endpoint: ```cs app.MapGet("/api/events", async (long checkpoint, DateTimeOffset date, string region, string category, SalesFigureType salesFigureType) => { var orderEventSummaryList = new List(); // Create a list to hold filtered order events var readResults = kurrentdb.ReadStreamAsync(Direction.Forwards, // Read the stream in the forward direction "$et-order-placed", StreamPosition.Start, resolveLinkTos:true, // from the start of the $et-order-placed stream maxCount: checkpoint + 1); // up to the checkpoint + 1 (note: checkpoint is zero-based) await foreach (var resolvedEvent in readResults) // For each event in the stream { if (EventEncoder.Decode(resolvedEvent.Event.Data, "order-placed") // Try to deserialize the event to an OrderPlaced event is not OrderPlaced orderPlaced) continue; // Skip this message if it is not an OrderPlaced event if (OrderDoesNotMatchRegionOrCategory(orderPlaced)) continue; // Skip if the order does not match the requested region or category switch (salesFigureType) { case SalesFigureType.DailySales: // If the sales figure type is daily sales if (OrderDoesNotMatchRequestDate(orderPlaced)) continue; // Skip if the order was not placed on the report date break; case SalesFigureType.TotalMonthlySales: // If the sales figure type is total monthly sales if (OrderIsPlacedAfterRequestDate(orderPlaced)) continue; // Skip if the order was placed after the report date break; default: throw new ArgumentOutOfRangeException(); // If the sales figure type is not recognized, throw an exception } var eventNumber = resolvedEvent.OriginalEventNumber.ToInt64(); // Get its event number from the stream return orderEventSummaryList.OrderByDescending(x => x.EventNumber) // Order the list by event number in descending order .ToList(); // and convert it to a list } return orderEventSummaryList; // ... } ``` This endpoint performs the following: * Reads the `$et-order-placed` stream from the start to the checkpoint * For each event in the stream, skip it if: * The event can not be deserialized to `OrderPlaced` object * The order does not match the requested region or category * The order does not match the requested date if events for daily sales was requested * The order is placed after the requested date if events for total monthly sales was requested * Create an event summary and append to the `orderEventSummaryList` which is returned to caller 3. Run this command in the terminal to test the API: ```sh curl -s "http://localhost:3000/api/events?checkpoint=534&category=women®ion=middle-east&date=2025-01-31&salesFigureType=0" | jq ``` This queries the events from the stream up to event number 534 and filters out orders made for `women` category placed in the `middle-east` region on `2025-01-31`. This returns the total daily sales (for total monthly sales, set `salesFigureType` to `1`) This results in a JSON similar to this: ```json [ { "eventNumber": 526, "orderId": "order-c469503f30164b6b91fcf1f1d37491a9", "at": "2025-01-31T10:43:38.2328057+00:00", "region": "Middle-East", "category": "women", "totalSalesForCategory": 1679.37 }, { "eventNumber": 519, "orderId": "order-7c18a37d1cf04cd792a4adcfb8d258c4", "at": "2025-01-31T00:52:29.8409144+00:00", "region": "Middle-East", "category": "women", "totalSalesForCategory": 2798.95 } ] ``` --- --- url: >- https://docs.kurrent.io/dev-center/use-cases/time-travel/tutorial/tutorial-intro.md --- # Introduction This tutorial guides you through implementing time-travel capabilities in your applications using KurrentDB. You will learn how to reconstruct and query the state of your system at any point in history, an essential feature for auditing to understand data evolution. ## Objectives In this tutorial, you will: * Learn the principles of time-traveling in event-native applications * Project events into read models for sales reporting * Enable time-travel by recording and querying historical states * Explore both snapshot-based and on-demand time-travel approaches * Discover interactive interfaces to visualize historical data * Audit the lineage of data changes ## Prerequisites Before starting, ensure you have the following: * A GitHub account to use GitHub Codespaces * Basic knowledge of C# * Familiarity with command-line operations ## Tutorial Overview This tutorial consists of the following steps: ### [Part 1: Set up Codespaces](tutorial-1.md) 1. **[Set up your Codespaces](tutorial-1.md#step-1-set-up-your-codespaces)**: Start an interactive coding environment in your browser where all tools and databases are installed. ### [Part 2: Initialize KurrentDB with Sample Orders](tutorial-2.md) 2. **[Start Databases and Append OrderPlaced Events](tutorial-2.md#step-2-start-databases-and-append-orderplaced-event-to-kurrentdb)**: Start KurrentDB and append sample order events for use in reporting. 3. **[Browse OrderPlaced Events in KurrentDB Admin UI](tutorial-2.md#step-3-browse-orderplaced-events-in-kurrentdb-admin-ui)**: Explore the event streams in the Admin UI. ### [Part 3: Project Events to Sales Report Read Model](tutorial-3.md) 4. **[Start the Report Projection Application](tutorial-3.md#step-4-start-the-report-projection-application)**: Start the app that listens for `OrderPlaced` events and builds a denormalized JSON sales report read model. 5. **[Start and Browse the Report Web Application](tutorial-3.md#step-5-start-and-browse-the-report-web-application)**: Open the web app to view the sales report generated from the read model. 6. **[Examine the Report Projection Application](tutorial-3.md#step-6-examine-the-report-projection-application)**: Review how the projection app loads, updates, and saves the read model in response to events. 7. **[Examine the Report Projection Logic](tutorial-3.md#step-7-examine-the-report-projection-logic)**: Explore how the projection logic transforms order events into the month-end sales report. ### [Part 4: Time Travel with Pre-computed Read Models](tutorial-4.md) 8. **[Add Time-Travel Support to the Sales Report Projection](tutorial-4.md#step-8-add-time-travel-support-to-report-projection)**: Modify the projection so the read model records sales data for every day of the month, enabling time-travel queries. 9. **[Explore Time Travel Capabilities in the Report Web Application](tutorial-4.md#step-9-explore-time-travel-capability-in-the-report-web-application)**: Use the web app's time slider to view historical snapshots of the sales report for any day. ### [Part 5: Time Travel with On-demand Event Replay](tutorial-5.md) 10. **[Discover the Auditing Capabilities in the Report Web Application](tutorial-5.md#step-10-discover-the-auditing-capabilities-in-the-report-web-application)**: Use the web app to audit and reconstruct the sales report state at any point in time by replaying events on demand. 11. **[Examine the Event Audit API](tutorial-5.md#step-11-examine-the-event-audit-api)**: Review how the API reads and filters events from the event store to support on-demand time-travel and auditing. --- --- url: 'https://docs.kurrent.io/getting-started/agent-skills.md' --- # Agent Skills Kurrent Agent Skills give AI coding assistants (Claude Code, Cursor, Codex, GitHub Copilot CLI, and other agent platforms) first-class knowledge of [Kurrent](https://kurrent.io) and its ecosystem. The repository ships one plugin (`kurrent`) containing five skills and three agents. Each ships focused references that the agent loads on demand, so it can answer your questions and write your code with accurate, up-to-date context instead of guessing. The skills are open source and maintained at [github.com/kurrent-io/skills](https://github.com/kurrent-io/skills). ## Skills | Skill | What it covers | |-------|----------------| | [kurrent-docs](https://github.com/kurrent-io/skills/tree/main/skills/kurrent-docs) | The everyday router for SDK and server work. Covers the six client SDKs, the self-hosted KurrentDB server, Kurrent Cloud, and the `esc` CLI. **Start here.** | | [kurrentdb-connection](https://github.com/kurrent-io/skills/tree/main/skills/kurrentdb-connection) | Opinionated guidance for configuring the gRPC client across all six SDKs. Covers connection strings, node discovery, keepalive, deadlines, serverless reuse, and connection failure triage. | | [kurrentdb-client-detection](https://github.com/kurrent-io/skills/tree/main/skills/kurrentdb-client-detection) | Inventories the client surface in an application codebase. Finds which SDK, which connection scheme, and which call sites need rewriting. | | [kurrentdb-server-detection](https://github.com/kurrent-io/skills/tree/main/skills/kurrentdb-server-detection) | Inventories a deployed server. Finds the version, cluster topology, license status, and deployment method. | | [kurrent-upgrade](https://github.com/kurrent-io/skills/tree/main/skills/kurrent-upgrade) | Onboarding onto the gRPC client. Covers porting an app off the legacy TCP client and rebranding the EventStoreDB gRPC client to KurrentDB across six languages. | ## Agents Three agents cover the KurrentDB application lifecycle: migration, review, and runtime diagnosis. | Agent | What it covers | |-------|----------------| | [migration-specialist](https://github.com/kurrent-io/skills/tree/main/agents/migration-specialist.agent.md) | Orchestrates a client migration. Detects the current client and connection scheme, routes to the right upgrade flavour, and walks through it step by step. | | [code-reviewer](https://github.com/kurrent-io/skills/tree/main/agents/code-reviewer.agent.md) | Reviews application code that uses the client SDKs. Covers idiomatic usage and anti-patterns, with a post-migration mode that audits the legacy surface and runs the build and tests as a gate. | | [troubleshooter](https://github.com/kurrent-io/skills/tree/main/agents/troubleshooter.agent.md) | Diagnoses runtime failures. Covers connection and TLS errors, version conflicts, subscription lag, leader-election and gossip problems, scavenge hangs, and projection divergence. | ## Installation ### Claude Code Run the following commands from a Claude Code session: 1. Add the marketplace: ```bash /plugin marketplace add kurrent-io/skills ``` 2. Install the plugin: ```bash /plugin install kurrent@kurrent-skills ``` 3. Follow the prompts to complete the installation, then run `/reload-plugins` to activate it. ### Cursor Run the following commands from a Cursor session: 1. Add the marketplace: ```bash /add-plugin kurrent-io/skills ``` 2. Follow the prompts to install the `kurrent` plugin from the marketplace UI. ### Codex 1. Add the `kurrent-io/skills` marketplace to Codex: ```bash codex plugin marketplace add kurrent-io/skills ``` 2. Start Codex and open the plugins browser: ```bash /plugins ``` 3. Navigate to the "Kurrent Skills" tab and install the `kurrent` plugin. > **Note:** Codex's native plugin spec installs the skills but not the agents. Skills that delegate to them will still work, but Codex handles the orchestration inline instead of spawning specialised subagents. Once Codex's plugin spec supports custom agents, this gap goes away. ### GitHub Copilot CLI 1. Add the marketplace: ```bash copilot plugin marketplace add kurrent-io/skills ``` 2. Install the plugin: ```bash copilot plugin install kurrent ``` 3. Verify it loaded: ```bash copilot plugin list ``` From an interactive session, `/plugin list`, `/agent`, and `/skills list` should show the `kurrent` plugin. ### Vercel skills CLI (skills.sh) Works with Claude Code, Cursor, Cline, opencode, and any other agent the [Vercel skills CLI](https://github.com/vercel-labs/skills) supports. The CLI auto-detects installed agents and links each skill into the right place. 1. List the skills available in the repository: ```bash npx skills add kurrent-io/skills --list ``` 2. Install everything to every detected agent: ```bash npx skills add kurrent-io/skills --all ``` 3. Or install a specific skill into a specific agent — for example, the everyday router into Claude Code globally: ```bash npx skills add kurrent-io/skills \ --skill kurrent-docs \ -a claude-code -g ``` Pass `-y` for non-interactive installs in CI. ### Local install from repository 1. Clone the repository: ```bash git clone https://github.com/kurrent-io/skills.git ``` 2. Install the skills for your harness: Copy the `skills/` directory (or individual skills under it) to the location where your coding agent reads its skills or context files. Refer to your agent's documentation for the correct path. ## Contributing Contributions and feedback are welcome on the [`kurrent-io/skills`](https://github.com/kurrent-io/skills) repository. See [`AGENTS.md`](https://github.com/kurrent-io/skills/blob/main/AGENTS.md) for contribution guidelines. --- --- url: 'https://docs.kurrent.io/getting-started/concepts.md' --- # Concepts This section discusses some of the fundamental concepts of event sourcing and KurrentDB. ## Event In KurrentDB, an event is a factual occurrence from the past. It has an *event type* that headlines what happened and an *event body* that outlines the details: ![What are events](./images/what-are-events.png#light) ![What are events](./images/what-are-events-dark.png#dark) An event usually represents a state change in an application, its entities, or business processes. In this case below: 1. Ada requested a $1,000 loan. 2. Bob approved the loan. 3. A loan payment of $50 was received. ![Examples of events](./images/examples-of-event.png#light) ![Examples of events](./images/examples-of-event-dark.png#dark) ::: note To learn more about how to perform basic operations around events, [click here](/clients/appending-events.md). ::: ## Event Log The event log is an append-only sequence of events stored within the database. It is the ultimate source of truth, capturing every event appended to KurrentDB: ![Event log](./images/event-log.png#light) ![Event log](./images/event-log-dark.png#dark) New events are added exclusively to the end of the log—never at the start or in the middle: ![Immutable log](./images/event-log-is-append-only.png#light) ![Immutable log](./images/event-log-is-append-only-dark.png#dark) Internally, the event log consists of a series of data files that store events in the exact order in which they were appended. ## Event Stream KurrentDB's event log can store billions of events, many of which might be unrelated. Events are commonly arranged into smaller, logically related groups known as **event streams** to keep them organized and speed up retrieval. An event stream is a sequenced group of events from the event log that is identified by a stream ID: ![Event stream](./images/what-are-event-streams.png#light) ![Event stream](./images/what-are-event-streams-dark.png#dark) ### Example of Event Stream Consider managing a loan system with millions of loan applications and customer events. Searching a single, vast event log for specific loan or customer details is slow and cumbersome. Instead, organizing events into fine-grained streams using a unique Loan ID or Customer ID allows faster access. For example, a stream named "loan-123" would hold only the events related to Loan ID #123, while "customer-321" would contain events specific to Customer ID #321. This setup enables quick access to relevant events without combing through the entire event log. ### Event Stream Basics Event stream consists of a **stream ID** (a simple string) and a **sequence of events**. Event streams improve the read and event retrieval performance through indexing. It also helps enforce business constraints across related events using optimistic concurrency control. To append an event to KurrentDB, it must be associated with a specific stream ID. This process simultaneously appends the event to the event log and the specified stream: ![How events are appended](./images/how-events-are-appended.png#light) ![How events are appended](./images/how-events-are-appended-dark.png#dark) ::: note To learn more about streams, its structure and behaviors in detail, [click here](@server/features/streams.md). ::: ### Event Stream Design In KurrentDB, a stream typically represents an instance of an object, entity, or business process. For example: | Stream ID | Description | |--------------|--------------------------------------------------------------------| | loan-123 | Represents loan application ID#123 | | customer-321 | Represents customer ID#321 | | payment-222 | Represents payment process ID#222 from an external payment gateway | However, there are cases where it makes sense for a stream to encompass a more extensive set of events. In these cases, streams may align to a broader set of events: | Stream ID | Description | |-----------------|--------------------------------------------------------| | loan | Represents all loan applications | | customer | Represents all customers | | payment-gateway | Represents all events from an external payment gateway | Designing streams and deciding which events belong to which stream involves balancing performance, scalability, and maintainability. Understanding the trade-offs is the starting point for designing streams that best suit organizational goals. ## Immutable Events KurrentDB is designed to be immutable. Once an event is appended, its type, body, or any part of it cannot be modified. The event remains unchanged forever. The same principle applies to the event log and stream; once an event is appended, its position is locked. Events are never reordered or shifted within the log or stream. This immutability guarantees data integrity and consistency while enabling performance optimizations. ::: note While events can not be updated, streams can be truncated for housekeeping purposes. To learn how this works, [click here](@server/features/streams.md#deleting-streams-and-events) ::: ## Stream Indexing A KurrentDB index entry is automatically created whenever an event is appended to the event log. The index uses the hash of the stream ID as the key, with the corresponding index entry as the value. Each entry consists of: * the stream ID * the event's number within the stream (also known as the version number or sequence number) * the event's position within the log ![Conceptual model of the index](./images/conceptual-model-of-the-index.png#light) ![Conceptual model of the index](./images/conceptual-model-of-the-index-dark.png#dark) Conceptually, this resembles a simple key-value store, where the stream's ID is the key, and the value is a set of pointers that direct you to the events within the event log. These pointers don't store the events but reference their positions in the event log. It's important to remember that neither the stream nor the index physically hold the actual events. ::: note To learn more about how indexing works in detail, [click here](@server/configuration/indexes.md). ::: ## Guaranteed Consistent Ordering in Event Log and Stream KurrentDB ensures that all events in both the event log and its streams are consistently ordered by append time. Moreover, the event log maintains a global ordering of events across all streams. ![Global order](./images/consistent-event-ordering.png#light) ![Global order](./images/consistent-event-ordering-dark.png#dark) Events within each stream also retain this global ordering, even though they are only a subset of events from the event log. This ordering is crucial for order-sensitive operations where the correct sequence of events is necessary. For example, running a complex fraud detection system relies on the precise order of events across multiple accounts and customer interactions. ## Fine-grained Event Streams KurrentDB natively supports billions of streams. This enables a design that leverages fine-grained streams, providing precise control to track and isolate individual entities, actions, or processes—even when working at a massive scale. Organizing events into smaller, more focused streams enhances the speed and efficiency of reads and boosts system performance by processing only the relevant events. For example, a business can maintain dedicated streams for each of its millions of customers. Each stream would contain the complete history of interactions across various systems, providing an unparalleled, detailed view of the customer journey. Without fine-grained streams, events are lumped into longer, disorganized streams, leading to a mix of loosely related data. This makes locating and processing events for a specific customer slower and less efficient, as unrelated events must be sifted through simultaneously. ## Optimistic Concurrency Control With KurrentDB, optimistic concurrency control can be applied to prevent accidental overwrites or lost updates due to race conditions. This is especially important when multiple writers try to append to the same stream concurrently without checking if the stream has already changed. This is crucial to enforce business constraints across events in a stream. For example, a financial institution has a stream representing a digital wallet where overdrawing is prohibited. The stream contains deposit and withdrawal events specific to the wallet. KurrentDB prevents someone from making multiple withdrawals simultaneously to draw funds that are not available. ![Optimistic concurrency](./images/optimistic-concurrency-control.png#light) ![Optimistic concurrency](./images/optimistic-concurrency-control-dark.png#dark) Optimistic concurrency control also operates without requiring resource locks, meaning these protections come without the performance hit of managing locks. This helps maintain high performance even when multiple concurrent writes occur. ::: note To learn more about how to apply optimistic concurrency control, [click here](/clients/appending-events.md#handling-concurrency) ::: --- --- url: 'https://docs.kurrent.io/getting-started/evaluate/business-process-support.md' --- # How KurrentDB Supports your Business Processes KurrentDB is well-suited to manage and track a wide range of business processes, from simple to sophisticated. While it's designed to handle processes that exhibit all the characteristics below, KurrentDB is an excellent fit if your business processes demonstrate one or more of these traits. | Business Process Requirement | Processes Descriptions | How KurrentDB Supports | | --- | --- | --- | | Auditable and traceable | Processes where every action or change you make is recorded and can be reviewed later, such as financial transactions or compliance checks. For example, you might track all customer interactions in a support system or monitor every step in a food processing plant. | KurrentDB captures every change in its immutable event log, providing a complete, reliable history, and you can organize events into one of its fine grained streams to support quick event retrieval for analysis. You can also add causation and correlation IDs to events to track the cause and effect of those events. | | Distributed and interdependent with multiple systems | Processes where multiple systems, applications, or components rely on one another to complete tasks or functions, requiring coordinated communication and updates. These processes can be difficult to evolve and often face delays and errors due to problems with dependent systems. | KurrentDB communicates asynchronously with other applications using its connector and subscription subsystem. This is a great way to decouple systems, allowing different parts of your business to scale and evolve independently, while still maintaining seamless communication and coordination between processes. | | Reactive to change (Real-time) | Processes that immediately respond to new data or events as they happen, such as updating your account balance right after a transaction, or triggering an alert in your fraud detection system. | Every event captured by KurrentDB can be delivered to you or your consumer in real-time or on-demand through its connectors or subscription subsystem. This allows you to instantly stream and track events while providing a complete historical record for contextual analysis if you need to replay events. | | Support variety of flexible use cases | Processes that possess a variety of basic, alternative, and exceptional flows allow your business to adapt to customer needs and concerns. For example, an ordering system that supports returns, exchanges, pre-ordering, cross-border ordering, gift cards, and store credits. | KurrentDB’s immutable event log provides visibility across every step of a process, which simplifies troubleshooting and debugging. Events and asynchronous communication allow you to design highly flexible use cases as you can notify various applications and users to handle exceptional use cases as they occur without introducing excessive dependencies. Fine-grained streams allows you to quickly access events from a specific entity or process. | | Long-running | Activities that take an extended period to complete often require multiple steps and interactions involving several people or systems. For instance, you might experience this in the ordering process, from placing the order to receiving the delivery. | With KurrentDB’s durable event log, which persists all events indefinitely, you can easily track long-running business processes. Its sequential and globally ordered structure simplifies the reconciliation of events across different applications over time, allowing you to deploy eventually consistent operations more efficiently and enhance your overall development experience. | | Order-sensitive | Order-sensitive (or non-commutative) business processes are workflows where you must follow tasks in a specific sequence to ensure successful execution. For example, a particular order of actions across different accounts triggers an alert in fraud detection. Similarly, generating a global sales report requires consolidating data from multiple sources and currency conversion to USD, which depends on when the foreign exchange rate is updated in the system. | KurrentDB’s globally consistent event log aggregates events from multiple data sources, making it the de facto source of truth for their ordering when they were not ordered in the first place. This simplifies implementing order-sensitive operations, even when clocks between your applications aren’t synchronized. | | Automatable | Repetitive, rule-based tasks that are performed with minimal human intervention using software. Examples include invoice processing, customer onboarding, and inventory management. A common challenge is ensuring your data is consistent and visible across systems due to complex workflows and distributed operations | KurrentDB’s connector and subscription subsystems allow the streaming or publishing of events to other applications, helping to automate cross-application business processes. The event log ensures resilience, as checkpoints save the last completed actions. If a service fails, it can return to a previously known state rather than starting from scratch. | | Adaptable to evolving requirements | Processes designed to change and grow as business requirements, needs, regulations, or market conditions shift. For example, an e-commerce platform may need to modify its inventory management process to handle new trends in product availability or shipping delays, or a financial service provider might have to adjust its loan approval process to meet new compliance requirements | In KurrentDB, events are immutable facts that don’t change. As requirements evolve, events can be replayed to regenerate read models that align to the new criteria. Additionally, with KurrentDB, your applications can be more loosely coupled, allowing you to break down large monolithic systems into smaller, independent services. This reduces your upgrade footprint, as only a few services may need updates, lowering the overall risk compared to upgrading an entire monolith. | | Compliant with rules, constraints, and regulations | Processes within your organization that follow operational, industry standards, or legal requirements to ensure smooth operations. For example, your financial transactions adhere to banking regulations, and your employee payroll systems comply with labor laws, tax regulations, and your company’s policies on wages and benefits. | KurrentDB's event log provides a complete and immutable history of changes in your application or auditing requirements. By using optimistic concurrency controls, you can ensure transactional consistency when needed. Additionally, the sequencing and ordering guarantees of events simplify the implementation of eventually consistent operations. | | Consistent | Processes with data that you need to keep synchronized across systems, applications, or databases while adhering to business rules and constraints. This ensures that your data remains trustworthy and meaningful for various reports and analysis. | KurrentDB's event log can act as your application transaction log and historical source of truth, allowing you to reliably synchronize downstream applications eventually, even when different parts of your system are not up to date due to delays or downtime. KurrentDB's optimistic concurrency control can enforce rules that require immediate consistency without locking or performance penalties. | | Performant | Processes that operate efficiently and can handle high demands with minimal delays. For example, you might manage real-time financial transactions, oversee order processing systems, or handle inventory management for many users. | KurrentDB’s append-only log is optimized for handling write-heavy workloads. You can let other applications or services project events into read models optimized for reading like a cache. You can also use multiple read replicas and subscribers to handle many events. | | Scalable | Processes that efficiently handle increased workloads without a significant drop in performance or needing a proportional increase in resources. For example, imagine you're running a global e-commerce retailer that needs to scale to meet peak holiday season traffic. | Applications that use KurrentDB can be effectively decoupled from other applications, making them more scalable. KurrentDB supports read replicas to help you scale the number of reads it handles. | | Measurable | Processes within your organization that you can track and evaluate, such as a manufacturing process where each step is measured to analyze and optimize for operational efficiency. | KurrentDB allows the analysis of past actions to identify opportunities for process optimization. By using streams, you can focus on specific criteria and retrieve events based on a particular group (such as manufacturing line #1), a specific date (like January 1st), or a status (such as in-progress or completed). | --- --- url: 'https://docs.kurrent.io/getting-started/evaluate/data-pipeline.md' --- # KurrentDB in Data Engineering Pipelines ## Introduction Traditional data pipelines processing state-based data models often struggle to meet modern demands like compliance, analytics, AI training, and operational responsiveness. These models, which capture only the current state of an entity, fall short in handling historical data, traceability, and responsiveness. This article explores these limitations and highlights how KurrentDB addresses the shortcomings of handling state-based data models in traditional pipelines. ## Challenges of Data Pipelines with State-Based Data Models Many traditional data sources use state-based data models, capturing only the current state of an entity at a specific time. While sufficient for transactional needs, this approach presents significant challenges for modern data pipelines, especially when dealing with compliance, analytics, AI training, and operational responsiveness. ### 1. Lack of Historical Data for Auditing and AI Training One of the most significant limitations of state-based systems is the lack of historical data. These systems typically overwrite values without preserving previous states, making it hard to track changes over time: * **Audits and Compliance**: Regulatory frameworks often require detailed and complete audit trails, which can be difficult for state-based systems to provide. * **AI Training and Business Analytics**: AI systems need comprehensive event histories to detect meaningful patterns and trends. ### 2. Lack of Data Lineage and Traceability State-based systems don’t readily offer data lineage or traceability. As data moves through various transformations in the pipeline, such as being aggregated in data warehouses or other sinks, it’s often unclear how this data was derived: * **No Traceability**: Without clear lineage, businesses can't trace how final reports or models were produced, leading to compliance risks and a lack of insight into their data. * **Unclear Impact of Changes**: A change in a source system can cascade through a pipeline in unpredictable ways, making it difficult to know how downstream analytics or AI models will be affected. ### 3. Lack of Data Consistency State-based models can lead to data inconsistencies when source data is overwritten without alerting downstream systems: * **Out-of-Sync Data**: Untracked changes in source data can misalign downstream systems, especially when updates occur through unofficial channels, leading to inaccurate insights. * **Hard to Detect**: Without versioning or sequencing, downstream systems struggle to identify outdated data, potentially leading to decisions based on stale information. ### 4. Lack of Data Availability and Responsive Operations State-based systems frequently rely on batch processing, which introduces delays in making data available for analysis: * **Lag in Data Availability**: Batch processing introduces delays between data generation and analysis, leading to decisions based on outdated information and reduced operational efficiency. * **Losing Consumed Events**: While streaming platforms can alleviate some latency issues, once events are consumed, they are typically discarded. This makes replaying or reanalyzing past events impossible without incurring significant storage costs. ### 5. Lack of Event Organization Many streaming platforms only provide a limited number of topics or streams, which leads to unrelated events being grouped into large, shared topics. * **Slow Event Retrieval**: Extracting relevant events from these mixed topics can be slow and resource-intensive. Consumers are often forced to process vast amounts of irrelevant data. * **Difficulty in Event Compartmentalization**: Unrelated event consumers lack dedicated topics, forcing them to share a large, mixed topic. This undermines modularity and encapsulation principles, increasing system complexity. ## Role of KurrentDB in a Data Pipeline KurrentDB serves as a repository, hub, or distribution center for events, centralizing the management of events from multiple sources. This enables KurrentDB to address many challenges inherent in traditional data pipelines. It offers a solution that enhances historical data granularity and quality while improving responsiveness to the dynamic nature of business operations. ### Four Major Functions of KurrentDB in a Data Pipeline ![](./images/four-major-functions-of-kurrentdb-in-a-data-pipeline.png) In a data pipeline, KurrentDB performs four major functions: 1. Store and collect events from various data sources 2. Process, refine, enrich, clean, and aggregate events 3. Organize events into fine-grained event streams 4. Deliver events to downstream systems ### KurrentDB in a Pipeline ![](./images/kurrentdb-in-data-pipeline.png) #### Input from Data Pipeline KurrentDB typically ingests events from these data sources: * Change data capture (CDC) from traditional databases (e.g., relational) * Databases that support event sourcing (such as KurrentDB) where events are natively stored * Messaging and Event Streaming platforms * Event-driven applications (such as sensor event recording applications) #### Processes in Data Pipeline KurrentDB primarily participates in the following data pipeline processes: * Data ingestion * Data storage * Data processing #### Output to Data Pipeline KurrentDB distributes processed and organized events to downstream storage and applications, such as: * Data warehouses * Data lakes * Business intelligence tools * Operational databases * Business applications * Messaging or streaming platforms ## How to use KurrentDB in a Data Pipeline KurrentDB plays a key role in data pipelines by providing a centralized platform to store, process, organize, and deliver events from various sources. It stores a complete history of events, offering flexibility and reliability throughout the pipeline. Here’s how it works: ### Step 1. Store Events ![](./images/central-hub-for-events.png) KurrentDB collects and stores raw events from various data sources, such as databases, applications, or event-driven systems. To maximize future flexibility, events from these sources are typically stored in their raw format within dedicated coarse-grained streams. These streams are not prematurely separated. For example, one stream might be used per data source or table rather than creating a separate stream for each entity, like a customer. The events can be ingested in real-time or through batch processing. ::: info **Key Purposes of KurrentDB**: Central Hub for Events KurrentDB acts as a hub for events from multiple data sources, storing all events in an immutable log, ensuring: * **Auditability and Compliance**: Immutable event logs create a complete audit trail, ensuring compliance with regulatory standards. * **AI Training and Behavioral Insights**: Full event histories provide richer data for AI model training and behavioral analysis. * **Event Retention**: Unlike many streaming platforms, KurrentDB retains events even after consumption, allowing them to be replayed for future use. ::: ### Step 2. Process Events ![](./images/restoration-center-for-events.png) Once events are ingested, KurrentDB allows them to be processed using its **projection subsystem**. This process makes them more useful for downstream systems, while granular events can be aggregated into higher-level insights. * **Clean**: Ensure data conforms to specific schemas, addressing missing fields or incorrect formats. * **Enrich**: Optionally add additional context, such as the reason behind specific actions, which might not be apparent from raw event data from CDC (see footnote 1). * **Aggregate**: Combine several detailed events into a more high-level integration events for easier consumption by external systems. ::: info **Key Purposes of KurrentDB**: Restoration Center for Events KurrentDB cleans and transforms raw events into enriched, refined formats that improve: * **Data Quality and Context**: Processed events are enriched with additional context, making it easier to uncover valuable business insights. * **Data Lineage**: KurrentDB maintains causation metadata, allowing businesses to trace the transformation path of events and perform root cause analysis. ::: ### Step 3. Organize Events ![](./images/sorting-facility-of-events.png) After events are processed, they are organized into fine-grained event streams. KurrentDB supports the organization of billions of streams, each tailored for specific business use cases. These streams can be de-normalized, duplicating events across multiple streams for more efficient retrieval without cross-referencing unrelated streams and events. This is particularly useful when downstream systems need to replay events for a specific customer in a company with millions of customers. Events can be organized into dedicated streams, one per customer, each containing the customer's journey and interactions across multiple systems. Contrast this with a single stream containing events from millions of customers. In the latter case, downstream systems must manually filter out irrelevant events from all other customers, wasting time and resources while increasing complexity. ::: info **Key Purposes of KurrentDB**: Sorting Facility of Events KurrentDB organizes events into streams that allow: * **Dedicated Event Streams**: Events can be categorized into specialized streams, ensuring consumers consume only the required data without sifting through irrelevant events. * **Quick Event Access**: Indexed streams provide fast access to specific subsets of events without replaying long, mixed streams like those found in traditional messaging platforms. ::: ### **Step 4. Deliver Events** ![](./images/distribution-center-of-events.png) Organized and curated events are delivered to downstream systems through KurrentDB’s connectors and subscription subsystem. These allow events to be pushed in real-time, pulled on-demand, or scheduled for batch retrieval from the fine-grained streams. ::: info **Key Purposes of KurrentDB**: Distribution Center of Events KurrentDB distributes events efficiently, ensuring: * **Immediate Data Streaming**: Subscriptions reduce latency and staleness, enabling real-time analytics and responsive operations * **Event Replayability**: Unlike traditional messaging and streaming platforms, where events are discarded after consumption, KurrentDB retains events, allowing downstream systems to replay for future analysis. * **Data Consistency and Synchronization**: With the event log’s strict sequence of events, downstream systems can track events to ensure they are processing events in the correct order and are up-to-date, identifying if any are missing or out of sync. ::: ## Conclusion State-based data models in data pipelines often lead to inefficiencies and risks, especially in compliance, AI training, and operational responsiveness. KurrentDB addresses these challenges through its immutable log, connector, subscription, and projection subsystems. This solution offers several key benefits: * **Complete historical data retention**: Provides full event histories for better auditing and AI model training. * **Enhanced data lineage and traceability**: Ensures businesses can trace the origin and transformation of data across pipelines. * **Improved data consistency**: Ensures downstream systems remain synchronized with real-time data. * **Efficient event organization and replayability**: Enables quick access to relevant data and replay of events for future analysis. By leveraging KurrentDB, organizations can overcome the limitations of traditional pipelines and achieve greater data accuracy and operational efficiency. ## Footnotes 1. In some cases, Change Data Capture (CDC) events may lack clarity regarding the intent behind a change. For example, if a vehicle is marked as "unavailable," it may be unclear whether it is reserved or undergoing maintenance. This ambiguity can only be resolved if the source application is updated to include additional contextual information, such as a "reason" field accompanying the status change. However, this challenge is generally absent in event-sourced applications, where the intent behind each action or change is usually well-defined within the event itself. --- --- url: >- https://docs.kurrent.io/getting-started/evaluate/state-vs-event-based-data-model.md --- # State vs Event-Based Data Model ## State vs Event-Based Data Model In today’s fast-evolving tech landscape, businesses need to evaluate the role and function of their databases and how data is stored. Traditional databases are designed to maintain only the current state of an application or system. In contrast, KurrentDB leverages an event-based data model where the history of changes is immutably stored as events, which can be replayed to produce the current state. Understanding the differences and nuances between these data models can help organizations make more informed decisions on how to manage its data. This article examines the nuances between these models, the challenges they address, and how KurrentDB provides an alternate approach to data management and system architecture. ## Two Perspectives: Current State and Historical Events Business objects in a system can be viewed as current state or historical events. The current state, or state-based data model, represents an object’s condition at a particular time—such as the balance or withdrawal limit of a digital wallet. In contrast, an event-based data model reflects the actions that have shaped the current state, like deposits, withdrawals, and limit updates. ### Example Leveraging a state-based data model, an individual viewing the table below understands the **current state** of a digital wallet. The wallet holder is John Doe. On September 24, 2024, the current balance is US$200 and the withdrawal limit is US$300. | Date | September 24, 2024 | | --- | --- | | Wallet Holder | John Doe | | Balance | $200 | | Currency | USD | | Withdraw Limit | $300 | Under an event-based data model, an individual can sequentially replay the historical events that compose the current state. 1. Registered John Doe to the USD digital wallet with a $100 withdrawal limit (July 1, 2024) 2. Deposited $100 to the digital wallet (July 3, 2024) 3. Withdrew $100 from the digital wallet (July 22, 2024) 4. Deposited $500 to the digital wallet (August 13, 2024) 5. Increased withdrawal limit of digital wallet to $300 (September 2, 2024) 6. Withdrew $300 from digital wallet (September 12, 2024) State-based and event-based models each offer unique insights. A state-based digital wallet provides quick and direct information, offering a clear view of "what" exists now. In contrast, the event-based model provide a complete record of historical insights and the "why" behind the digital wallet's current state, which can be derived by replaying the actions that led to the present (or any point in time). To fully manage and understand any business object, both perspectives—"what" and "why"—are necessary. ## Modern Challenges of the State-Based Data Model *Summary of modern challenges of the state-based data model:* | **Challenge Area** | **Description of Challenge** | | --- | --- | | Data Analytics | **Absence of historical context**:- Hinders advanced analytics and machine learning- Restricts understanding of user behavior over time- Limits root cause analysis and data lineage tracking | | Data Integration | **Delays between data collection and processing:**- Prevent real-time insights- Lead to inefficiencies and inconsistencies**Batching large datasets:**- Slows down system synchronization- Can be resource intensive and introduces delays | | **Application Development** | **Tightly coupled systems**:- Reduce scalability- Makes systems harder to evolve- Makes debugging difficult | ### Data Analytics Traditional RDBMS only store current states and lack the historical context crucial for advanced analytics and predictive modeling using machine learning and AI. For instance, if an e-commerce company only stores the latest status of customer purchases, it becomes difficult to analyze why certain products were returned or why specific customers churned. This absence of historical data and the "why" behind actions and behaviors makes it harder to analyze root causes and trace data lineage. ### Data Integration Batch processing, commonly used to handle large datasets, introduces delays between data collection and processing, making it harder to generate real-time insights or take swift action. For example, a logistics company may batch-process shipment data overnight, delaying the detection of errors in real-time package tracking. This lag can lead to inefficiencies and difficulties in synchronizing distributed systems, which can increase resource usage and result in inconsistent data. ### Application Development Application development also faces challenges with systems that depend on state-based data models. Due to the centralized, transactional nature of RDBMS, these systems frequently lead to tightly coupled architectures. For example, in a healthcare application, updates to patient records might require changes to multiple interconnected internal and vendor services, making the system harder to scale and maintain as new features are added or the system grows more complex. ## KurrentDB and Event-Based Data Model KurrentDB, an event-native database, effectively solves many of the challenges posed by traditional state-based models by focusing on events as the core unit of data. KurrentDB preserves the entire history of business processes by storing, processing, and delivering data as events, making it ideal for event-based data models. Unlike state-based data models, where updates overwrite previous data, KurrentDB retains every event in its original form. While historical events can be replayed to reconstruct the current state, the inverse is impossible with state-based systems. Once reduced to a single state, the detailed history of the events that shape the current state is irretrievably lost. The event-based model, being more raw and granular than the state-based model, offers broad and in-depth historical insights. At the same time, it can be transformed into the current state for quick and efficient analysis. ::: info Event-based models provide the best of both worlds, offering the flexibility to present data in both state and event-based data models. ::: ## KurrentDB as a Solution to State-Based Challenges *Summary of KurrentDB Solution with Event-Based Data Model:* | **Challenge Area** | **KurrentDB Solution with Event-Based Data Model** | **Example** | | --- | --- | --- | | **Data Analytics** | - Provides granular insights into behaviors, actions, and system changes over time.- Allows for time travel and temporal analysis.- Enables tracking of system evolution and predicting future outcomes. | E-commerce: Track every step of a customer’s journey, from browsing to purchasing, to analyze buying patterns. | | **Data Integration** | - Processes events incrementally, reducing computational overhead.- Handles real-time updates efficiently.- Ensures fast synchronization with other systems. | Logistics: Process each package scan in real-time, ensuring timely updates without lengthy batch processing delays. | | **Application Development** | - Decouples the current state from business logic and actions.- Simplifies scaling and maintenance.- Facilitates adding new functionality incrementally without affecting other system parts. | Healthcare: Easily update patient records across internal and external systems without tightly coupling services, improving scalability. | ### Data Analytics For data analytics, KurrentDB’s event-based model offers new possibilities by providing raw, granular insights into behaviors, actions, and system changes over time—far beyond the properties and attributes available in state-based models. For example, in an e-commerce system, analysts can track every step of a customer’s journey, from browsing products to final purchase, allowing for detailed behavioral analysis. The historical context it preserves allows for time travel and temporal analysis, enabling analysts to track a system’s evolution, better understand past behaviors, reconstruct current states, and even predict future outcomes using machine learning models. For instance, understanding user purchase patterns over time could help predict future buying trends. ### System Integration For system integration, KurrentDB offers significant advantages for real-time applications by processing events incrementally. This reduces computational overhead by handling individual changes (events) instead of processing entire datasets as in batch processing. For example, in a logistics system, only specific updates—such as a package being scanned at a checkpoint—are processed in real-time rather than batch-processing millions of scans simultaneously. This triggers quick updates and immediate action to ensure fast, seamless synchronization that keeps systems up to date without delays. ### Application Development For application development, KurrentDB’s event-based model effectively decouples the current state (e.g., a table) from the business logic and actions that manipulate it. With fewer dependencies, decoupled systems can scale horizontally more efficiently, allowing new functionality to be added incrementally without affecting other parts of the system. This leads to cleaner, more modular codebases that are less prone to breaking when changes are introduced, reducing the likelihood of bugs and issues. ## **Comparison of State-Based and Event-Based Data Models** *Comparison table that contrasts the state-based and event-based data models:* | **Aspect** | **State-Based Data Model** | **Event-Based Data Model** | | --- | --- | --- | | **Primary Focus** | Stores the current state of an object or entity. | Records and stores every event or change that occurs to an object or entity. | | **Data Representation** | Focuses on "what" the state is currently. | Focuses on "why" and "how" of the current state by tracking the sequence of changes over time as events. | | **Historical Data** | Only the latest state is stored; previous states are overwritten. | Retains the full history of events, allowing reconstruction of any past state. | | **Storage Requirements** | Efficient in terms of storage as only the most recent state is saved. | Generally requires more storage since every event is retained. | | **Transparency & Traceability** | Lacks historical context, making it difficult to audit changes or trace past states. | Provides complete transparency, enabling detailed traceability and auditing. | | **Analytics & Machine Learning** | Limited, as only the current state is available for analysis, lacking the historical context. | Ideal for advanced analytics and machine learning, as every event can be analyzed in detail. | | **Real-Time Processing** | Often relies on batch processing, leading to delays in real-time insights. | Supports real-time processing by handling and processing events as they occur. | | **Scalability** | Tends to tightly couple systems, leading to challenges in scaling and adding new features. | Loosely coupled, making systems easier to scale and extend without affecting other parts. | | **System Integration** | Typically relies on centralized, tightly coupled architectures. | Encourages decoupled, event-driven architectures, improving modularity and flexibility. | ## Summary * Data can be viewed from two perspectives: the current state, which presents the “what,“ and historical events, which present the "why" and "how" * Traditional state-based models are prevalent in relational database management systems (RDBMS) and store only the most recent data. * Despite technological advancements, state-based systems struggle with modern challenges like machine learning, real-time analytics, and scalability. * KurrentDB is a practical database that leverages an event-based storage model. Storing all historical events preserves detailed insights and enables replayability for reconstructing current (and past) states. * Event-based models can generate the current state via replay of historical events. The inverse is not possible since state-based models can not be reduced to events that were not stored. * This event-based model offers transparency, real-time processing, and loosely coupled systems while addressing key challenges in data analytics, system integration, and application development. --- --- url: 'https://docs.kurrent.io/getting-started/features.md' --- # KurrentDB Feature List ## Core Features ### Event-Native Database | Feature Name | Description | |--------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Append-only Log](./concepts.md#event-log) | A log where events are only appended and never modified, serving as an immutable audit trail that captures all historical changes within a system. | | [Immutable Event](./concepts.md#immutable-events) | All events stored in KurrentDB cannot be altered once appended, ensuring data integrity and simplifying troubleshooting. | | Guaranteed Write | Writes are guaranteed to be fully durable once acknowledged. | | [Guaranteed Consistent Event Ordering](./concepts.md#guaranteed-consistent-ordering-in-event-log-and-stream) | Events are ordered by their append time across both the event log and streams to facilitate operations where the sequence of execution is crucial. Events are guaranteed to be read in the same consistent order whenever they are consumed. | | Sequential Event Numbering | An event appended to a stream is automatically assigned a strictly monotonically increasing number (without gap) to ensure reliable state reconstruction and concurrency handling | | [Stream Indexing](@server/configuration/indexes.md) | Events in streams are indexed to provide fast access to groups of events in the event log. | | [Fine Grained Stream](./concepts.md#fine-grained-event-streams) | EventStore supports billions of streams, allowing granular event organization to efficiently track the lifecycle of every distinct entity within a system. | | [Optimistic Concurrency Control](./concepts.md#optimistic-concurrency-control) | Concurrent appends that lead to lost updates can be prevented with optimistic concurrency control. This is done in a lock-free manner, to reduce contention and performance overhead. | | [Multiple Hosting Options](https://www.kurrent.io/downloads) | KurrentDB is available fully managed with [Kurrent Cloud](/cloud/introduction.md) or self-managed on Linux, Windows, macOS, or with Docker | ### Projection | Feature Name | Description | |-------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | [By Category System Projection](@server/features/projections/system.md#by-category) | Quickly retrieve indexed events from streams that share the same category. | | [By Event Type System Projection](@server/features/projections/system.md#by-event-type) | Quickly retrieve indexed events that share the same event type. | | [By Correlation ID System Projection](@server/features/projections/system.md#by-correlation-id) | Quickly retrieve indexed events with the same correlation ID, enabling data lineage and root cause analysis. | | [User-Defined Projection](@server/features/projections/custom.md) | Allows users to define custom projections in javascript to transform or filter events to another stream or a state programmatically. | ### Subscription | Feature Name | Description | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| | [Catch-up Subscription](/clients/subscriptions.md) | Self-managed subscription of filtered events by stream or event type from a particular position or in real-time as events occur. | | [Persistent Subscription](@server/features/persistent-subscriptions.md) | Server-managed subscription that supports multiple competing consumers, checkpointing, retries, and parking (i.e., dead-lettering) | ### Connector | Feature Name | Description | |----------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Connector](@server/features/connectors/README.md) | Fully configurable integration to external systems that can push events from KurrentDB in real-time. Supports at least once delivery, retry, event filtering, event transformation, automatic checkpointing, leases, and high availability. | | [HTTP Sink](@server/features/connectors/sinks/http.md) | Publishes events from KurrentDB to an HTTP endpoint. | | [Kafka Sink](@server/features/connectors/sinks/kafka.md) | Publishes events from KurrentDB to Kafka topic or partition using a key found in the events. Supports broker acknowledgment and basic authentication. | | RabbitMQ Sink | Publishes events from KurrentDB to a RabbitMQ exchange. Supports broker acknowledgment and basic authentication over a secured connection. | | MongoDB Sink | Publishes events from KurrentDB to a MongoDB collection or document. Supports basic authentication. | ### Clustering | Feature Name | Description | |---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [High Availability Cluster](@server/configuration/cluster.md) | Ensures high availability and fault tolerance by using secure, quorum-based replication, leader election via the gossip protocol, and configurable node roles and communication, with each node independently maintaining data without shared disks. | | [Read-only Replica](@server/configuration/cluster.md) | Cluster nodes that replicate data asynchronously from the cluster but do not participate in write operations, elections, or quorum. Used primarily to scale read operations. | ### Interface and SDK | Feature Name | Description | |-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [KurrentDB Client](/clients/getting-started.md) | Client SDKs are available in Python, Java, .NET, Node.js, Go, and Rust to use and administer KurrentDB. | | gRPC API | Provides an API based on the gRPC protocol for high performance, low latency, and streaming support for all KurrentDB operations | | [HTTP API](@server/http-api/api.md) | Offers simple and basic RESTful administration of KurrentDB. | | TCP API | A deprecated API that provides low-level, high throughput TCP access to KurrentDB. Not supported for releases after 23.10. | ### User Interface | Feature Name | Description | |----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | [Admin UI](@server/features/admin-ui.md) | The web-based KurrentDB user interface that manages events, streams, server configurations, monitoring, etc. | | [Kurrent Navigator](https://navigator.kurrent.io/) | The next-generation KurrentDB user interface built as a native desktop application. | ## Security ### Authentication | Feature Name | Description | |---------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [User name/Password Authentication](@server/security/user-authentication.md#basic-authentication) | Authenticate users based on credentials provided. | | [OAuth Authentication](@server/security/user-authentication.md#oauth-authentication) | Connect to an identity server and authenticate users based on JWT | | [LDAP Authentication](@server/security/user-authentication.md#ldap-authentication) | Authenticate users against LDAP-based directory services. | | [User X.509 Certificates](@server/security/user-authentication.md#user-x509-certificates) | Support user-based X.509 certificates for authentication. | ### Authorization | Feature Name | Description | |--------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [User Management](@server/security/user-authentication.md) | Create, update, and assign users to pre-defined user groups. | | [Stream Access Control List](@server/security/user-authorization.md#access-control-lists) | Define which users or groups can read, write, or delete on a stream level. | | [Stream Access Policy](@server/security/user-authorization.md#stream-policy-authorization) | Define access policies to control who can read, write, or delete streams for multiple streams using stream rules. | ### Encryption | Feature Name | Description | |--------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Encryption at rest](@server/security/README.md#encryption-at-rest) | Secure events stored on disk using file-level encryption. | | [Encryption in transit](@server/security/protocol-security.md) | Use TLS to secure data during network transmission. | | [Kurrent Certificate Generation Tool](@server/operations/cert-update.md) | The command line interface that eases the generation of a certificate authority and node certificates for encryption in transit. | | [FIPS 140-2](@server/security/README.md#fips-140-2) | Compliance with FIPS 140-2 standards for cryptographic modules. | ## Operations ### Configuration | Feature Name | Description | |-------------------------------------------------------------------|------------------------------------------------------------------------------| | [Multiple Configuration Options](@server/configuration/README.md) | Configure KurrentDB through YAML, environment variables, or the command line | ### Data Cleanup and Housekeeping | Feature Name | Description | |--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Stream Truncate and Deletion](@server/features/streams.md#soft-delete-and-truncatebefore) | Truncate streams by max age, max count, or before a particular position or delete an entire stream. | | [Scavenging](@server/operations/scavenge.md) | Manually reclaim disk space for compliance or storage purposes by completely removing truncated and deleted streams from disk. | | [Auto-scavenging](@server/operations/auto-scavenge.md) | Automatically schedule and coordinate scavenging for a cluster in a way that minimizes performance impact. | ### Monitoring | Feature Name | Description | |---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Logging](@server/diagnostics/logs.md) | Provides detailed logs for server operations to console and log files. | | [Logs Endpoint](@server/diagnostics/logs.md#logs-download) | View or download logs over HTTP for authenticated KurrentDB users without needing file system access. | | [Metrics](@server/diagnostics/metrics.md) | Collect KurrentDB metrics such as CPU, memory, disk usage, and the status of projections, subscriptions, elections, etc. | | [Metrics with Prometheus](@server/diagnostics/metrics.md) | Allow systems to scrape metrics in Prometheus format for monitoring over an HTTP endpoint. | | [Metrics with OpenTelemetry Exporter](@server/diagnostics/integrations.md#opentelemetry-exporter) | Export and push metrics to an endpoint via the OpenTelemetry protocol. | ### Backup, Replication, and Migration | Feature Name | Description | |-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| | [Backup and Restore](https://docs.kurrent.io/server/v24.10/operations/backup.html#backup) | Provide functionality for full or differential backups and restores over disk snapshots or file copy backups. | | [Kurrent Replicator](https://replicator.eventstore.org/) | Facilitate replication or migration of data between different KurrentDB clusters or instances. | ### Administrative Tool | Feature Name | Description | |----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Kurrent CLI](/commercial-tools/cli-tool.md) | Command line tool for administrative operations on KurrentDB such as scavenge, backup, restore, merge index, delete stream, as well as user and projection management. | ## Next Steps * [KurrentDB Server Documentation](@server/quick-start/README.md): Gain a deeper understanding about other KurrentDB features. * [KurrentDB Client Documentation](/clients/appending-events.md#handling-concurrency): Explore how to use these features with KurrentDB clients. * [Kurrent Essentials](https://academy.kurrent.io/essentials): Developer greater understanding of these features in this in-depth guide. * [KurrentDB From Scratch](https://academy.kurrent.io/from-scratch): Get hands-on and build a basic KurrentDB application with this training series. * [An End-To-End Example With KurrentDB](https://www.youtube.com/watch?v=vIUw-jKpKfQ) Watch how to build an application with KurrentDB --- --- url: 'https://docs.kurrent.io/getting-started/going-further.md' --- # Going Further Now that you've completed the Getting Started Guide, consider the following paths to go further: * [Run Kurrent Cloud](https://docs.kurrent.io/cloud/introduction.html): Quickly deploy and run KurrentDB in a fully managed cloud environment. * [Install KurrentDB locally](@server/quick-start/installation.md): Install KurrentDB locally. * [KurrentDB Server Configuration Documentation](@server/configuration/README.md): Explore how to configure KurrentDB for your environment. * [KurrentDB Client SDK Documentation](@clients/grpc/getting-started.md): Understand how to interact with KurrentDB programmatically with KurrentDB clients. --- --- url: 'https://docs.kurrent.io/getting-started/introduction.md' --- # Welcome to Kurrent ## What is KurrentDB? KurrentDB is an event-native database designed specifically to store, process, and deliver application state changes, known as events. Each event captures a specific change in the state. Examples include when an order is placed, a payment is processed, or an item is shipped. By capturing all these incremental updates, KurrentDB captures temporal context and provides a complete audit trail of a business process. KurrentDB offers the following features: | Feature | Description | |---------|---------------------------------------------------------------------------------------------------------------------------| | [Append-only Event Log](./concepts.md#event-log) | A durable, sequential, and immutable log that captures events in a consistent order. | | [Streams](./concepts.md#event-stream) | Groups and indexes events to organize and speed up retrieval. | | [Subscriptions](@server/features/persistent-subscriptions.md) and [Connectors](@server/features/connectors/README.md) | Delivers events to external systems through push or pull options. | | [Projection](@server/features/projections/README.md) | Transforms and filters events into different streams or state. | | [Multiple Hosting Options](https://kurrent.io/downloads) | Fully managed with [Kurrent Cloud](/cloud/introduction.md), or self-managed on Linux, Windows, macOS, or with Docker. | | [Client SDK](@clients/grpc/getting-started.md) | Available in Python, Java, .NET, Node.js, Go, and Rust. | --- --- url: 'https://docs.kurrent.io/getting-started/kurrent-ecosystem.md' --- # The Kurrent Ecosystem The Kurrent ecosystem is made up of the core database (which can run standalone or in the cloud), client SDKs and APIs, connectors, and management user interfaces: ### KurrentDB At the heart of Kurrent’s platform is KurrentDB, the core database engine (formerly EventStoreDB). KurrentDB is an immutable, append-only event store designed for event-sourced architectures. Data is organized into streams – essentially sequences of events – and the engine supports billions of independent streams while maintaining consistent ordering of events within each stream. Every write appends a new event to a stream (no in-place updates), preserving a durable history of changes. An integrated indexing mechanism allows fast retrieval of events by stream or globally, and the database can handle real-time event ingestion at scale without compromising on write/read performance. ### Kurrent Cloud Kurrent Cloud is the fully managed cloud offering of the Kurrent platform, aimed at eliminating the operational overhead of running KurrentDB clusters. It allows one-click (or API-driven) deployment of managed KurrentDB clusters on all major cloud providers – Amazon Web Services, Microsoft Azure, and Google Cloud Platform. With Kurrent Cloud, developers can spin up a KurrentDB cluster in minutes through a web console, without having to manually provision VMs or containers. ### Kurrent Clients and APIs Kurrent offers a range of client SDKs for multiple languages and environments, including .NET (C#), Java (and other JVM languages), JavaScript/TypeScript (Node.js), Python, Rust, and Go. KurrentDB also exposes a straightforward HTTP API. This allows any application that can speak HTTP to read and write events or perform administrative operations over REST, enabling integration from scripts or systems where a custom SDK might not be available. ### Connectors Connectors make it easy to integrate data from KurrentDB into other systems. Each connector runs on the server-side and uses a catch-up subscription to receive events, filter or transform them, and push them to an external system. ### Projections Projections in KurrentDB let you continuously query event streams to detect patterns over time—ideal for scenarios like tracking sequences of medical events or analyzing user behavior in real time. ### Management Interfaces Kurrent provides both command line and graphical user interfaces for managing your KurrentDB installation. --- --- url: 'https://docs.kurrent.io/getting-started/kurrent-why.md' --- # Why Kurrent? Traditional databases often focus only on current state and overwrite data, which discards valuable information about the past and the events that shaped the current state. In contrast, KurrentDB keeps a complete history of changes, providing organizations with richer, and more contextual data that supports deeper insights - critical in today’s AI and data-driven environment. ::: details Use Case: Machine Learning A German tool manufacturer leveraged historical events from KurrentDB on manufacturing times and tool specifications (such as length and diameter) to [predict manufacturing durations for custom tools](https://www.kurrent.io/blog/from-data-to-insights-using-event-log-data-to-train-machine-learning-models). This prediction was then integrated into an online quote system, which automated the generation of instant, more accurate quotes—a significant improvement over the previous method of the sales team creating quotes based mainly on their experience. ::: KurrentDB also includes additional features that make it simple to develop event-native applications that enable real-time updates and minimize system dependencies while maintaining data consistency. ::: details Use Case: Real-time Streaming You can use KurrentDB to streamline your payment process. Holcim, a global construction material provider [replaced batch processing with real-time streaming of payment statuses](https://www.kurrent.io/case-studies/holcim) from SAP system to depots. In doing so, Holcim eliminated the previous day-long wait for payment verification and order collection. These types of significant improvements in customer service and operational efficiency can provide a competitive edge regardless of industry. ::: Finally, KurrentDB can reconstruct the current state of any object from its historical events. In doing so, KurrentDB provides the current and historical context that allows organizations to clearly understand the "what" and "why" (and "when") within a single system. ## Who Uses KurrentDB? Data engineers can provide context-rich events from KurrentDB to data pipelines to analyze historical and behavioral trends, uncovering patterns that traditional databases often miss. These patterns can, for example, reveal why a customer churned or highlight behaviors that lead to high-value contracts. Application developers can leverage KurrentDB's granular events to build real-time, distributed enterprise applications and break down tightly coupled systems. Events are typically simpler, self-contained, and independent. Unlike the tables and rows in traditional databases that are often interdependent with various functions. ::: details Use Case: Modernize Legacy Systems Insureon, an independent marketplace for online delivery of small business insurance, used KurrentDB to [modernize its legacy monolithic system](https://www.kurrent.io/case-studies/insureon), which was brittle and challenging to deploy. With KurrentDB, Insureon could provide new solutions faster without sacrificing scalability. This dramatically improved the company's business creativity and agility. ::: ## Where to Use KurrentDB? KurrentDB is valuable for critical enterprise applications and data pipelines with behavior-rich data, where interdependent business objects and processes interact in diverse ways over time. Industries such as finance, healthcare, supply chain, and manufacturing benefit from KurrentDB, as it supports flexible, scalable solutions with a complete audit trail. This enables businesses to maintain clear, comprehensive system oversight, which is essential for meeting compliance requirements and adapting to changing needs. --- --- url: 'https://docs.kurrent.io/getting-started/mcp.md' --- # MCP Servers [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers let your coding assistant — Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, and others — talk directly to KurrentDB. With an MCP server connected, the agent can read and write events, explore streams, and build, test, and deploy projections without leaving the editor. Kurrent maintains two MCP servers, each tuned for a different workflow. ## Available servers | Server | Repository | Best for | What it does | |--------|------------|----------|--------------| | **Kurrent MCP Server** | [`kurrent-io/mcp-server`](https://github.com/kurrent-io/mcp-server) | Exploring data and prototyping projections from a chat-style agent | Reads and writes events on streams, lists streams, and builds, creates, updates, tests, and inspects projections | | **Gaffer MCP Server** | [gaffer.kurrent.io](https://gaffer.kurrent.io/) | Local projection development, debugging, and testing | The `gaffer mcp` server exposes Gaffer's projection lifecycle — scaffold, validate, run, debug, and inspect state — to any MCP-aware assistant | ## Kurrent MCP Server The [Kurrent MCP Server](https://github.com/kurrent-io/mcp-server) gives an MCP-compatible client access to a running KurrentDB instance through eight tools. ### Available tools | Category | Tool | Purpose | |----------|------|---------| | Streams | `read_stream` | Read events from a stream (forwards or backwards, with a limit) | | Streams | `write_events_to_stream` | Append a new event with data, event type, and metadata | | Streams | `list_streams` | List streams via the `$streams` system stream | | Projections | `build_projection` | Generate projection code from a natural-language description | | Projections | `create_projection` | Create a projection in KurrentDB from provided code | | Projections | `update_projection` | Update an existing projection's code | | Projections | `test_projection` | Write sample events to verify projection behavior | | Projections | `get_projections_status` | Retrieve status and statistics for a projection | ### Prerequisites The server uses the `$streams` system stream and projections, so the KurrentDB instance must be started with: ```sh --run-projections=all --start-standard-projections ``` You also need [`uv`](https://docs.astral.sh/uv/) on the machine where the server runs (`brew install uv` on macOS) and the Python dependencies from the repository's `requirements.txt`. ### Configure your client Replace `path to mcp-server folder` with the cloned repository path and `insert kurrentdb connection here` with your `kurrentdb://` (or `esdb://`) connection string. #### VS Code (`.vscode/mcp.json`) ```json { "servers": { "KurrentDB": { "type": "stdio", "command": "uv", "args": [ "--directory", "path to mcp-server folder", "run", "server.py" ], "env": { "KURRENTDB_CONNECTION_STRING": "insert kurrentdb connection here" } } } } ``` #### Claude Desktop ```json { "mcpServers": { "KurrentDB": { "type": "stdio", "command": "uv", "args": [ "--directory", "path to mcp-server folder", "run", "server.py" ], "env": { "KURRENTDB_CONNECTION_STRING": "insert kurrentdb connection here" } } } } ``` See the [Claude Desktop MCP quickstart](https://modelcontextprotocol.io/quickstart/user) for how to load this configuration. #### Cursor or Windsurf Cursor reads `.cursor/mcp.json`; Windsurf reads `.codeium/windsurf/mcp_config.json`. ```json { "mcpServers": { "kurrentdb": { "command": "python", "args": ["path to mcp-server folder/server.py"], "env": { "KURRENTDB_CONNECTION_STRING": "insert kurrentdb connection here" } } } } ``` Access control is enforced by the credentials embedded in `KURRENTDB_CONNECTION_STRING`, so scope that user to whatever the agent should be able to do. ## Gaffer MCP Server [Gaffer](https://gaffer.kurrent.io/) is the developer toolkit for KurrentDB projections — local run, debug, and test loops for the same JavaScript projection engine that ships inside KurrentDB. The `gaffer mcp` server exposes that projection lifecycle and debugging surface to any MCP-aware assistant (Claude Code, Cursor, Claude Desktop, VS Code, and others), so an agent can scaffold, validate, run, and inspect projections without leaving the editor. Gaffer has its own documentation site. Rather than duplicate it here, follow it directly: * **MCP server setup** (per-client config for VS Code, Claude Code, Cursor, Claude Desktop): [gaffer.kurrent.io/cli/mcp](https://gaffer.kurrent.io/cli/mcp/) * **Install** (CLI and VS Code extension): [gaffer.kurrent.io/getting-started/install](https://gaffer.kurrent.io/getting-started/install/) * **Testing projections** with `@kurrent/projections-testing`: [gaffer.kurrent.io/testing/nodejs](https://gaffer.kurrent.io/testing/nodejs/) ## Picking a server * **Working from a chat-style agent (Claude Desktop, Claude Code) and want it to inspect or mutate a KurrentDB instance directly** → use the [Kurrent MCP Server](https://github.com/kurrent-io/mcp-server). * **Authoring, debugging, or testing projections locally** → use [Gaffer](https://gaffer.kurrent.io/) and its `gaffer mcp` server. Both can be active at the same time — the Kurrent MCP Server talks to a running KurrentDB, while Gaffer drives the local projection runtime. --- --- url: 'https://docs.kurrent.io/getting-started/quickstart/index.md' --- # KurrentDB Quickstart This quickstart will guide you through getting started with KurrentDB using GitHub Codespaces. ::: info GitHub Codespaces provides an instant and preconfigured development environment in your browser for this quickstart. To learn more about Github Codespaces, [click here](https://github.com/features/codespaces). ::: ## Objectives In this quickstart, you will: * Start a KurrentDB server using Docker in GitHub Codespaces. * Append an event to KurrentDB with sample code. * View the appended event using the Admin UI. * Read the appended event with sample code using the KurrentDB client. ## Prerequisites Before starting, ensure you have the following: * A GitHub account to use GitHub Codespaces. * Basic knowledge of one of the development languages/platforms below. * Familiarity with command-line operations. ::: tip If you have trouble with this quickstart, you can find more help in the ["KurrentDB From Scratch" tutorial series on Kurrent Academy](https://academy.kurrent.io/from-scratch). ::: ## Step 1: Set up Your Codespace 1. Choose one of the development languages/platforms below and click the Codespaces link: ::: tabs#dev-language-platform @tab Select > @tab Python |Quickstart Language/Platform|Link to Codespaces| |-----|--------| ||[![Open in Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/?repo=793881789\&devcontainer_path=.devcontainer%2Fquickstart%2Fdevcontainer.json)| @tab Java |Quickstart Language/Platform|Link to Codespaces| |-----|--------| ||[![Open in Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/?repo=793886190\&devcontainer_path=.devcontainer%2Fquickstart%2Fdevcontainer.json)| @tab .NET |Quickstart Language/Platform|Link to Codespaces| |-----|--------| ||[![Open in Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/?repo=790993560\&devcontainer_path=.devcontainer%2Fquickstart%2Fdevcontainer.json)| @tab node.js |Quickstart Language/Platform|Link to Codespaces| |-----|--------| ||[![Open in Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/?repo=793877904\&devcontainer_path=.devcontainer%2Fquickstart%2Fdevcontainer.json)| ::: 2. Log in to GitHub if required. 3. Follow the instructions to create a new Codespace. 4. Wait for your Codespace to build. This can take up to a few minutes. 5. Once complete, you will see a welcome message in the terminal: ``` Hello!👋 Welcome to the EventStoreDB Getting Started Quickstart Guide. ``` ::: tip For this quickstart, you can safely ignore and close any Codespaces notifications that appear on the bottom right of the page. ::: ## Step 2: Start the KurrentDB Server 1. Once your Codespace is loaded, run the following command in the terminal to start the KurrentDB server: ```sh ./start_db.sh ``` This is a custom script written for this quickstart to help start KurrentDB in Docker. 2. You will see the following message printed in the terminal: ``` 🚀 EventStoreDB Server has started!! 🚀 URL to the EventStoreDB Admin UI 👉: https://XXXXXXXXX.XXX ``` ::::: details (Optional) Learn more about how to start KurrentDB in Docker and the `start_db.sh` script #### Understanding How to Start KurrentDB Server in Docker and How `start_db.sh` Works `start_db.sh` is a custom script written for the quickstart that will: * Check if Docker is currently running locally * Pull the KurrentDB server Docker container * Start the server in Docker * Print the KurrentDB Admin UI URL in the terminal To see how to start the KurrentDB server in Docker, follow these steps 1. Run the following command to open `start_db.sh`: ```sh code ./start_db.sh ``` ::: tip Alternatively, you can locate and open the file from the EXPLORER window found on the left side of Codespaces. You can find the path to the file in the command above. ::: 2. In step 3 of the script, review how KurrentDB is started with the `docker run` command: ```bash docker run # Start a new Docker container using the 'docker run' command -d \ # Run the container in detached mode (in the background) --name esdb-node \ # Assign the container a name ('esdb-node' in this case) -p 2113:2113 \ # Map port 2113 on the host to port 2113 in the Docker container. Required for the KurrentDB eventstore/eventstore:24.10.1 \ # Specify the Docker image to use, in this case, the latest supported version of KurrentDB --insecure \ # Run KurrentDB in insecure mode, without authentication and SSL/TLS security (usually for development) --run-projections=All \ # Enable all projections in KurrentDB, including system and user projections --enable-atom-pub-over-http # Enable the AtomPub API over HTTP. Required for the KurrentDB Admin UI ``` 3. Review other parts of the script if you wish. 4. Feel free to modify and re-run the script in your Codespace. ::: info For more information about other KurrentDB parameters and settings, [click here](@server/configuration/README.md). ::: :::: info To view the source code on Github, click the link below: ::: tabs#dev-language-platform @tab Select > @tab Python [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Python-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Python) @tab Java [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Java-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Java) @tab .NET [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20.NET-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Dotnet) @tab node.js [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Node.js-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Nodejs) ::: :::: ::::: ## Step 3: Navigate to the KurrentDB Admin UI 1. In Codespaces, copy the URL to KurrentDB Admin UI printed in the terminal from the last step. 2. Open a new browser tab. 3. In the address bar of the new tab, paste the URL and navigate to it. 4. This will display the KurrentDB Admin UI. 5. Keep the Admin UI open for the next steps. !\[KurrentDB Admin UI Dashboard]\(images/hello-world/admin-ui.png =300x) ## Step 4: Install Required Package for Sample Code 1. In Codespace, run the following command to install the KurrentDB client package. This will be used in the sample codes: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh pip install -r requirements.txt ``` @tab Java ```sh mvn package ``` @tab .NET This step is not required for .NET. Please continue to the next step. @tab node.js ```sh yarn install ``` ::: ::::: details (Optional) Learn more about the KurrentDB client packages #### Understanding Required Packages for KurrentDB Development The KurrentDB client packages enable your code to connect to the database, append events, and read events from streams in the language/platform of your choice. To understand what packages are installed, follow these steps: 1. Run the following command to examine package dependencies: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh code ./requirements.txt ``` @tab Java ```sh code ./pom.xml ``` @tab .NET ```sh code ./SampleAppend/SampleAppend.csproj ``` @tab node.js ```sh code ./package.json ``` ::: ::: tip Alternatively, you can locate and open the file from the EXPLORER window on the left of Codespaces. You can find the path to the file in the command above. ::: 2. Review the KurrentDB client packages listed as dependencies: ::: tabs#dev-language-platform @tab Select > @tab Python ``` esdbclient==1.0.19 ``` @tab Java ```xml com.eventstore db-client-java 5.3.2 ``` For the most recent version of the KurrentDB client package, see [Maven Central](https://mvnrepository.com/artifact/io.kurrent/kurrentdb-client). @tab .NET ```xml ``` @tab node.js ```json "dependencies": { "@eventstore/db-client": "^6.1.0" } ``` ::: ::: note The version of the KurrentDB client above may be outdated. For more information about the client, [click here](@clients/grpc/getting-started.md). ::: :::: info To view the source code on GitHub, click the link below: ::: tabs#dev-language-platform @tab Select > @tab Python [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Python-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Python) @tab Java [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Java-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Java) @tab .NET [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20.NET-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Dotnet) @tab node.js [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Node.js-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Nodejs) ::: :::: ::::: ## Step 5: Append an Event to KurrentDB 1. In Codespaces, run this command to execute the sample. This appends an event to KurrentDB: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh python3 sample_append.py ``` @tab Java ```sh java -cp \ target/eventstoredb-demo-1.0-SNAPSHOT-jar-with-dependencies.jar \ com.eventstoredb_demo.SampleWrite ``` @tab .NET ```sh dotnet run --project SampleAppend/ ``` @tab node.js ```sh node sample_append.js ``` ::: 2. You should see a confirmation for the event append, similar to this: ``` ************************ 🎉 Congratulations, you have written an event! Stream: SampleStream Event Type: SampleEventType Event Body: {"id":"1","importantData":"some value"} ************************" ``` ::::: details (Optional) Learn more about the sample code #### Understanding How the Append Sample Works To help you understand how events are appended to KurrentDB programmatically, you will explore the sample code used in this step. The sample code demonstrates: * **Establishing a Connection**: Connect to KurrentDB using the client library. * **Creating an Event**: Create a new event with a specific type and data payload. * **Appending the Event to a Stream**: Append the new event to a specific stream. To see how this works, follow these steps: 1. Run the following command to open the sample code: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh code ./sample_append.py ``` @tab Java ```sh code ./src/main/java/com/eventstoredb_demo/SampleWrite.java ``` @tab .NET ```sh code ./SampleAppend/Program.cs ``` @tab node.js ```sh code ./sample_append.js ``` ::: ::: tip Alternatively, you can locate and open the file from the EXPLORER window on the left of Codespaces. You can find the path to the file in the command above. ::: 2. In step 1 of the code, review how the client connects to KurrentDB: ::: tabs#dev-language-platform @tab Select > @tab Python ```python # Create an instance of EventStoreDBClient, connecting to the KurrentDB at localhost without TLS client = EventStoreDBClient(uri="esdb://localhost:2113?tls=false") ``` @tab Java ```java // configure the settings to connect to KurrentDB locally without TLS EventStoreDBClientSettings settings = EventStoreDBConnectionString.parseOrThrow("esdb://localhost:2113?tls=false"); // apply the settings and create an instance of the client EventStoreDBClient client = EventStoreDBClient.create(settings); ``` @tab .NET ```c# // Create an instance of EventStoreClientSettings, connecting to the KurrentDB at localhost without TLS var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); await using var client = new EventStoreClient(settings); ``` @tab node.js ```js // Create an instance of EventStoreDBClient, connecting to the KurrentDB at localhost without TLS const client = EventStoreDBClient.connectionString("esdb://localhost:2113?tls=false"); ``` ::: 1. In step 2 of the code, review how a new event is initiated: ::: tabs#dev-language-platform @tab Select > @tab Python ```python event_type = "SampleEventType" # Define the event type for the new event new_event = NewEvent( # Create a new event with a type and body type=event_type, # Specify the event type data=b'{"id":"1", "importantData":"some value"}' # Specify the event data body as a JSON in byte format ) ``` @tab Java ```java // Build the KurrentDB event data structure String eventType = "SampleEventType"; // Define the name of the event type for the new event byte[] eventBody = "{\"id\":\"1\", \"importantData\":\"some value\"}" // Define the body of the event in a UTF8 encoded byte array .getBytes(StandardCharsets.UTF_8); EventData eventData = EventData.builderAsJson(eventType, eventBody) // Create the new event object with the type and body .build(); ``` @tab .NET ```c# var eventType = "SampleEventType"; // Define the event type for the new event var eventData = new EventData( // Create a new event with a type and body Uuid.NewUuid(), // Specify a new UUID for the event eventType, // Specify the event type @"{""id"": ""1"", ""importantData"": ""some value""}"u8 // Specify the event data body as JSON encoded with UTF8 .ToArray() // Convert the body into a byte array ); ``` @tab node.js ```js const eventType = "SampleEventType" // Define the event type for the new event const event = jsonEvent({ // Create a new event with a type and body type: eventType, // Specify the event type data: { // "id":"1", "importantData":"some value" // Specify the event data body }, }); ``` ::: 4. In step 3 of the code, review how the client appends the new event to KurrentDB: ::: tabs#dev-language-platform @tab Select > @tab Python ```python event_stream = "SampleStream" # Define the stream name where the event will be appended client.append_to_stream( # Append the event to a stream event_stream, # Name of the stream to append the event to events=[new_event], # The event to append (in a list) current_version=StreamState.ANY # Set to append regardless of the current stream state (you can ignore this for now) ) ``` @tab Java ```java // Set append option to append regardless of what revision the stream is at (i.e. disable concurrency check) AppendToStreamOptions options = AppendToStreamOptions.get().expectedRevision(ExpectedRevision.any()); // Append event to stream String eventStream = "SampleStream"; client.appendToStream(eventStream, options, eventData).get(); ``` @tab .NET ```c# var eventStream = "SampleStream"; // Define the stream name where the event will be appended await client.AppendToStreamAsync( // Append the event to a stream eventStream, // Name of the stream to append the event to StreamState.Any, // Set to append regardless of the current stream state (you can ignore this for now) [eventData] // The event to append (in a list) ); ``` @tab node.js ```js const eventStream = "SampleStream"; await client.appendToStream(eventStream, event); // append the event to the stream ``` ::: 5. Feel free to modify and re-run the sample in your Codespace. ::: info To learn more about other KurrentDB client functions, [click here](@clients/grpc/getting-started.md). ::: :::: info To view the source code on GitHub, click the link below: ::: tabs#dev-language-platform @tab Select > @tab Python [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Python-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Python) @tab Java [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Java-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Java) @tab .NET [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20.NET-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Dotnet) @tab node.js [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Node.js-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Nodejs) ::: :::: ::::: ## Step 6: Verify the Appended Event in the Admin UI 1. In the Admin UI, click the `Stream Browser` link from the top navigation bar. 2. Under `Recently Changed Streams`, click the `SampleStream` link. 3. Click on the `JSON` link in the rightmost column of the table. 4. You should see the content of the appended event. ## Step 7: Read the Event from KurrentDB Programmatically 1. In Codespaces, run this command to execute the sample. This reads the event from KurrentDB: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh python3 sample_read.py ``` @tab Java ```sh java -cp \ target/eventstoredb-demo-1.0-SNAPSHOT-jar-with-dependencies.jar \ com.eventstoredb_demo.SampleRead ``` @tab .NET ```sh dotnet run --project SampleRead/ ``` @tab node.js ```sh node sample_read.js ``` ::: 2. You should see the content of the appended event similar to this: ``` ************************ You have read an event! Stream: SampleStream Event Type: SampleEventType Event Body: {"id":"1","importantData":"some value"} ************************" ``` ::::: details (Optional) Learn more about the sample code #### Understanding How the Read Sample Works You will explore the sample code used in this step to gain a deeper understanding of how events are read from KurrentDB programmatically. The sample code demonstrates the following: * **Establishing a Connection**: Illustrates how to connect to KurrentDB using the client library. * **Reading Events from a Stream**: Reads events from a specific stream. * **Processing Retrieved Events**: Iterates over the events retrieved from the stream. * **Deserializing Event Data**: Extracts and deserializes the event data from the retrieved events. * **Displaying Event Information**: Prints out the event details—such as the stream name, event type, and event body—to the console. 1. Run the following command to open the sample code: ::: tabs#dev-language-platform @tab Select > @tab Python ```sh code ./sample_read.py ``` @tab Java ```sh code ./src/main/java/com/eventstoredb_demo/SampleRead.java ``` @tab .NET ```sh code ./SampleRead/Program.cs ``` @tab node.js ```sh code ./sample_read.js ``` ::: ::: tip Alternatively, you can locate and open the file from the EXPLORER window on the left of Codespaces. You can find the path to the file in the command above. ::: 2. In step 1 of the code, review how the client connects to KurrentDB: ::: tabs#dev-language-platform @tab Select > @tab Python ```python # Create an instance of EventStoreDBClient, connecting to the EventStoreDB at localhost without TLS client = EventStoreDBClient(uri="esdb://localhost:2113?tls=false") ``` @tab Java ```java // configure the settings to connect to EventStoreDB locally without TLS EventStoreDBClientSettings settings = EventStoreDBConnectionString. parseOrThrow("esdb://localhost:2113?tls=false"); // apply the settings and create an instance of the client EventStoreDBClient client = EventStoreDBClient.create(settings); ``` @tab .NET ```c# // Create an instance of EventStoreClientSettings, connecting to the KurrentDB at localhost without TLS var settings = EventStoreClientSettings.Create("esdb://localhost:2113?tls=false"); await using var client = new EventStoreClient(settings); ``` @tab node.js ```js // Create an instance of KurrentDBClient, connecting to the KurrentDB at localhost without TLS const client = EventStoreDBClient.connectionString("esdb://localhost:2113?tls=false"); ``` ::: 3. In step 2 of the code, review how the client reads all the events from the stream: ::: tabs#dev-language-platform @tab Select > @tab Python ```python events = client.get_stream("SampleStream") # Read all events from SampleStream ``` @tab Java ```java ReadStreamOptions options = ReadStreamOptions.get() // Create a read option for client to read events .forwards() // Client should read events forward in time .fromStart() // Client should read from the start of stream .maxCount(10); // Client should read at most 10 events // get events from stream String eventStream = "SampleStream"; ReadResult result = client.readStream(eventStream, options).get(); ``` @tab .NET ```c# var events = client.ReadStreamAsync( // Read events from stream Direction.Forwards, // Read events forward in time "SampleStream", // Name of stream to read from StreamPosition.Start // Read from the start of the stream ); ``` @tab node.js ```js // Read events from the SampleStream const stream_name = "SampleStream"; // Define the name of the stream to read from let events = client.readStream( // Read events from stream stream_name, // Specify the stream name { // fromRevision: START, // Read from the start of the stream direction: FORWARDS, // Read events forward in time maxCount: 20 // Read at most 20 events } ); ``` ::: 4. In step 3 of the code, review how the events are deserialized and printed: ::: tabs#dev-language-platform @tab Select > @tab Python ```python for event in events: # For each event print("************************"); # print("You have read an event!"); # print("Stream: " + event.stream_name); # Print the stream name of the event print("Event Type: " + event.type); # Print the type of the event print("Event Body: " + event.data.decode()); # Print the body of the event after converting it to string from a byte array print("************************"); ``` @tab Java ```java for (ResolvedEvent resolvedEvent : result.getEvents()) { // For each event in stream RecordedEvent recordedEvent = resolvedEvent.getOriginalEvent(); // Get the original event (can ignore for now) // System.out.println("************************"); // System.out.println("You have read an event!"); // System.out.println("Stream: " + recordedEvent.getStreamId()); // Print the stream name of the event System.out.println("Event Type: " + recordedEvent.getEventType()); // Print the type of the event System.out.println("Event Body: " + new String(recordedEvent.getEventData(), // Print the body of the event after converting it from a byte array StandardCharsets.UTF_8)); // UTF8 is used to convert byte array to string System.out.println("************************"); } ``` @tab .NET ```c# await foreach (var evt in events) { // For each event in stream Console.WriteLine("************************"); // Console.WriteLine("You have read an event!"); // Console.WriteLine("Stream: " + evt.Event.EventStreamId); // Print the stream name of the event Console.WriteLine("Event Type: " + evt.Event.EventType); // Print the type of the event Console.WriteLine("Event Body: " + Encoding.UTF8.GetString( // Print the body of the event. convert the byte array to string evt.Event.Data.ToArray())); // Gets the event body as a byte array Console.WriteLine("************************"); } ``` @tab node.js ```js for await (const resolvedEvent of events) { // For each event found in SampleStream console.log("************************"); // console.log("You have read an event!"); // console.log("Stream: " + resolvedEvent.event?.streamId); // Print the stream name of the event console.log("Event Type: " + resolvedEvent.event?.type); // Print the type of the event console.log("Event Body: " + JSON.stringify(resolvedEvent.event?.data)); // Print the body of the event as a string console.log("************************"); } ``` ::: 5. Feel free to modify and re-run the sample in your Codespace. ::: info To learn more about other KurrentDB client functions, [click here](@clients/grpc/getting-started.md). ::: :::: info To view the source code on Github, click the link below: ::: tabs#dev-language-platform @tab Select > @tab Python [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Python-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Python) @tab Java [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Java-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Java) @tab .NET [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20.NET-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Dotnet) @tab node.js [![](https://img.shields.io/badge/GitHub-EventStoreDB%20From%20Scratch%20Node.js-blue?logo=github)](https://github.com/kurrent-io/EventStoreDB-From-Scratch-Nodejs) ::: :::: ::::: ## Summary In this quickstart, you: 1. Started the KurrentDB server. 2. Navigated to the Admin UI. 3. Appended an event to KurrentDB. 4. Verified the event in the Admin UI. 5. Read the event from KurrentDB programmatically. Feel free to experiment further by appending more events, reading them, or exploring the advanced features of KurrentDB. --- --- url: >- https://docs.kurrent.io/samples/clients/dotnet/legacy/v23.3/secure-with-tls/index.md --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: 'https://docs.kurrent.io/samples/clients/dotnet/v1.0/secure-with-tls/index.md' --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: 'https://docs.kurrent.io/samples/clients/dotnet/v1.1/secure-with-tls/index.md' --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: 'https://docs.kurrent.io/samples/clients/dotnet/v1.2/secure-with-tls/index.md' --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: 'https://docs.kurrent.io/samples/clients/dotnet/v1.3/secure-with-tls/index.md' --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: 'https://docs.kurrent.io/samples/clients/dotnet/v1.4/secure-with-tls/index.md' --- # Secure EventStoreDB with TLS * [Secure EventStoreDB with TLS](#secure-eventstoredb-with-tls) * [Overview](#overview) * [Certificates](#certificates) * [Description](#description) * [Running Sample](#running-sample) * [1. Generate self-signed certificates](#1-generate-self-signed-certificates) * [2. Run samples with Docker](#2-run-samples-with-docker) * [3. Run samples locally (without Docker)](#3-run-samples-locally-without-docker) * [3.1 Install certificate - Linux (Ubuntu, Debian, WSL) or MacOS](#31-install-certificate---linux-ubuntu-debian) * [3.2 Install certificate - Windows](#32-install-certificate---windows) * [3.3 Run EventStoreDB node](#33-run-eventstoredb-node) * [3.3 Run client application](#33-run-client-application) ## Overview The sample shows how to run the .NET client secured by TLS certificates. Read more in the docs: * [Security](https://developers.eventstore.com/server/v20/server/security/) * [Running EventStoreDB with `docker-compose`](https://developers.eventstore.com/server/v20/server/installation/docker.html#use-docker-compose) * [Event Store Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) It is essential for production use to configure EventStoreDB security features to prevent unauthorised access to your data. EventStoreDB supports gRPC with TLS and SSL. Each protocol has its security configuration, but you can only use one set of certificates for TLS and HTTPS. ### Certificates The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](https://github.com/EventStore/es-gencert-cli), which also has instructions on generating and installing the certificates and configure EventStoreDB nodes to use them. You need to generate CA (certificate authority) `./es-gencert-cli create-ca -out ./es-ca` And certificate for each node in your cluster. `./es-gencert-cli-cli create-node -ca-certificate ./es-ca/ca.crt -ca-key ./es-ca/ca.key -out ./node -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,eventstoredb` The client application should have public CA certificate installed (***Note:*** private keys should not be shared to clients). While generating the certificate, you need to remember to pass: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` or * DNS names to `-dns-names`: e.g. `localhost,eventstoredb` that will match the URLs that you will be accessing EventStoreDB nodes. The [Certificate Generation CLI](https://github.com/EventStore/es-gencert-cli) is also available as the Docker image. Check the [docker-compose.certs.yml](./docker-compose.certs.yml) See instruction how to install certificates [below](#3-run-run-samples-locally-without-docker). You can find helpers scripts that are also installing created CA on local machine: * Linux (Debian based, MacOS and WSL) - [create-certs.sh](./create-certs.sh), * Windows - [create-certs.ps1](./create-certs.ps1) ## Description The sample shows how to connect with the client and append new event. You can run it locally or through docker configuration. Suggested order of reading: * The full code is located in [Program.cs](./Program.cs) file * [Dockerfile](./Dockerfile) - for building the sample image * [docker-compose.yml](./docker-compose.yml) - for running a single EventStoreDB node. * [docker-compose.app.yml](./docker-compose.app.yml) - for running the sample client app. * [docker-compose.certs.yml](./docker-compose.certs.yml) - for generating certificates. ## Running Sample ### 1. Generate self-signed certificates Use following command to generate and install certificates: * Linux/MacOS ```console ./create-certs.sh ``` * Windows ```powershell .\create-certs.ps1 ``` *Note: to regenerate certificates you need to remove the [./certs](./certs) folder.* ### 2. Run samples with Docker The following command will run both server and client with preconfigured TLS connection setup. ```console docker-compose -f docker-compose.yml -f docker-compose.app.yml up ``` ### 3. Run samples locally (without Docker) Assuming the certificates were generated and installed. #### 3.1 Run EventStoreDB Use the following command to run EventStoreDB ```console docker-compose up -d ``` #### 3.2 Run client application Run the application from your favourite IDE or the console: ```console dotnet run ./secure-with-tls.csproj ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node clusters * Back up and restore clusters * Perform rolling upgrades and update configurations ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v25.x * v24.x * v23.x ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/getting-started/installation.md --- This section covers the various aspects of installing the Operator. ::: important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: * A Kubernetes cluster running `v1.23.1` or later. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * kubectl [install guide](https://kubernetes.io/docs/tasks/tools/install-kubectl) * k9s [install guide](https://k9scli.io/topics/install/) * Helm 3 CLI [install guide](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ## Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ## Install Custom Resource Definitions (CRDs) The Operator uses Custom Resource Definitions (CRDs) to extend Kubernetes. You can install them automatically with Helm or manually. The following resource types are supported: * [KurrentDB](resource-types.md#kurrentdb) * [KurrentDBBackup](resource-types.md#kurrentdbbackup) Since CRDs are managed globally by Kubernetes, special care must be taken to install them. ### Automatic Install It's recommended to install and manage the CRDs using Helm. See [Deployment Modes](#deployment-modes) for more information. ### Manual Install If you prefer to install CRDs yourself: ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.0.0 --untar # Install the CRDs (real license data is not needed at this step) helm template -s 'templates/crds/*.yaml' kurrentdb-operator \ --set operator.license.file= --set operator.license.key= \ | kubectl apply -f - ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created ``` After installing CRDs manually, you should include the `--set crds.enabled=false` flag for the `helm install` command, and include one of `--set crds.enabled=false`, `--reuse-values`, or `--reset-then-reuse-values` for the `helm upgrade` command. ::: caution If `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ## Deployment Modes The Operator can be scoped to track Kurrent resources across **all** or **specific** namespaces. ### Cluster-wide In cluster-wide mode, the Operator tracks Kurrent resources across **all** namespaces and requires `ClusterRole`. Helm creates the `ClusterRole` automatically. To deploy the Operator in this mode, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.0.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Deploys the Operator into the `kurrent` namespace (use `--create-namespace` to create it). Feel free to modify this namespace. * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Applies the Operator license * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. ### Specific Namespace(s) In this mode, the Operator will track Kurrent resources across **specific** namespaces. This mode reduces the level of permissions required. The Operator will create a `Role` in each namespace that it is expected to manage. To deploy the Operator in this mode, the following command can be used: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.0.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic \ --set operator.namespaces='{kurrent, foo}' ``` Here's what the command does: * Sets the namespace of where the Operator will be deployed i.e. `kurrent` (feel free to change this) * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Configures the Operator license * Sets the underlying Operator configuration to target the namespaces: `kurrent` and `foo` * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the command (unless you are using the Operator to target the namespace that it will be deployed in to). ::: *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. #### Augmenting Namespaces The Operator deployment can be updated to adjust which namespaces are watched. For example, in addition to the `kurrent` and `foo` namespaces (from the example above), a new namespace `bar` may also be watched using the command below: ```bash helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.0.0 \ --namespace kurrent \ --reuse-values \ --set operator.namespaces='{kurrent,foo,bar}' ``` This will trigger: * a new `Role` to be created in the `bar` namespace * a rolling restart of the Operator to pick up the new configuration changes ## Deployment Validation Using the k9s tool, navigate to the namespace listing using the command `:namespaces`. It should show the namespace where the Operator was deployed: ![Namespaces](images/install/namespace-list.png) After stepping in to the `kurrent` namespace, type `:deployments` in the k9s console. It should show the following: ![Operator Deployment](images/install/deployments-list.png) Pods may also be viewed using the `:pods` command, for example: ![Operator Pod](images/install/pods-list.png) Pressing the `Return` key on the selected Operator pod will allow you to drill through the container hosted in the pod, and then finally to the logs: ![Operator Logs](images/install/logs.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/getting-started/resource-types.md --- # Supported Resource Types The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` ## KurrentDB This resource type is used to define a database deployment. ### API | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster (1 or 3) | | `image` *string* | Yes | KurrentDB container image URL | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDbNetwork](#kurrentdbnetwork)* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDbSecurity](#kurrentdbsecurity)* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core)* | No | A secret that contains the Enterprise license for the database | #### KurrentDbNetwork | Field | Required | Description | |------------------------------------------------------------------|----------|--------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDbLoadBalancer](#kurrentdbloadbalancer)* | Yes | Defines a load balancer to use with the database | #### KurrentDbLoadBalancer | Field | Required | Description | |---------------------|----------|----------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | #### KurrentDbSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateSubjectName` *string* | No | Subject name used in the TLS certificate (this maps directly to the database property `CertificateSubjectName`) | | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API | Field | Required | Description | |------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If this is not specified, the leader will be picked as the source. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/database-backup.md --- # Database Backup The sections below details how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Kubernetes cluster already has a volume snapshot class configured. * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been deployed that requires backing up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Viewing Backups Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-backup/namespace-list.png) From here, press the `Return` key on the namespace where the `KurrentDBBackup` was created, in the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbbackups` and press the `Return` key. The following screen will show a list of database backups for the selected namespace. ![Backup Listing](images/database-backup/backup-list.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/database-deployment.md --- # Database Deployment The sections below detail the different deployment options for KurrentDB. For detailed information on the various properties, visit the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) section. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before deploying a `KurrentDB` cluster, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using certificates from LetsEncrypt * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using LetsEncrypt certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` ## Three Node Secure Cluster (using LetsEncrypt) Using LetsEncrypt, or any publicly trusted certificate, in an operator-managed KurrentDB cluster is not supported in v1.0.0; please upgrade to v1.4.0. ## Viewing Deployments Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-deployment/namespace-list.png) From here, press the `Return` key on the namespace where `KurrentDB` was deployed. In the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbs` and press the `Return` key. The following screen will show a list of deployed databases for the selected namespace, as shown below: ![Databases](images/database-deployment/database-list.png) Summary information is shown on this screen. For more information press the `d` key on the selected database. The following screen will provide additional information about the deployment: ![Database Details](images/database-deployment/db-decribe.png) Scrolling further will also show the events related to the deployment, such as: * transitions between states * gossip endpoint * leader details * database version ## Accessing Deployments ### External The Operator will create services of type `LoadBalancer` to access a KurrentDB cluster (for each node) when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. Note, these values will be passed through as-is. For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` ## Updating Deployments `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/database-restore.md --- # Database Restore The sections below detail how a database restore can be performed. Refer to the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been backed up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.0.0/operations/operator-upgrade.md --- # Operator Upgrade The sections below detail how to upgrade the Operator. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before upgrading the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The [Helm 3 CLI](https://helm.sh/docs/intro/install/) tool is installed and configured to interact with your Kubernetes cluster. ## Helm The Operator can be upgraded using the command below: ```bash helm repo update helm upgrade kurrentdb-operator kurrentdb-operator-repo/kurrentdb-operator \ --version {version} \ --namespace kurrent \ --reset-then-reuse-values ``` Here's what the command does: * Refreshes the local Helm repository index * Defines where Operator is installed i.e. `kurrent` (feel free to change this) * Define the upgrade `{version}` version e.g. 1.1.0 * Performs the upgrade (leverages existing release values) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/getting-started/index.md --- # Getting Started *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node clusters * Back up and restore clusters * Perform rolling upgrades and update configurations ### New in 1.1.0 * Deploy Read-only Replica nodes into your KurrentDB cluster. See the [example](../operations/database-deployment.md#three-node-insecure-cluster-with-two-read-only-replicas), and [reference](./resource-types.md#kurrentdbreadonlyreplicasspec) * Configure arbitrary scheduling constraints on your KurrentDB pods. See the [example](../operations/database-deployment.md#deploying-with-scheduling-constraints), and [reference](./resource-types.md#kurrentdbconstraints) ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v25.x * v24.x * v23.x ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/getting-started/installation.md --- This section covers the various aspects of installing the Operator. ::: important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: * A Kubernetes cluster running `v1.23.1` or later. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * kubectl [install guide](https://kubernetes.io/docs/tasks/tools/install-kubectl) * k9s [install guide](https://k9scli.io/topics/install/) * Helm 3 CLI [install guide](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ## Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ## Install Custom Resource Definitions (CRDs) The Operator uses Custom Resource Definitions (CRDs) to extend Kubernetes. You can install them automatically with Helm or manually. The following resource types are supported: * [KurrentDB](resource-types.md#kurrentdb) * [KurrentDBBackup](resource-types.md#kurrentdbbackup) Since CRDs are managed globally by Kubernetes, special care must be taken to install them. ### Automatic Install It's recommended to install and manage the CRDs using Helm. See [Deployment Modes](#deployment-modes) for more information. ### Manual Install If you prefer to install CRDs yourself: ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.1.0 --untar # Install the CRDs helm template -s 'templates/crds/*.yaml' kurrentdb-operator | kubectl apply -f - ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created ``` After installing CRDs manually, you should include the `--set crds.enabled=false` flag for the `helm install` command, and include one of `--set crds.enabled=false`, `--reuse-values`, or `--reset-then-reuse-values` for the `helm upgrade` command. ::: caution If `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ## Deployment Modes The Operator can be scoped to track Kurrent resources across **all** or **specific** namespaces. ### Cluster-wide In cluster-wide mode, the Operator tracks Kurrent resources across **all** namespaces and requires `ClusterRole`. Helm creates the `ClusterRole` automatically. To deploy the Operator in this mode, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.1.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Deploys the Operator into the `kurrent` namespace (use `--create-namespace` to create it). Feel free to modify this namespace. * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Applies the Operator license * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. ### Specific Namespace(s) In this mode, the Operator will track Kurrent resources across **specific** namespaces. This mode reduces the level of permissions required. The Operator will create a `Role` in each namespace that it is expected to manage. To deploy the Operator in this mode, the following command can be used: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.1.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic \ --set operator.namespaces='{kurrent, foo}' ``` Here's what the command does: * Sets the namespace of where the Operator will be deployed i.e. `kurrent` (feel free to change this) * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Configures the Operator license * Sets the underlying Operator configuration to target the namespaces: `kurrent` and `foo` * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the command (unless you are using the Operator to target the namespace that it will be deployed in to). ::: *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. #### Augmenting Namespaces The Operator deployment can be updated to adjust which namespaces are watched. For example, in addition to the `kurrent` and `foo` namespaces (from the example above), a new namespace `bar` may also be watched using the command below: ```bash helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.1.0 \ --namespace kurrent \ --reuse-values \ --set operator.namespaces='{kurrent,foo,bar}' ``` This will trigger: * a new `Role` to be created in the `bar` namespace * a rolling restart of the Operator to pick up the new configuration changes ## Deployment Validation Using the k9s tool, navigate to the namespace listing using the command `:namespaces`. It should show the namespace where the Operator was deployed: ![Namespaces](images/install/namespace-list.png) After stepping in to the `kurrent` namespace, type `:deployments` in the k9s console. It should show the following: ![Operator Deployment](images/install/deployments-list.png) Pods may also be viewed using the `:pods` command, for example: ![Operator Pod](images/install/pods-list.png) Pressing the `Return` key on the selected Operator pod will allow you to drill through the container hosted in the pod, and then finally to the logs: ![Operator Logs](images/install/logs.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/getting-started/resource-types.md --- # Supported Resource Types The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` ## KurrentDB This resource type is used to define a database deployment. ### API | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster (1 or 3) | | `image` *string* | Yes | KurrentDB container image URL | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork](#kurrentdbnetwork)* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity](#kurrentdbsecurity)* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core)* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | | `readOnlyReplias` *[KurrentDBReadOnlyReplicasSpec](#kurrentdbreadonlyreplicasspec)* | No | Read-only replica configuration the Kurrent DB Cluster. | #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | No | Number of read-only replicas in the cluster. Defaults to zero. | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | #### KurrentDBConstraints | Field | Required | Description | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the Kurrent DB may consider during scheduling. | | `affinity` *[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core)* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the Kurrent DB pod. | | `tolerations` *list of [Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core)* | No | The tolerations for scheduling the Kurrent DB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core)* | No | The topology spread constraints for scheduling the Kurrent DB pod. | #### KurrentDBNetwork | Field | Required | Description | |------------------------------------------------------------------|----------|----------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer](#kurrentdbloadbalancer)* | Yes | Defines a load balancer to use with the database | #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateSubjectName` *string* | No | Subject name used in the TLS certificate (this maps directly to the database property `CertificateSubjectName`) | | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API | Field | Required | Description | |------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If this is not specified, the leader will be picked as the source. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/index.md' --- # Operations A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/database-backup.md --- # Database Backup The sections below details how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Kubernetes cluster already has a volume snapshot class configured. * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been deployed that requires backing up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Viewing Backups Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-backup/namespace-list.png) From here, press the `Return` key on the namespace where the `KurrentDBBackup` was created, in the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbbackups` and press the `Return` key. The following screen will show a list of database backups for the selected namespace. ![Backup Listing](images/database-backup/backup-list.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/database-deployment.md --- # Database Deployment The sections below detail the different deployment options for KurrentDB. For detailed information on the various properties, visit the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) section. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before deploying a `KurrentDB` cluster, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster with Two Read-Only Replicas Note that read-only replicas are only supported by KurrentDB in clustered configurations, that is, with multiple primary nodes. The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per primary node, but read-only replicas will have 2gb of memory * 512mb of storage will be allocated for the data disk per node * The main KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` * The read-only replicas that are provisioned will be exposed as `kurrentdb-replica-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true readOnlyReplicas: replicas: 2 resources: requests: cpu: 1000m memory: 1Gi ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using certificates from LetsEncrypt * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using LetsEncrypt certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` ## Three Node Secure Cluster (using LetsEncrypt) Using LetsEncrypt, or any publicly trusted certificate, in an operator-managed KurrentDB cluster is not supported in v1.0.0; please upgrade to v1.4.0. ## Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) * [Affinity and Anti-Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) * [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) * [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) * [Node Name](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename) For example, the following KurrentDB resource would schedule KurrentDB pods onto nodes labeled with `machine-size:large`, preferring to spread the replicas each in their own availability zone: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: my-kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true nodeSelector: machine-size: large topologySpreadConstraints: maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: my-kurrentdb-cluster whenUnsatisfiable: DoNotSchedule ``` If no scheduling constraints are configured, the operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. ## Viewing Deployments Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-deployment/namespace-list.png) From here, press the `Return` key on the namespace where `KurrentDB` was deployed. In the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbs` and press the `Return` key. The following screen will show a list of deployed databases for the selected namespace, as shown below: ![Databases](images/database-deployment/database-list.png) Summary information is shown on this screen. For more information press the `d` key on the selected database. The following screen will provide additional information about the deployment: ![Database Details](images/database-deployment/db-decribe.png) Scrolling further will also show the events related to the deployment, such as: * transitions between states * gossip endpoint * leader details * database version ## Accessing Deployments ### External The Operator will create services of type `LoadBalancer` to access a KurrentDB cluster (for each node) when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. Note, these values will be passed through as-is. For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` ## Updating Deployments `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/database-restore.md --- # Database Restore The sections below detail how a database restore can be performed. Refer to the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been backed up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.1.0/operations/operator-upgrade.md --- # Operator Upgrade The sections below detail how to upgrade the Operator. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before upgrading the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The [Helm 3 CLI](https://helm.sh/docs/intro/install/) tool is installed and configured to interact with your Kubernetes cluster. ## Helm The Operator can be upgraded using the command below: ```bash helm repo update helm upgrade kurrentdb-operator kurrentdb-operator-repo/kurrentdb-operator \ --version {version} \ --namespace kurrent \ --reset-then-reuse-values ``` Here's what the command does: * Refreshes the local Helm repository index * Defines where Operator is installed i.e. `kurrent` (feel free to change this) * Define the upgrade `{version}` version e.g. 1.1.0 * Performs the upgrade (leverages existing release values) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node clusters * Back up and restore clusters * Perform rolling upgrades and update configurations ### New in 1.2.0 * Configure the advertised FQDN with a template. See the [docs](resource-types.md#kurrentdbnetwork). ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v25.x * v24.x * v23.x ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/getting-started/installation.md --- This section covers the various aspects of installing the Operator. ::: important The Operator is an Enterprise only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: * A Kubernetes cluster running `v1.23.1` or later. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * kubectl [install guide](https://kubernetes.io/docs/tasks/tools/install-kubectl) * k9s [install guide](https://k9scli.io/topics/install/) * Helm 3 CLI [install guide](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ## Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ## Install Custom Resource Definitions (CRDs) The Operator uses Custom Resource Definitions (CRDs) to extend Kubernetes. You can install them automatically with Helm or manually. The following resource types are supported: * [KurrentDB](resource-types.md#kurrentdb) * [KurrentDBBackup](resource-types.md#kurrentdbbackup) Since CRDs are managed globally by Kubernetes, special care must be taken to install them. ### Automatic Install It's recommended to install and manage the CRDs using Helm. See [Deployment Modes](#deployment-modes) for more information. ### Manual Install If you prefer to install CRDs yourself: ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.2.0 --untar # Install the CRDs helm template -s 'templates/crds/*.yaml' kurrentdb-operator | kubectl apply -f - ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created ``` After installing CRDs manually, you should include the `--set crds.enabled=false` flag for the `helm install` command, and include one of `--set crds.enabled=false`, `--reuse-values`, or `--reset-then-reuse-values` for the `helm upgrade` command. ::: caution If `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, and all KurrentDB and KurrentDBBackup resources will be deleted as well! ::: ## Deployment Modes The Operator can be scoped to track Kurrent resources across **all** or **specific** namespaces. ### Cluster-wide In cluster-wide mode, the Operator tracks Kurrent resources across **all** namespaces and requires `ClusterRole`. Helm creates the `ClusterRole` automatically. To deploy the Operator in this mode, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.2.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Deploys the Operator into the `kurrent` namespace (use `--create-namespace` to create it). Feel free to modify this namespace. * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Applies the Operator license * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. ### Specific Namespace(s) In this mode, the Operator will track Kurrent resources across **specific** namespaces. This mode reduces the level of permissions required. The Operator will create a `Role` in each namespace that it is expected to manage. To deploy the Operator in this mode, the following command can be used: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.2.0 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic \ --set operator.namespaces='{kurrent, foo}' ``` Here's what the command does: * Sets the namespace of where the Operator will be deployed i.e. `kurrent` (feel free to change this) * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Configures the Operator license * Sets the underlying Operator configuration to target the namespaces: `kurrent` and `foo` * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the command (unless you are using the Operator to target the namespace that it will be deployed in to). ::: *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. #### Augmenting Namespaces The Operator deployment can be updated to adjust which namespaces are watched. For example, in addition to the `kurrent` and `foo` namespaces (from the example above), a new namespace `bar` may also be watched using the command below: ```bash helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.2.0 \ --namespace kurrent \ --reuse-values \ --set operator.namespaces='{kurrent,foo,bar}' ``` This will trigger: * a new `Role` to be created in the `bar` namespace * a rolling restart of the Operator to pick up the new configuration changes ## Deployment Validation Using the k9s tool, navigate to the namespace listing using the command `:namespaces`. It should show the namespace where the Operator was deployed: ![Namespaces](images/install/namespace-list.png) After stepping in to the `kurrent` namespace, type `:deployments` in the k9s console. It should show the following: ![Operator Deployment](images/install/deployments-list.png) Pods may also be viewed using the `:pods` command, for example: ![Operator Pod](images/install/pods-list.png) Pressing the `Return` key on the selected Operator pod will allow you to drill through the container hosted in the pod, and then finally to the logs: ![Operator Logs](images/install/logs.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/getting-started/resource-types.md --- # Supported Resource Types The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` ## KurrentDB This resource type is used to define a database deployment. ### API | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster (1 or 3) | | `image` *string* | Yes | KurrentDB container image URL | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork](#kurrentdbnetwork)* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity](#kurrentdbsecurity)* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core)* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | | `readOnlyReplias` *[KurrentDBReadOnlyReplicasSpec](#kurrentdbreadonlyreplicasspec)* | No | Read-only replica configuration the Kurrent DB Cluster. | #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | No | Number of read-only replicas in the cluster. Defaults to zero. | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | #### KurrentDBConstraints | Field | Required | Description | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the Kurrent DB may consider during scheduling. | | `affinity` *[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core)* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the Kurrent DB pod. | | `tolerations` *list of [Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core)* | No | The tolerations for scheduling the Kurrent DB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core)* | No | The topology spread constraints for scheduling the Kurrent DB pod. | #### KurrentDBNetwork | Field | Required | Description | |------------------------------------------------------------------|----------|----------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer](#kurrentdbloadbalancer)* | Yes | Defines a load balancer to use with the database | | `fqdnTemplate` *string* | No | The template string used to define the external advertised address of a node | Note that `fqdnTemplate` supports the following expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{podName}` expands to the name of the pod * `{nodeTypeSuffix}` expands to `""` for a primary node or `"-replica"` for a replica node When `fqdnTemplate` is empty, it defaults to `{podName}.{name}{nodeTypeSuffix}.{domain}`. #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateSubjectName` *string* | No | Subject name used in the TLS certificate (this maps directly to the database property `CertificateSubjectName`) | | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API | Field | Required | Description | |------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If this is not specified, the leader will be picked as the source. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/database-backup.md --- # Database Backup The sections below details how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Kubernetes cluster already has a volume snapshot class configured. * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been deployed that requires backing up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Viewing Backups Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-backup/namespace-list.png) From here, press the `Return` key on the namespace where the `KurrentDBBackup` was created, in the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbbackups` and press the `Return` key. The following screen will show a list of database backups for the selected namespace. ![Backup Listing](images/database-backup/backup-list.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/database-deployment.md --- # Database Deployment The sections below detail the different deployment options for KurrentDB. For detailed information on the various properties, visit the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) section. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before deploying a `KurrentDB` cluster, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster with Two Read-Only Replicas Note that read-only replicas are only supported by KurrentDB in clustered configurations, that is, with multiple primary nodes. The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per primary node, but read-only replicas will have 2gb of memory * 512mb of storage will be allocated for the data disk per node * The main KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` * The read-only replicas that are provisioned will be exposed as `kurrentdb-replica-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true readOnlyReplicas: replicas: 2 resources: requests: cpu: 1000m memory: 1Gi ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using certificates from LetsEncrypt * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using LetsEncrypt certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` ## Three Node Secure Cluster (using LetsEncrypt) Using LetsEncrypt, or any publicly trusted certificate, in an operator-managed KurrentDB cluster is not supported in v1.0.0; please upgrade to v1.4.0. ## Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) * [Affinity and Anti-Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) * [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) * [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) * [Node Name](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename) For example, the following KurrentDB resource would schedule KurrentDB pods onto nodes labeled with `machine-size:large`, preferring to spread the replicas each in their own availability zone: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: my-kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true nodeSelector: machine-size: large topologySpreadConstraints: maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: my-kurrentdb-cluster whenUnsatisfiable: DoNotSchedule ``` If no scheduling constraints are configured, the operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. ## Viewing Deployments Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-deployment/namespace-list.png) From here, press the `Return` key on the namespace where `KurrentDB` was deployed. In the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbs` and press the `Return` key. The following screen will show a list of deployed databases for the selected namespace, as shown below: ![Databases](images/database-deployment/database-list.png) Summary information is shown on this screen. For more information press the `d` key on the selected database. The following screen will provide additional information about the deployment: ![Database Details](images/database-deployment/db-decribe.png) Scrolling further will also show the events related to the deployment, such as: * transitions between states * gossip endpoint * leader details * database version ## Accessing Deployments ### External The Operator will create services of type `LoadBalancer` to access a KurrentDB cluster (for each node) when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. Note, these values will be passed through as-is. For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` ## Updating Deployments `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/database-restore.md --- # Database Restore The sections below detail how a database restore can be performed. Refer to the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) for detailed information. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before installing and executing the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * A `KurrentDB` has already been backed up. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Picking certificate names Each node in each KurrentDB cluster you create will advertise a fully-qualified domain name (FQDN). Clients will expect those advertised names to match the names you configure on your TLS certificates. You will need to understand how the FQDN is calculated for each node in order to request a TLS certificate that is valid for each node of your kurrentdb cluster. By default, the [network.fqdnTemplate field of your KurrentDB spec](../getting-started/resource-types.md#kurrentdbnetwork) is `{podName}.{name}{nodeTypeSuffix}.{domain}`, which may require multiple wildcard names on your certificate, like both `*.myName.myDomain.com` and `*.myName-replica.myDomain.com`. You may prefer to instead configure an `fqdnTemplate` like `{podName}.{domain}`, which could be covered by a single wildcard: `*.myDomain.com`. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.2.0/operations/operator-upgrade.md --- # Operator Upgrade The sections below detail how to upgrade the Operator. ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: Before upgrading the Operator, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The [Helm 3 CLI](https://helm.sh/docs/intro/install/) tool is installed and configured to interact with your Kubernetes cluster. ## Helm The Operator can be upgraded using the command below: ```bash helm repo update helm upgrade kurrentdb-operator kurrentdb-operator-repo/kurrentdb-operator \ --version {version} \ --namespace kurrent \ --reset-then-reuse-values ``` Here's what the command does: * Refreshes the local Helm repository index * Defines where Operator is installed i.e. `kurrent` (feel free to change this) * Define the upgrade `{version}` version e.g. 1.2.0 * Performs the upgrade (leverages existing release values) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node clusters * Back up and restore clusters * Perform rolling upgrades and update configurations ### New in 1.3.1 * Fix/improve support for resizing KurrentDB clusters, including explicitly handling data safety, minimizing downtime, and allowing the user to cancel a resize operation that is not progressing. See [Updating Replica Count](../operations/modify-deployments.md#updating-replica-count) for details. * Support for custom labels and annotations on all child resources (StatefulSets, Pods, LoadBalancers, etc). * Allow users to use public certificate authorities like LetsEncrypt without having to manually pass the publicly trusted cert in a secret. * Allow manual overrides to the generated ConfigMap that is passed to KurrentDB. Previously, if a user manually altered the ConfigMap it would get immediately overwritten, whereas now it will "stick" until the next time the KurrentDB resource is updated. * Fix a bug affecting the KurrentDBBackup behavior when cluster's fqdnTemplate met certain criteria. * Fix and clarified the `credentialsSecretName` behavior in the helm chart. It is not normally required at all, but in previous versions, it was generating warning events with the default configuration. * Add a new `crds.keep` value to the helm chart. With the default value of `true`, CRDs installed by the helm chart will not be deleted by helm, which offers a layer of protection against accidental data loss. In earlier versions of the helm chart, or with `crds.keep=false`, a transition from `crds.enabled=true` to `crds.enabled=false` would cause the deletion of the CRDs and all KurrentDB and KurrentDBBackup objects across the cluster. ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v25.x * v24.x * v23.x ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/getting-started/installation.md --- This section covers the various aspects of installing the Operator. ::: important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Prerequisites ::: tip To get the best out of this guide, a basic understanding of [Kubernetes concepts](https://kubernetes.io/docs/concepts/) is essential. ::: * A Kubernetes cluster running any [non-EOL version of Kubernetes](https://kubernetes.io/releases/). * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) * [Helm 3 CLI](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ## Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ## Install Custom Resource Definitions (CRDs) The Operator uses Custom Resource Definitions (CRDs) to extend Kubernetes. You can install them automatically with Helm or manually. The following resource types are supported: * [KurrentDB](resource-types.md#kurrentdb) * [KurrentDBBackup](resource-types.md#kurrentdbbackup) Since CRDs are managed globally by Kubernetes, special care must be taken to install them. ### Automatic Install It's recommended to install and manage the CRDs using Helm. See [Deployment Modes](#deployment-modes) for more information. ### Manual Install If you prefer to install CRDs yourself: ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.3.1 --untar # Install the CRDs kubectl apply -f kurrentdb-operator/templates/crds ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created ``` After installing CRDs manually, you should include the `--set crds.enabled=false` flag for the `helm install` command, and include one of `--set crds.enabled=false`, `--reuse-values`, or `--reset-then-reuse-values` for the `helm upgrade` command. ::: caution If you set the value of `crds.keep` to `false` (the default is `true`), helm upgrades and rollbacks can result in data loss. If `crds.keep` is `false` and `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ## Deployment Modes The Operator can be scoped to track Kurrent resources across **all** or **specific** namespaces. ### Cluster-wide In cluster-wide mode, the Operator tracks Kurrent resources across **all** namespaces and requires `ClusterRole`. Helm creates the `ClusterRole` automatically. To deploy the Operator in this mode, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.3.1 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Deploys the Operator into the `kurrent` namespace (use `--create-namespace` to create it). Feel free to modify this namespace. * Creates the namespace (if it already exists, leave out the `--create-namespace` flag). * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`). * Applies the Operator license. * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. ### Specific Namespace(s) In this mode, the Operator will track Kurrent resources across **specific** namespaces. This mode reduces the level of permissions required. The Operator will create a `Role` in each namespace that it is expected to manage. To deploy the Operator in this mode, the following command can be used: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.3.1 \ --namespace kurrent \ --create-namespace \ --set crds.enabled=true \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic \ --set operator.namespaces='{kurrent, foo}' ``` Here's what the command does: * Sets the namespace of where the Operator will be deployed i.e. `kurrent` (feel free to change this) * Creates the namespace (if it already exists, leave out the `--create-namespace` flag) * Deploys CRDs (this can be skipped by changing to `--set crds.enabled=false`) * Configures the Operator license * Configures the Operator to operate on resources the namespaces `kurrent` and `foo` * Deploys a new Helm release called `kurrentdb-operator` in the `kurrent` namespace ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the command. ::: *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent STATUS: deployed REVISION: 1 TEST SUITE: None ``` Once installed, navigate to the [deployment validation](#deployment-validation) section. #### Augmenting Namespaces The Operator deployment can be updated to adjust which namespaces are watched. For example, in addition to the `kurrent` and `foo` namespaces (from the example above), a new namespace `bar` may also be watched using the command below: ```bash helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.3.1 \ --namespace kurrent \ --reuse-values \ --set operator.namespaces='{kurrent,foo,bar}' ``` This will trigger: * a new `Role` to be created in the `bar` namespace * a rolling restart of the Operator to pick up the new configuration changes ## Deployment Validation Using the k9s tool, navigate to the namespace listing using the command `:namespaces`. It should show the namespace where the Operator was deployed: ![Namespaces](images/install/namespace-list.png) After stepping in to the `kurrent` namespace, type `:deployments` in the k9s console. It should show the following: ![Operator Deployment](images/install/deployments-list.png) Pods may also be viewed using the `:pods` command, for example: ![Operator Pod](images/install/pods-list.png) Pressing the `Return` key on the selected Operator pod will allow you to drill through the container hosted in the pod, and then finally to the logs: ![Operator Logs](images/install/logs.png) ## Upgrading an Installation The Operator can be upgraded using the following `helm` commands: ```bash helm repo update helm upgrade kurrentdb-operator kurrentdb-operator-repo/kurrentdb-operator \ --namespace kurrent \ --version {version} \ --reset-then-reuse-values ``` Here's what these commands do: * Refresh the local Helm repository index * Locate an existing operator installation in namespace `kurrent` * Select the target upgrade version `{version}` e.g. `1.3.1` * Perform the upgrade, preserving values that were set during installation --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/getting-started/resource-types.md --- # Supported Resource Types The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` ## KurrentDB This resource type is used to define a database deployment. ### API | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster (1 or 3) | | `image` *string* | Yes | KurrentDB container image URL | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork](#kurrentdbnetwork)* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database, see [below](#configuring-kurrent-db) | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity](#kurrentdbsecurity)* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core)* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | | `readOnlyReplias` *[KurrentDBReadOnlyReplicasSpec](#kurrentdbreadonlyreplicasspec)* | No | Read-only replica configuration the Kurrent DB Cluster. | | `extraMetadata` *[KurrentDBExtraMetadataSpec](#kurrentdbextrametadataspec)* | No | Additional annotations and labels for child resources. | #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |---------------------------------------------------------------------------------------------------------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | No | Number of read-only replicas in the cluster. Defaults to zero. | | `resources` *[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core)* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core)* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints](#kurrentdbconstraints)* | No | Scheduling constraints for the Kurrent DB pod. | #### KurrentDBConstraints | Field | Required | Description | |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the Kurrent DB may consider during scheduling. | | `affinity` *[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core)* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the Kurrent DB pod. | | `tolerations` *list of [Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core)* | No | The tolerations for scheduling the Kurrent DB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core)* | No | The topology spread constraints for scheduling the Kurrent DB pod. | #### KurrentDBExtraMetadataSpec | Field | Required | Description | |------------------------------------------------------------------|----------|---------------------------------------------------------------------| | All *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for all child resource types. | | ConfigMaps *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for ConfigMaps. | | StatefulSets *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for StatefulSets. | | Pods *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for Pods. | | PersistentVolumeClaims *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for PersistentVolumeClaims. | | HeadlessServices *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for the per-cluster headless Services. | | HeadlessPodServices *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for the per-pod headless Services. | | LoadBalancers *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for LoadBalancer-type Services. | #### ExtraMetadataSpec | Field | Required | Description |-----------------------|-----------|-----------------------------------| | Labels *object* | No | Extra labels for a resource. | | Annotations *object* | No | Extra annotations for a resource. | #### KurrentDBNetwork | Field | Required | Description | |------------------------------------------------------------------|----------|----------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer](#kurrentdbloadbalancer)* | Yes | Defines a load balancer to use with the database | | `fqdnTemplate` *string* | No | The template string used to define the external advertised address of a node | Note that `fqdnTemplate` supports the following expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{podName}` expands to the name of the pod * `{nodeTypeSuffix}` expands to `""` for a primary node or `"-replica"` for a replica node When `fqdnTemplate` is empty, it defaults to `{podName}.{name}{nodeTypeSuffix}.{domain}`. #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate. | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use. | | `certificateSubjectName` *string* | No | Deprecated field. The value of this field is always ignored. | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API | Field | Required | Description | |-----------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If this is not specified, the leader will be picked as the source. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | | `extraMetadata` *[KurrentDBBackupExtraMetadataSpec](#kurrentdbbackupextrametadataspec)* | No | Additional annotations and labels for child resources. | #### KurrentDBBackupExtraMetadataSpec | Field | Required | Description | |------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------| | All *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for all child resource types (currently only VolumeSnapshots). | | VolumeSnapshots *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for VolumeSnapshots. | ## Configuring Kurrent DB The [`KurrentDB.spec.configuration` yaml field](#kurrentdb) may contain any valid configuration values for Kurrent DB. However, some values may be unnecessary, as the Operator provides some defaults, while other values may be ignored, as the Operator may override them. The Operator-defined default configuration values, which may be overridden by the user's `KurrentDB.spec.configuration` are: | Default Field | Default Value | |------------------------------|---------------| | DisableLogFile | true | | EnableAtomPubOverHTTP | true | | Insecure | false | | PrepareTimeoutMs | 3000 | | CommitTimeoutMs | 3000 | | GossipIntervalMs | 2000 | | GossipTimeoutMs | 5000 | | LeaderElectionTimeoutMs | 2000 | | ReplicationHeartbeatInterval | 1000 | | ReplicationHeartbeatTimeout | 2500 | | NodeHeartbeatInterval | 1000 | | NodeHeartbeatTimeout | 2500 | The Operator-managed configuration values, which take precedence over the user's `KurrentDB.spec.configuration`, are: | Managed Field | Value | |------------------------------| -------------------------------------------------------------| | Db | hard-coded volume mount point | | Index | hard-coded volume mount point | | Log | hard-coded volume mount point | | Insecure | true if `KurrentDB.spec.security.certificateSecret` is empty | | DiscoverViaDns | false (`GossipSeed` is used instead) | | AllowAnonymousEndpointAccess | true | | AllowUnknownOptions | true | | NodeIp | 0.0.0.0 (to accept traffic from outside pod) | | ReplicationIp | 0.0.0.0 (to accept traffic from outside pod) | | NodeHostAdvertiseAs | Derived from pod name | | ReplicationHostAdvertiseAs | Derived from pod name | | AdveritseHostToClientAs | Derived from `KurrentDB.spec.newtork.fqdnTemplate` | | ClusterSize | Derived from `KurrentDB.spec.replicas` | | GossipSeed | Derived from pod list | | ReadOnlyReplica | Automatically set for ReadOnlyReplica pods | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/database-backup.md --- # Database Backup The sections below detail how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. The example above can be deployed using the following steps: * Copy the YAML snippet above to a file called `backup.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f backup.yaml ``` Once deployed, navigate to the [Viewing Backups](#viewing-backups) section. ## Viewing Backups Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-backup/namespace-list.png) From here, press the `Return` key on the namespace where the `KurrentDBBackup` was created, in the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbbackups` and press the `Return` key. The following screen will show a list of database backups for the selected namespace. ![Backup Listing](images/database-backup/backup-list.png) ## Periodic Backups You can use [Kubernetes CronJobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) for basic periodic backup functionality. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/database-deployment.md --- # Database Deployment The sections below detail the different deployment options for KurrentDB. For detailed information on the various properties, visit the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) section. ## Prerequisites Before deploying a `KurrentDB` cluster, the following requirements should be met: * The Operator has been installed as per the [Installation](../getting-started/installation.md) section. * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) :::important With the examples listed in this guide, the Operator is assumed to have been deployed such that it can track the `kurrent` namespace for deployments. ::: ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Insecure Cluster with Two Read-Only Replicas Note that read-only replicas are only supported by KurrentDB in clustered configurations, that is, with multiple primary nodes. The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per primary node, but read-only replicas will have 2gb of memory * 512mb of storage will be allocated for the data disk per node * The main KurrentDB instances that are provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` * The read-only replicas that are provisioned will be exposed as `kurrentdb-replica-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true readOnlyReplicas: replicas: 2 resources: requests: cpu: 1000m memory: 1Gi ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using self-signed certificates * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) per node * 1gb of memory will be used per node * 512mb of storage will be allocated for the data disk per node * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using Self-Signed certificates](managing-certificates.md#using-self-signed-certificates) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` Once deployed, navigate to the [Viewing Deployments](#viewing-deployments) section. ## Single Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is enabled using certificates from LetsEncrypt * KurrentDB version 25.0.0 will be used * 1vcpu will be requested as the minimum (upper bound is unlimited) * 1gb of memory will be used * 512mb of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-cluster-0.kurrentdb-cluster.kurrent.test` ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster.kurrent.test' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true security: certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, ensure that the steps described in section [Using LetsEncrypt certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt) have been followed. Follow these steps to deploy the cluster: * Copy the YAML snippet above to a file called `cluster.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f cluster.yaml ``` ## Three Node Secure Cluster (using LetsEncrypt) Using LetsEncrypt, or any publicly trusted certificate, in an operator-managed KurrentDB cluster is not supported in v1.0.0; please upgrade to v1.4.0. ## Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) * [Affinity and Anti-Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) * [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) * [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) * [Node Name](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename) For example, in cloud deployments, you may want to maximize uptime by asking each replica of a KurrentDB cluster to be deployed in a different availability zone. The following KurrentDB resource does that, and also requires KurrentDB to schedule pods onto nodes labeled with `machine-size:large`: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: my-kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true constraints: nodeSelector: machine-size: large topologySpreadConstraints: maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: my-kurrentdb-cluster whenUnsatisfiable: DoNotSchedule ``` If no scheduling constraints are configured, the operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. ## Viewing Deployments Using the k9s tool, navigate to the namespaces list using the command `:namespaces`, it should show a screen similar to: ![Namespaces](images/database-deployment/namespace-list.png) From here, press the `Return` key on the namespace where `KurrentDB` was deployed. In the screen above the namespace is `kurrent`. Now enter the k9s command `:kurrentdbs` and press the `Return` key. The following screen will show a list of deployed databases for the selected namespace, as shown below: ![Databases](images/database-deployment/database-list.png) Summary information is shown on this screen. For more information press the `d` key on the selected database. The following screen will provide additional information about the deployment: ![Database Details](images/database-deployment/db-decribe.png) Scrolling further will also show the events related to the deployment, such as: * transitions between states * gossip endpoint * leader details * database version ## Accessing Deployments ### External The Operator will create services of type `LoadBalancer` to access a KurrentDB cluster (for each node) when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. Note, these values will be passed through as-is. For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/database-restore.md --- # Database Restore The sections below detail how a database restore can be performed. Refer to the [KurrentDB API](../getting-started/resource-types.md#kurrentdb) for detailed information. ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrentdb-cluster.kurrent.test loadBalancer: enabled: true ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Picking certificate names Each node in each KurrentDB cluster you create will advertise a fully-qualified domain name (FQDN). Clients will expect those advertised names to match the names you configure on your TLS certificates. You will need to understand how the FQDN is calculated for each node in order to request a TLS certificate that is valid for each node of your kurrentdb cluster. By default, the [network.fqdnTemplate field of your KurrentDB spec](../getting-started/resource-types.md#kurrentdbnetwork) is `{podName}.{name}{nodeTypeSuffix}.{domain}`, which may require multiple wildcard names on your certificate, like both `*.myName.myDomain.com` and `*.myName-replica.myDomain.com`. You may prefer to instead configure an `fqdnTemplate` like `{podName}.{domain}`, which could be covered by a single wildcard: `*.myDomain.com`. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.3.1/operations/modify-deployments.md --- # Modify Deployments Updating KurrentDB deployments through the Operator is done by modifying the KurrentDB Custom Resources (CRs) using standard Kubernetes tools. Most updates are processed almost immediately, but there is special logic in place around resizing the number of replicas in a cluster. ## Applying Updates `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` Note that the actual count of replicas in a cluster may take time to update. See [Updating Replica Count](#updating-replica-count), below. ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` ## Updating Primary Replica Count A user configures the KurrentDB cluster by setting the `.spec.replicas` setting of a KurrentDB resource. The current actual number of replicas can be observed as `.status.replicas`. The process to grow or shrink the replicas in a cluster safely requires carefully stepping the KurrentDB cluster through a series of consensus states, which the Operator handles automatically. In both cases, if the resizing flow gets stuck for some reason, you can cancel the resize by setting `.spec.replicas` back to its original value. ### Upsizing a KurrentDB Cluster The steps that the Operator takes to go from 1 to 3 nodes in a KurrentDB cluster are: * Take a VolumeSnapshot of pod 0 (the initial pod). * Reconfigure pod 0 to expect a three-node cluster. * Start a new pod 1 from the VolumeSnapshot. * Wait for pod 0 and pod 1 to establish quorum. * Start a new pod 2 from the VolumeSnapshot. Note that the database cannot process writes between the time that the Operator reconfigures pod 0 for a three-node cluster and when pod 0 and pod 1 establish quorum. The purpose of the VolumeSnapshot is to greatly reduce the amount of replication pod 1 must do from pod 0 before quorum is established, which greatly reduce the amount of downtime during the resize. ### Downsizing a KurrentDB Cluster The steps that the Operator takes to go from 3 nodes to 1 in a KurrentDB cluster are: * Make sure pod 0 and pod 1 are caught up with the leader (which may be one of them). * Stop pod 2. * Wait for quorum to be re-established between pods 0 and 1. * Stop pod 1. * Reconfigure pod 0 as a one-node cluster. Note that the database cannot process writes briefly after the Operator stops pod 2, and again briefly after the Operator reconfigures pod 0. :::important It is technically possible for data loss to occur when the Operator stops pod 2 if there are active writes against the database, and either of the other two pods happen to fail at approximately the same time pod 2 stops. The frequency of an environment failure should hopefully be low enough that this is not a realistic concern. However, to reduce the risk to truly zero, you must ensure that there are no writes against the database at the time when you downsize your cluster. ::: ## Updating Read-Only Replica Count Since Read-Only Replica nodes are not electable as leaders, it is simpler to increase or decrease the number of running read-only replicas. Still, when adding new read-only replicas, the Operator uses VolumeSnapshots to expedite the initial catch-up reads for new read-only replicas. The steps that the Operator takes to increase the number of read-only replicas are: * Take a VolumeSnapshot of a primary node. * Start new read-only replica node(s) based on that snapshot. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node clusters * Back up and restore clusters * Automate backups with a schedule and retention policies * Perform rolling upgrades and update configurations ### New in 1.4.0 * Support configurable traffic strategies for each of server-server and client-server traffic. This enables the use of LetsEncrypt certificates without creating Ingresses, for example. See [Traffic Strategies][ts] for details. * Support backup scheduling and retention policies. There is a new [KurrentDBBackupSchedule][bs] CRD with a CronJob-like syntax. There are also two mechanisms for configuring retention policies: a `.keep` count on `KurrentDBBackupSchedule`, and a new `.ttl` on `KurrentDBBackup`. * Support standalone read-only replicas pointed at a remote cluster. This enables advanced topologies like a having your quorum nodes in one region and a read-only replica in a distant region. See [Deploying Standalone Read-Only Replicas][ror] for an example. * Support template strings in some extra metadata for child resources of the `KurrentDB` object. This allows, for example, to annotate each of the automatically created LoadBalancers with unique external-dns annotations. See [KurrentDBExtraMetadataSpec][em] for details. [ts]: ../operations/advanced-networking.md#traffic-strategy-options [bs]: resource-types.md#kurrentdbbackupschedulespec [ror]: ../operations/database-deployment.md#deploying-standalone-read-only-replicas [em]: resource-types.md#kurrentdbextrametadataspec ### New in 1.4.1 * Fix rolling restarts to be quorum-aware for extra data safety. * Add quorum-aware full restarts for changes that must be applied to all nodes at once, like adding TLS. * Fix the `internodeTrafficStrategy: SplitDNS` setting to run correctly on more container runtimes. * Fix a hang caused adding to pod labels in `extraMetadata` after a KurrentDB was deployed. * Correctly enforce the immutability of the `sourceBackup` setting to prevent confusing behavior. * Fix the helm chart to prevent allowing two operator instances to briefly conflict during upgrades. ### New in 1.4.2 * Fix bug where deleting KurrentDBs with LoadBalancers enabled could leave dangling cloud resources. * Automatically grow PVC requested storage size to match the `restoreSize` of a VolumeSnapshot, when starting new nodes from VolumeSnapshots. This could happen when a user had SourceBackup set or when adding new quorum nodes or read-only replicas to an existing cluster. * Allow extra metadata for resources deployed by the Helm chart. See `values.yaml` in the Helm chart for details. ### New in 1.4.3 * Officially support running in RedHat OpenShift clusters. * Support deploying through the Operator Lifecycle Manager (OLM) in addition to Helm. OLM is the recommended mechanism for deploying operators in OpenShift. ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v25.x * v24.x * v23.10+ ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/getting-started/installation.md --- This section covers the various aspects of installing the Operator. The Operator supports installation [via Helm](#install-using-helm) and [via the Operator Lifecycle Manager (OLM)](#install-using-olm). OLM is the recommended way to install on Red Hat OpenShift clusters, where OLM is installed by default. ::: important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Install Using Helm ### Prerequisites * A Kubernetes cluster running any [non-EOL version of Kubernetes](https://kubernetes.io/releases/). * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [Helm 3 CLI](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ### Basic Installation The most basic installation will: * install necessary CRDs * run the Operator with a `ClusterRole` * configure the Operator to watch all namespaces To install in this way, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.4.3 \ --create-namespace \ --namespace kurrent-system \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Creates `kurrent-system` and deploys the Operator into it. * Deploys CRDs. * Applies the Operator license. * Populates a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent-system STATUS: deployed REVISION: 1 TEST SUITE: None ``` Additional customizations are described in the following sections. ### Option: Targeting Specific Namespaces The Operator may be installed to only control resources in specific namespaces. When installed in this way, it uses a `Role` in each target namespace rather than a `ClusterRole`. For example, to configure the Operator to control resources in namespaces `foo` and `bar`, add a flag like `--set operator.namespaces='{foo,bar}'` to the installation steps described in [Basic Installation](#basic-installation). ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the Helm installation. ::: Note that there is no requirement that namespace used for the Helm installation overlap with the namespaces controlled by the Operator. ### Option: Manual CRD Installation Some prefer to deploy CRDs manually, rather than through Helm. In this case, you must manually install the CRDs before the Helm installation (and again with each upgrade): ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.4.3 --untar # Install the CRDs kubectl apply -f kurrentdb-operator/templates/crds ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackupschedules.kubernetes.kurrent.io created ``` Then, follow the installation steps described in [Basic Installation](#basic-installation) with the additional flag `--set crds.enabled=false`. Due to the extra steps at both installation and upgrade, we recommend letting the Helm chart automatically manage your CRDs. ::: caution If you set the value of `crds.keep` to `false` (the default is `true`), helm upgrades and rollbacks can result in data loss. If `crds.keep` is `false` and `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ### Upgrading A Helm Installation The Operator can be upgraded using the following `helm` commands: ```bash helm repo update kurrent-latest helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --namespace kurrent \ --version {version} \ --reset-then-reuse-values ``` Here's what these commands do: * Refresh the local Helm repository index * Locate an existing operator installation in namespace `kurrent` * Select the target upgrade version `{version}` e.g. `1.4.3` * Perform the upgrade, preserving values that were set during installation ## Install Using OLM ### Prerequisites * An OpenShift cluster (version 4.17 or newer), or Kubernetes with OLM installed. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * `oc` (or `kubectl`) installed, on installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Namespaces Choose the namespace into which you will install your operator, and also the namespaces you want your operator to control. Create the namespaces now: ```bash # a namespace into which we will install the operator oc create namespace kurrent-system # namespaces we want the operator to control oc create namespace foo bar ``` ### Create A `Secret` The Operator requires a `Secret` in its namespace containing a valid license. For OLM installations, the name of the secret must be `kurrentdb-operator`. ```bash oc create secret generic -n kurrent-system kurrentdb-operator \ --from-file=licenseKey=/path/to/license.key \ --from-file=licenseFile=/path/to/license.lic ``` ### Create A `CatalogSource` A `CatalogSource` is the resource that tells OLM where to look for available operator versions. ```bash oc apply -f - << EOF apiVersion: operators.coreos.com/v1alpha1 kind: CatalogSource metadata: name: kurrentdb-operator namespace: olm spec: sourceType: grpc image: docker.kurrent.io/kurrent-latest/kurrentdb-operator-catalog:latest displayName: KurrentDB Operator publisher: "Kurrent, Inc" grpcPodConfig: securityContextConfig: restricted EOF ``` ### Create An `OperatorGroup` An `OperatorGroup` is the resource that tells OLM which namespaces an operator should target. ```bash oc apply -f - <- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/getting-started/resource-types.md --- # Supported Resource Types The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` * `KurrentDBBackupSchedule` ## KurrentDB This resource type is used to define a database deployment. ### API #### KurrentDBSpec | Field | Required | Description | |---------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster. May be 1, 3, or, for [standalone ReadOnly-Replicas][ror], it may be 0. | | `image` *string* | Yes | KurrentDB container image URL | | `resources` *[ResourceRequirements][d1]* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim][d2]* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork][d3]* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database, see [below](#configuring-kurrent-db) | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity][d4]* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector][d5]* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints][d6]* | No | Scheduling constraints for the Kurrent DB pod. | | `readOnlyReplias` *[KurrentDBReadOnlyReplicasSpec][d7]* | No | Read-only replica configuration the Kurrent DB Cluster. | | `extraMetadata` *[KurrentDBExtraMetadataSpec][d8]* | No | Additional annotations and labels for child resources. | | `quorumNodes` *string array* | No | A list of endpoints (in host:port notation) to reach the quorum nodes when .Replicas is zero, see [standalone ReadOnlyReplicas][ror] | [d1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [d2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [d3]: #kurrentdbnetwork [d4]: #kurrentdbsecurity [d5]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core [d6]: #kurrentdbconstraints [d7]: #kurrentdbreadonlyreplicasspec [d8]: #kurrentdbextrametadataspec [ror]: ../operations/database-deployment.md#deploying-standalone-read-only-replicas #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |----------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | No | Number of read-only replicas in the cluster. Defaults to zero. | | `resources` *[ResourceRequirements][r1]* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim][r2]* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints][r3]* | No | Scheduling constraints for the Kurrent DB pod. | [r1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [r2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [r3]: #kurrentdbconstraints #### KurrentDBConstraints | Field | Required | Description | |----------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the Kurrent DB may consider during scheduling. | | `affinity` *[Affinity][c1]* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the Kurrent DB pod. | | `tolerations` *list of [Toleration][c2]* | No | The tolerations for scheduling the Kurrent DB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint][c3]* | No | The topology spread constraints for scheduling the Kurrent DB pod. | [c1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core [c2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core [c3]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core #### KurrentDBExtraMetadataSpec | Field | Required | Description | |----------------------------------------------------|----------|---------------------------------------------------------------------| | `all` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for all child resource types. | | `configMaps` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for ConfigMaps. | | `statefulSets` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for StatefulSets. | | `pods` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for Pods. | | `persistentVolumeClaims` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for PersistentVolumeClaims. | | `headlessServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-cluster headless Services. | | `headlessPodServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-pod headless Services. | | `loadBalancers` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for LoadBalancer-type Services. | [m1]: #extrametadataspec Note that select kinds of extra metadata support template expansion to allow multiple instances of a child resource to be distinguished from one another. In particular, `ConfigMaps`, `StatefulSets`, and `HeadlessServices` support "per-node-kind" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a primary node or `"-replica"` for a replica node Additionally, `HeadlessPodServices` and `LoadBalancers` support "per-pod" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a primary node or `"-replica"` for a replica node * `{podName}` expands to the name of the pod corresponding to the resource * `{podOrdinal}` the ordinal assigned to the pod corresponding to the resource Notably, `Pods` and `PersistentVolumeClaims` do not support any template expansions, due to how `StatefulSets` work. #### ExtraMetadataSpec | Field | Required | Description |-------------------------|-----------|-----------------------------------| | `labels` *object* | No | Extra labels for a resource. | | `annotations` *object* | No | Extra annotations for a resource. | #### KurrentDBNetwork | Field | Required | Description | |----------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer][n1]* | Yes | Defines a load balancer to use with the database | | `fqdnTemplate` *string* | No | The template string used to define the external advertised address of a node | | `internodeTrafficStrategy` *string* | No | How servers dial each other. One of `"ServiceName"` (default), `"FQDN"`, or `"SplitDNS"`. See [details][n2]. | | `clientTrafficStrategy` *string* | No | How clients dial servers. One of `"ServiceName"` or `"FQDN"` (default). See [details][n2]. | | `splitDNSExtraRules` *list of [DNSRule][n3]* | No | Advanced configuration for when `internodeTrafficStrategy` is set to `"SplitDNS"`. | [n1]: #kurrentdbloadbalancer [n2]: ../operations/advanced-networking.md#traffic-strategy-options [n3]: #dnsrule Note that `fqdnTemplate` supports the following expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a primary node or `"-replica"` for a replica node * `{podName}` expands to the name of the pod When `fqdnTemplate` is empty, it defaults to `{podName}.{name}{nodeTypeSuffix}.{domain}`. #### DNSRule | Field | Required | Description | |--------------------|----------|----------------------------------------------------------------------------------------| | `host` *string* | Yes | A host name that should be intercepted. | | `result` *string* | Yes | An IP address to return, or another hostname to look up for the final IP address. | | `regex` *boolean* | No | Whether `host` and `result` should be treated as regex patterns. Defaults to `false`. | Note that when `regex` is `true`, the regex support is provided by the [go standard regex library](https://pkg.go.dev/regexp/syntax), and [referencing captured groups](https://pkg.go.dev/regexp#Regexp.Expand) differs from some other regex implementations. For example, to redirect lookups matching the pattern ``` .my-db.my-namespace.svc.cluster.local ``` to ``` .my-domain.com ``` you could use the following dns rule: ```yaml host: ([a-z0-9-]*)\.my-db\.my-namespace\.svc\.cluster\.local result: ${1}.my-domain.com regex: true ``` #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate. | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use. | | `certificateSubjectName` *string* | No | Deprecated field. The value of this field is always ignored. | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API #### KurrentDBBackupSpec | Field | Required | Description | |----------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If unspecified, the leader is used. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | | `extraMetadata` *[KurrentDBBackupExtraMetadataSpec][b1]* | No | Additional annotations and labels for child resources. | | `ttl` *string* | No | A time-to-live for this backup. If unspecified, the TTL is treated as infinite. | [b1]: #kurrentdbbackupextrametadataspec The format of the `ttl` may be in years (`y`), weeks (`w`), days (`d`), hours (`h`), or seconds (`s`), or a combination like `1d12h` #### KurrentDBBackupExtraMetadataSpec | Field | Required | Description | |------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------| | All *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for all child resource types (currently only VolumeSnapshots). | | VolumeSnapshots *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for VolumeSnapshots. | ## KurrentDBBackupSchedule This resource type is used to define a schedule for creating database backups and retention policies. #### KurrentDBBackupScheduleSpec | Field | Required | Description | |------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------| | `schedule` *string* | Yes | A CronJob-style schedule. See [Writing a CronJob Spec][s2]. | | `timeZone` *string* | No | A timezone specification. Defaults to `Etc/UTC`. | | `template` *[KurrentDBBackup][s1]* | Yes | A `KurrentDBBackup` template. | | `keep` *integer* | No | The maximum of complete backups this schedule will accumulate before it prunes the oldes ones. If unset, there is no limit. | | `suspend` *boolean* | No | [s1]: #kurrentdbbackupspec [s2]: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#writing-a-cronjob-spec Note that the only metadata allowed in `template.metadata` is `name`, `labels`, and `annotations`. If `name` is provided, it will be extended with an index like `my-name-1` when creating backups, otherwise created backups will be based on the name of the schedule resource. ## Configuring Kurrent DB The [`KurrentDB.spec.configuration` yaml field](#kurrentdbspec) may contain any valid configuration values for Kurrent DB. However, some values may be unnecessary, as the Operator provides some defaults, while other values may be ignored, as the Operator may override them. The Operator-defined default configuration values, which may be overridden by the user's `KurrentDB.spec.configuration` are: | Default Field | Default Value | |------------------------------|---------------| | DisableLogFile | true | | EnableAtomPubOverHTTP | true | | Insecure | false | | PrepareTimeoutMs | 3000 | | CommitTimeoutMs | 3000 | | GossipIntervalMs | 2000 | | GossipTimeoutMs | 5000 | | LeaderElectionTimeoutMs | 2000 | | ReplicationHeartbeatInterval | 1000 | | ReplicationHeartbeatTimeout | 2500 | | NodeHeartbeatInterval | 1000 | | NodeHeartbeatTimeout | 2500 | The Operator-managed configuration values, which take precedence over the user's `KurrentDB.spec.configuration`, are: | Managed Field | Value | |------------------------------| -------------------------------------------------------------| | Db | hard-coded volume mount point | | Index | hard-coded volume mount point | | Log | hard-coded volume mount point | | Insecure | true if `KurrentDB.spec.security.certificateSecret` is empty | | DiscoverViaDns | false (`GossipSeed` is used instead) | | AllowAnonymousEndpointAccess | true | | AllowUnknownOptions | true | | NodeIp | 0.0.0.0 (to accept traffic from outside pod) | | ReplicationIp | 0.0.0.0 (to accept traffic from outside pod) | | NodeHostAdvertiseAs | Derived from pod name | | ReplicationHostAdvertiseAs | Derived from pod name | | AdveritseHostToClientAs | Derived from `KurrentDB.spec.newtork.fqdnTemplate` | | ClusterSize | Derived from `KurrentDB.spec.replicas` | | GossipSeed | Derived from pod list | | ReadOnlyReplica | Automatically set for ReadOnlyReplica pods | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/advanced-networking.md --- # Advanced Networking KurrentDB is a clustered database, and all official KurrentDB clients are cluster-aware. As a result, there are times when a client will find out from one server how to connect to another server. To make this work, each server advertises how clients and other servers should contact it. The Operator lets you customize these advertisements. Such customizations are influenced by your cluster topology, where your KurrentDB clients will run, and also your security posture. This page will help you select the right networking and security configurations for your needs. ## Configuration Options This document is intended to help pick appropriate traffic strategies and certificate options for your situation. Let us first examine the range of possible settings for each. ### Traffic Strategy Options Servers advertise how they should be dialed by other servers according to the `KurrentDB.spec.network.internodeTrafficStrategy` setting, which is one of: * `"ServiceName"` (default): servers use each other's Kubernetes service name to contact each other. * `"FQDN"`: servers use each other's fully-qualified domain name (FQDN) to contact each other. * `"SplitDNS"`: servers advertise FQDNs to each other, but a tiny sidecar DNS resolver in each server pod intercepts the lookup of FQDNs for local pods and returns their actual pod IP address instead (the same IP address returned by the `"ServiceName"` setting). Servers advertise how they should be dialed by clients according to the `KurrentDB.spec.network.clientTrafficStrategy` setting, which is one of: * `"ServiceName"`: clients dial servers using the server's Kubernetes service name. * `"FQDN"` (default): clients dial servers using the server's FQDN. Note that the `"SplitDNS"` settings is not an option for the `clientTrafficStrategy`, simply because the KurrentDB Operator does not deploy your clients and so cannot inject a DNS sidecar container into your client pods. However, it is possible to write a [CoreDNS rewrite rule][rr] to accomplish a similar effect as `"SplitDNS"` but for client-to-server traffic. [rr]: https://coredns.io/2017/05/08/custom-dns-entries-for-kubernetes/ ### Certificate Options Except for test deployments, you always want to provide TLS certificates to your KurrentDB deployments. The reason is that insecure deployments disable not only TLS, but also all authentication and authorization features of the database. There are three basic options for how to obtain certificates: * Use self-signed certs: you can put any name in your self-signed certs, including Kubernetes service names, which enables `"ServiceName"` traffic strategies. A common technique is to use [cert-manager][cm] to manage the self-signed certificates and to use [trust-manager][tm] to distribute trust of those self-signed certificates to clients. * Use a publicly-trusted certificate provider: you can only put FQDNs on your certificate, which limits your traffic strategies to FQDN-based connections (`"FQDN"` or `"SplitDNS"`). * Use both: self-signed certs on the servers, plus an Ingress using certificates from a public certificate provider and configured for TLS termination. Note that at this time, the Operator does not assist with the creation of Ingresses. [cm]: https://cert-manager.io/ [tm]: https://cert-manager.io/docs/trust/trust-manager/ ## Considerations Now let us consider a few different aspects of your situation to help guide the selection of options. ### What are your security requirements? The choice of certificate provider has a security aspect to it. The KurrentDB servers use the certificate to authenticate each other, so anybody who has read access to the certificate or who can produce a matching, trusted certificate, can impersonate another server, and obtain full access to the database. The obvious implication of this is that access to the Kubernetes Secrets which contain server certificates should be limited to those who are authorized to administer the database. But it may not be obvious that if control of your domain's DNS configuration is shared by many business units in your organization, it may be the case that self-signed certificates with `internodeTrafficStrategy` of `"ServiceName"` provides the tightest control over database access. So your security posture may require that you choose one of: * self-signed certs and `"ServiceName"` traffic strategies, if all your clients are inside the Kubernetes cluster * self-signed certs on servers with `internodeTrafficStrategy` of `"ServiceName"` plus Ingresses configured with publicly-trusted certificate providers and `clientTrafficStrategy` of `"FQDN"` ### Where will your KurrentDB servers run? If any servers are not in the same Kubernetes cluster, for instance, if you are using the [standalone read-only-replica feature](database-deployment.md#deploying-standalone-read-only-replicas) to run a read-only replica in a second Kubernetes cluster from the quorum nodes, then you will need to pick from a few options to ensure internode connectivity: * `internodeTrafficStrategy` of `"SplitDNS"`, so every server connects to others by their FQDN, but when a connection begins to another pod in the same cluster, the SplitDNS feature will direct the traffic along direct pod-to-pod network interfaces. This solution assumes FQDNs on certificates, which enables you to use publicly trusted certificate authorities to generate certificates for each cluster, which can also ease certificate management. * `internodeTrafficStrategy` of `"ServiceName"`, plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster. This solution requires self-signed certificates, and also that the certificates on servers in both clusters are signed by the same self-signed Certificate Authority. [ens]: https://kubernetes.io/docs/concepts/services-networking/service/#externalname ### Where will your KurrentDB clients run? If any of your KurrentDB clients will run outside of Kubernetes, your `clientTrafficStrategy` must be `"FQDN"` to ensure connectivity. If your KurrentDB clients are all within Kubernetes, but spread through more than one Kubernetes cluster, you may use one of: * `clientTrafficStrategy` of `"FQDN"`. * `clientTrafficStrategy` of `"ServiceName"` plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster(s), as described above. ### How bad are hairpin traffic patterns for your deployment? Hairpin traffic patterns occur when a pod inside a Kubernetes cluster connects to another pod in the same Kubernetes cluster through its public IP address rather than its pod IP address. The traffic moves outside of Kubernetes to the public IP then "hairpin" turns back into the cluster. For example, with `clientTrafficStrategy` of `"FQDN"`, clients connecting to a server inside the same cluster will not automatically connect directly to the server pod, even though they are both inside the Kubernetes cluster and that would be the most direct possible connection. Hairpin traffic patterns are never good, but they're also not always bad. You will need to evaluate the impact in your own environment. Consider some of the following possibilities: * In a cloud environment, sometimes internal traffic is cheaper than traffic through a public IP, so there could be a financial impact. * If the FQDN connects to, for example, an nginx ingress, then pushing Kubernetes-internal traffic through nginx may either over-burden your nginx instance or it may slow down your traffic unnecessarily. Between servers, hairpin traffic can always be avoided with an `internodeTrafficStrategy` of `"SplitDNS"`. For clients, one solution is to prefer a `clientTrafficStrategy` of `"ServiceName"`, or you may consider adding a [CoreDNS rewrite rule][rr]. ## Common Solutions With the above considerations in mind, let us consider a few common solutions. ### Everything in One Kubernetes Cluster When all your KurrentDB servers and clients are within a single Kubernetes cluster, life is easy: * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Use cert-manager to configure a certificate based on the KurrentDB based around service names. * Use trust-manager to configure clients to trust the self-signed certificates. This solution provides the highest possible security, avoids hairpin traffic patterns, and leverages Kubernetes-native tooling to ease the pain of self-signed certificate management. ### Servers Anywhere, Clients Anywhere If using publicly trusted certificates is acceptable (see [above](#what-are-your-security-requirements)), almost every need can be met with one of the simplest configurations: * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Use cert-manager to automatically create certificates through an ACME provider like LetsEncrypt. * If clients may be outside of Kubernetes or multiple Kubernetes clusters are in play, set `KurrentDB.spec.network.loadBalancer.enable` to `true`, making your servers publicly accessible. This solution is still highly secure, provided your domain's DNS management is tightly controlled. It also supports virtually every server and client topology. Server hairpin traffic never occurs and client hairpin traffic — if a problem — can be addressed with a [CoreDNS rewrite rule][rr]. ### Multiple Kubernetes Clusters and a VPC Peering If you want all your KurrentDB resources within private networking for extra security, but also need to support multiple Kubernetes clusters in different regions, you can set up a VPC Peering between your clusters and configure your inter-cluster traffic to use it. There could be many variants of this solution; we'll describe one based on ServiceNames and one based on FQDNs. #### ServiceName-based Variant * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Ensure that each server has an IP address in the VPC Peering. * In each Kubernetes cluster, manually configure [ExternalName Services][ens] for each server not in that cluster. ExternalName Services can only redirect to hostnames, not bare IP addresses, so you may need to ensure that there is a DNS name to resolve each server's IP address in the VPC Peering. * Use self-signed certificates, and make sure to use the same certificate authority to sign certificates in each cluster. #### FQDN-based Variant * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Ensure that each server has an IP address in the VPC Peering. * Ensure that each server's FQDN resolves to the IP address of that server in the VPC peering. * If client-to-server hairpin traffic within each Kubernetes cluster is a problem, add a [CoreDNS rewrite rule][rr] to each cluster to prevent it. * Use a publicly-trusted certificate authority to create certificates based on the FQDN. They may be generated per-Kubernetes cluster independently, since the certificate trust will be automatic. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/database-backup.md --- # Database Backup The sections below detail how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrent.test loadBalancer: enabled: true ``` ## Automatically delete backups with a TTL A TTL can be set on a backup to delete the backup after a certain amount of time has passed since its creation. For example, to delete the backup 5 days after it was created: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ttl: 5d ``` ## Scheduling Backups A `KurrentDBBackupSchedule` can be created with a CronJob-like schedule. Schedules also support a `.spec.keep` setting to automatically limit how many backups created by that schedule are retained. Using a schedule with `.keep` is slightly safer than using TTLs on the individual backups. This is because if, for some reason, you ceased to be able to create new backups, the TTL will continue to delete backups until you have none left, while in the same situation .keep would leave all your old snapshots in place until a new one could be created. For example, to create a new backup every midnight (UTC), and to keep the last 7 such backups at any time, you could create a `KurrentDBBackupSchedule` resource like this: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-backup-schedule spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: metadata: name: my-backup spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster keep: 7 ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/database-deployment.md --- # Example Deployments This page shows various deployment examples of KurrentDB. Each example assumes the that the Operator has been installed in a way that it can at least control KurrentDB resources in the `kurrent` namespace. Each example is designed to illustrate specific techniques: * [Single Node Insecure Cluster](#single-node-insecure-cluster) is the "hello world" example that illustrates the most basic features possible. An insecure cluster should not be used in production. * [Three Node Insecure Cluster with Two Read-Only Replicas](#three-node-insecure-cluster-with-two-read-only-replicas) illustrates how to deploy a clustered KurrentDB instance and how to add read-only replicas to it. * [Three Node Secure Cluster (using self-signed certificates)](#three-node-secure-cluster-using-self-signed-certificates) illustrates how to secure a cluster with self-signed certificates using cert-manager. * [Three Node Secure Cluster (using LetsEncrypt)](#three-node-secure-cluster-using-letsencrypt) illustrates how to secure a cluster with LetsEncrypt. * [Deploying Standalone Read-only Replicas](#deploying-standalone-read-only-replicas) illustrates an advanced topology where a pair of read-only replicas is deployed in a different Kubernetes cluster than where the quorum nodes are deployed. * [Deploying With Scheduling Constraints](#deploying-with-scheduling-constraints): illustrates how to deploy a cluster with customized scheduling constraints for the KurrentDB pods. * [Custom Database Configuration](#custom-database-configuration) illustrates how to make direct changes to the KurrentDB configuration file. ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB version 25.0.0 will be used * 1 vCPU will be requested as the minimum (upper bound is unlimited) * 1 GB of memory will be used * 512 MB of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' ``` ## Enable Enterprise Features The Operator license provided during Helm installation is different from the KurrentDB license used to unlock enterprise features. Configure your KurrentDB license by creating a Secret containing the license key, and provide a reference to that Secret in the `.spec.licenseSecret` field. Note that the Secret resource and the KurrentDB resource must be in the same namespace. ```yaml apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' licenseSecret: name: my-license-secret key: licenseKey ``` ## Three Node Insecure Cluster with Two Read-Only Replicas Note that read-only replicas are only supported by KurrentDB in clustered configurations, that is, with multiple quorum nodes. The following `KurrentDB` resource type defines a three node cluster with the following properties: * Security is not enabled * 1 GB of memory will be used per quorum node, but read-only replicas will have 2 GB of memory * The quorum nodes will be exposed as `kurrentdb-{idx}.kurrent.test` * The read-only replicas will be exposed as `kurrentdb-replica-{idx}.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' readOnlyReplicas: replicas: 2 ``` ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * Security is enabled using self-signed certificates * The KurrentDB servers will be exposed as `kurrentdb-{idx}.kurrent.test` * Servers will dial each other by Kubernetes service name (`*.kurrent.svc.cluster.local`) * Clients will dial servers by the FQDN (`*.kurrent.test`) * The self-signed certificate is valid for both service name and FQDN. ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, be sure to follow the steps in [Using Self-Signed Certificates](managing-certificates.md#using-self-signed-certificates). ## Three Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a three node cluster with the following properties: * Security is enabled using certificates from LetsEncrypt * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrent.test` * The LetsEncrypt certificate is only valid for the FQDN (`*.kurrent.test`) * Clients will dial servers by FQDN * Server will dial each other by FQDN but because of the `SplitDNS` feature, they will still connect via direct pod-to-pod networking, as if they had dialed each other by Kubernetes service name. ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.kurrent.test' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key ``` Before deploying this cluster, be sure to follow the steps in [Using LetsEncrypt Certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt). ## Deploying Standalone Read-only Replicas This example illustrates an advanced topology where a pair of read-only replicas is deployed in a different Kubernetes cluster than where the quorum nodes are deployed. We make the following assumptions: * LetsEncrypt certificates are used everywhere, to ease certificate management * LoadBalancers are enabled to ensure each node is accessible through its FQDN * `internodeTrafficStrategy` is `"SplitDNS"` to avoid hairpin traffic patterns between servers * the quorum nodes will have `-qn` suffixes in their FQDN while the read-only replicas will have `-rr` suffixes This `Certificate` should be deployed in **both** clusters: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.kurrent.test' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer ``` This `KurrentDB` resource defines the quorum nodes in one cluster: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}-qn.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` And this `KurrentDB` resource defines the standalone read-only replica in another cluster. Notice that: * `.replicas` is 0, but `.quorumNodes` is set instead * `.readOnlyReplicas.replicas` is set * `fqdnTemplate` differs slightly from above ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 0 quorumNodes: - mydb-0-qn.kurrent.test:2113 - mydb-1-qn.kurrent.test:2113 - mydb-2-qn.kurrent.test:2113 readOnlyReplicas: replicas: 2 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}-sa.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` ## Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) * [Affinity and Anti-Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) * [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) * [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) * [Node Name](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename) For example, in cloud deployments, you may want to maximize uptime by asking each replica of a KurrentDB cluster to be deployed in a different availability zone. The following KurrentDB resource does that, and also requires KurrentDB to schedule pods onto nodes labeled with `machine-size:large`: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: my-kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' constraints: nodeSelector: machine-size: large topologySpreadConstraints: - maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: my-kurrentdb-cluster whenUnsatisfiable: DoNotSchedule ``` If no scheduling constraints are configured, the operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. The parameters which are defaulted or overridden by the operator are listed [in the CRD reference](../getting-started/resource-types.md#configuring-kurrent-db). For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:25.1.0 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' ``` ## Accessing Deployments ### External The Operator will create one service of type `LoadBalancer` per KurrentDB node when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Picking certificate names Each node in each KurrentDB cluster you create will advertise a fully-qualified domain name (FQDN). Clients will expect those advertised names to match the names you configure on your TLS certificates. You will need to understand how the FQDN is calculated for each node in order to request a TLS certificate that is valid for each node of your kurrentdb cluster. By default, the [network.fqdnTemplate field of your KurrentDB spec](../getting-started/resource-types.md#kurrentdbnetwork) is `{podName}.{name}{nodeTypeSuffix}.{domain}`, which may require multiple wildcard names on your certificate, like both `*.myName.myDomain.com` and `*.myName-replica.myDomain.com`. You may prefer to instead configure an `fqdnTemplate` like `{podName}.{domain}`, which could be covered by a single wildcard: `*.myDomain.com`. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.4.3/operations/modify-deployments.md --- # Modify Deployments Updating KurrentDB deployments through the Operator is done by modifying the KurrentDB Custom Resources (CRs) using standard Kubernetes tools. Most updates are processed almost immediately, but there is special logic in place around resizing the number of replicas in a cluster. ## Applying Updates `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:25.0.0"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` Note that the actual count of replicas in a cluster may take time to update. See [Updating Replica Count](#updating-replica-count), below. ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` ## Updating Primary Replica Count A user configures the KurrentDB cluster by setting the `.spec.replicas` setting of a KurrentDB resource. The current actual number of replicas can be observed as `.status.replicas`. The process to grow or shrink the replicas in a cluster safely requires carefully stepping the KurrentDB cluster through a series of consensus states, which the Operator handles automatically. In both cases, if the resizing flow gets stuck for some reason, you can cancel the resize by setting `.spec.replicas` back to its original value. ### Upsizing a KurrentDB Cluster The steps that the Operator takes to go from 1 to 3 nodes in a KurrentDB cluster are: * Take a VolumeSnapshot of pod 0 (the initial pod). * Reconfigure pod 0 to expect a three-node cluster. * Start a new pod 1 from the VolumeSnapshot. * Wait for pod 0 and pod 1 to establish quorum. * Start a new pod 2 from the VolumeSnapshot. Note that the database cannot process writes between the time that the Operator reconfigures pod 0 for a three-node cluster and when pod 0 and pod 1 establish quorum. The purpose of the VolumeSnapshot is to greatly reduce the amount of replication pod 1 must do from pod 0 before quorum is established, which greatly reduce the amount of downtime during the resize. ### Downsizing a KurrentDB Cluster The steps that the Operator takes to go from 3 nodes to 1 in a KurrentDB cluster are: * Make sure pod 0 and pod 1 are caught up with the leader (which may be one of them). * Stop pod 2. * Wait for quorum to be re-established between pods 0 and 1. * Stop pod 1. * Reconfigure pod 0 as a one-node cluster. Note that the database cannot process writes briefly after the Operator stops pod 2, and again briefly after the Operator reconfigures pod 0. :::important It is technically possible for data loss to occur when the Operator stops pod 2 if there are active writes against the database, and either of the other two pods happen to fail at approximately the same time pod 2 stops. The frequency of an environment failure should hopefully be low enough that this is not a realistic concern. However, to reduce the risk to truly zero, you must ensure that there are no writes against the database at the time when you downsize your cluster. ::: ## Updating Read-Only Replica Count Since Read-Only Replica nodes are not electable as leaders, it is simpler to increase or decrease the number of running read-only replicas. Still, when adding new read-only replicas, the Operator uses VolumeSnapshots to expedite the initial catch-up reads for new read-only replicas. The steps that the Operator takes to increase the number of read-only replicas are: * Take a VolumeSnapshot of a primary node. * Start new read-only replica node(s) based on that snapshot. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node KurrentDB clusters, even across multiple Kubernetes clusters * Back up and restore KurrentDB clusters * Automate backups with a schedule and retention policies * Automatically detect and load TLS certificate updates * Configure KurrentDB initial users and passwords * Perform rolling upgrades and update configurations ### New in 1.5.0 * Support Archiver nodes. Archiver nodes are a KurrentDB feature that lets you offload your old, less-frequently-accessed data into blob storage. See [an example][arx]. * Support for running KurrentDB pods under a specific `ServiceAccount`, to support IRSA access to cloud storage for archiving. See the [serviceAccountName setting][san] setting. * Management of initial user configuration. The `admin` and `ops` passwords can be set on database creation, as well as fully custom users. See [an example of a secure deployment][usr]. * Automatically detect TLS certificate updates and load them into the database with zero downtime. With cert-manager (or any automated cert renewal system), certificate rotation now requires zero administrator action. * Support multiple custom certificate authorities. This supports migrating from one CA to another without downtime, and also supports multi-kubernetes-cluster topologies with self-signed certificates without having to transfer root CA private keys between clusters. See the [certificateAuthoritySecret setting][sec]. * Support 5-node clusters. A 5-node cluster ensures that, even during a rolling restart, you can still lose one node without risk of data loss. * Support telemetry opt-out. See the [telemetryOptOut setting][tlm]. * Support loadBalancerClass configuration. See the [loadBalancerClass setting][lbs]. * Support for non-`cluster.local` cluster domains (automatically detected). * Label pods with their current role in the KurrentDB cluster. The label is updated after every successful health check, which is about once every minute. * Support NodePort configuration. * Allow administrators to explicitly request configuration reloads, rolling restarts, or full restarts of a KurrentDB cluster. See the [Manually Triggering Reload or Restarts][trg] for details. [arx]: ../operations/database-deployment.md#three-node-insecure-cluster-with-archiving [san]: resource-types.md#kurrentdbspec [usr]: ../operations/database-deployment.md#three-node-secure-cluster-using-self-signed-certificates [sec]: resource-types.md#kurrentdbsecurity [tlm]: resource-types.md#kurrentdbspec [lbs]: resource-types.md#kurrentdbloadbalancer [trg]: ../operations/modify-deployments.md#manually-triggering-reload-or-restart ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v26.x * v25.x * v24.x * v23.10+ ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/getting-started/installation.md --- This section covers the various aspects of installing the Operator. The Operator supports installation [via Helm](#install-using-helm) and [via the Operator Lifecycle Manager (OLM)](#install-using-olm). OLM is the recommended way to install on Red Hat OpenShift clusters, where OLM is installed by default. ::: important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Install Using Helm ### Prerequisites * A Kubernetes cluster running any [non-EOL version of Kubernetes](https://kubernetes.io/releases/). * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [Helm 3 CLI](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ### Basic Installation The most basic installation will: * install necessary CRDs * run the Operator with a `ClusterRole` * configure the Operator to watch all namespaces To install in this way, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.5.0 \ --create-namespace \ --namespace kurrent-system \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Creates `kurrent-system` and deploys the Operator into it. * Deploys CRDs. * Applies the Operator license. * Populates a new Helm release called `kurrentdb-operator` in the `kurrent` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent-system STATUS: deployed REVISION: 1 TEST SUITE: None ``` Additional customizations are described in the following sections. ### Option: Targeting Specific Namespaces The Operator may be installed to only control resources in specific namespaces. When installed in this way, it uses a `Role` in each target namespace rather than a `ClusterRole`. For example, to configure the Operator to control resources in namespaces `foo` and `bar`, add a flag like `--set operator.namespaces='{foo,bar}'` to the installation steps described in [Basic Installation](#basic-installation). ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the Helm installation. ::: Note that there is no requirement that namespace used for the Helm installation overlap with the namespaces controlled by the Operator. ### Option: Manual CRD Installation Some prefer to deploy CRDs manually, rather than through Helm. In this case, you must manually install the CRDs before the Helm installation (and again with each upgrade): ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.5.0 --untar # Install the CRDs kubectl apply -f kurrentdb-operator/templates/crds ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackupschedules.kubernetes.kurrent.io created ``` Then, follow the installation steps described in [Basic Installation](#basic-installation) with the additional flag `--set crds.enabled=false`. Due to the extra steps at both installation and upgrade, we recommend letting the Helm chart automatically manage your CRDs. ::: caution If you set the value of `crds.keep` to `false` (the default is `true`), helm upgrades and rollbacks can result in data loss. If `crds.keep` is `false` and `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ### Upgrading A Helm Installation The Operator can be upgraded using the following `helm` commands: ```bash helm repo update kurrent-latest helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --namespace kurrent \ --version {version} \ --reset-then-reuse-values ``` Here's what these commands do: * Refresh the local Helm repository index * Locate an existing operator installation in namespace `kurrent` * Select the target upgrade version `{version}` e.g. `1.5.0` * Perform the upgrade, preserving values that were set during installation ## Install Using OLM ### Prerequisites * An OpenShift cluster (version 4.17 or newer), or Kubernetes with OLM installed. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * `oc` (or `kubectl`) installed, on installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Namespaces Choose the namespace into which you will install your operator, and also the namespaces you want your operator to control. Create the namespaces now: ```bash # a namespace into which we will install the operator oc create namespace kurrent-system # namespaces we want the operator to control oc create namespace foo bar ``` ### Create A `Secret` The Operator requires a `Secret` in its namespace containing a valid license. For OLM installations, the name of the secret must be `kurrentdb-operator`. ```bash oc create secret generic -n kurrent-system kurrentdb-operator \ --from-file=licenseKey=/path/to/license.key \ --from-file=licenseFile=/path/to/license.lic ``` ### Create A `CatalogSource` A `CatalogSource` is the resource that tells OLM where to look for available operator versions. ```bash oc apply -f - << EOF apiVersion: operators.coreos.com/v1alpha1 kind: CatalogSource metadata: name: kurrentdb-operator namespace: olm spec: sourceType: grpc image: docker.kurrent.io/kurrent-latest/kurrentdb-operator-catalog:latest displayName: KurrentDB Operator publisher: "Kurrent, Inc" grpcPodConfig: securityContextConfig: restricted EOF ``` ### Create An `OperatorGroup` An `OperatorGroup` is the resource that tells OLM which namespaces an operator should target. ```bash oc apply -f - <- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/getting-started/resource-types.md --- # Configuration Reference The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` * `KurrentDBBackupSchedule` ## KurrentDB This resource type is used to define a database deployment. ### API #### KurrentDBSpec | Field | Required | Description | |----------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster. May be 1, 3, 5, or, for [standalone ReadOnly-Replicas][ror], it may be 0. | | `image` *string* | Yes | KurrentDB container image URL. See [Selecting An Image][img], below. | | `resources` *[ResourceRequirements][d1]* | No | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim][d2]* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork][d3]* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database, see [below](#configuring-kurrent-db) | | `environmentSecret` *string* | No | The name of a Secret to populate environment variables. If the secret changes a rolling restart occurs. | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity][d4]* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector][d5]* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints][d6]* | No | Scheduling constraints for the Kurrent DB pod. | | `readOnlyReplicas` *[KurrentDBReadOnlyReplicasSpec][d7]* | No | Read-only replica configuration for the Kurrent DB Cluster. | | `archiver` *[KurrentDBArchiverSpec][d8]* | No | Archiver replica configuration for the Kurrent DB Cluster. | | `extraMetadata` *[KurrentDBExtraMetadataSpec][d9]* | No | Additional annotations and labels for child resources. | | `quorumNodes` *string array* | No | A list of endpoints (in host:port notation) to reach the quorum nodes when .Replicas is zero, see [standalone ReadOnlyReplicas][ror] | | `serviceAccountName` *string* | No | A ServiceAccount for pods to run as (defaults to `default` in the current namespace). Useful for IRSA, see [archiver example][arx]. | | `telemetryOptOut` *boolean* | No | Opt-out of telemetry in the KurrentDB cluster. | | `users` *KurrentDBUsersSpec* | No | Initial user configuration. No deployment should be considered secure without configure initial user passwords. | | `configReloadKey` *string* | No | Has no effect, except a change to this value triggers a config reload. See [Manually Triggering Reload or Restart][trg]. | | `rollingRestartKey` *string* | No | Has no effect, except a change to this value triggers a rolling restart. See [Manually Triggering Reload or Restart][trg]. | | `fullRestartKey` *string* | No | Has no effect, except a change to this value triggers a full restart. See [Manually Triggering Reload or Restart][trg]. | [img]: #selecting-an-image [d1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [d2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [d3]: #kurrentdbnetwork [d4]: #kurrentdbsecurity [d5]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core [d6]: #kurrentdbconstraints [d7]: #kurrentdbreadonlyreplicasspec [d8]: #kurrentdbarchiverspec [d9]: #kurrentdbextrametadataspec [ror]: ../operations/database-deployment.md#deploying-standalone-read-only-replicas [arx]: ../operations/database-deployment.md#three-node-insecure-cluster-with-archiving [trg]: ../operations/modify-deployments.md#manually-triggering-reload-or-restart #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |----------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | No | Number of read-only replicas in the cluster. Defaults to zero. | | `resources` *[ResourceRequirements][r1]* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim][r2]* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints][r3]* | No | Scheduling constraints for the Kurrent DB pod. | [r1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [r2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [r3]: #kurrentdbconstraints #### KurrentDBArchiverSpec Other than `enabled`, each of the fields in `KurrentDBArchiverSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |----------------------------------------------|----------|--------------------------------------------------------------------------| | `enabled` *bool* | No | If an Archiver node should be added to the cluster. Defaults to False. | | `resources` *[ResourceRequirements][a1]* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim][a2]* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints][a3]* | No | Scheduling constraints for the Kurrent DB pod. | [a1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [a2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [a3]: #kurrentdbconstraints #### KurrentDBConstraints | Field | Required | Description | |----------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the Kurrent DB may consider during scheduling. | | `affinity` *[Affinity][c1]* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the Kurrent DB pod. | | `tolerations` *list of [Toleration][c2]* | No | The tolerations for scheduling the Kurrent DB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint][c3]* | No | The topology spread constraints for scheduling the Kurrent DB pod. | [c1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core [c2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core [c3]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core #### KurrentDBExtraMetadataSpec | Field | Required | Description | |----------------------------------------------------|----------|---------------------------------------------------------------------| | `all` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for all child resource types. | | `configMaps` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for ConfigMaps. | | `statefulSets` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for StatefulSets. | | `pods` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for Pods. | | `persistentVolumeClaims` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for PersistentVolumeClaims. | | `headlessServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-cluster headless Services. | | `headlessPodServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-pod headless Services. | | `loadBalancers` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for LoadBalancer-type Services. | [m1]: #extrametadataspec Note that select kinds of extra metadata support template expansion to allow multiple instances of a child resource to be distinguished from one another. In particular, `ConfigMaps`, `StatefulSets`, and `HeadlessServices` support "per-node-kind" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. Additionally, `HeadlessPodServices` and `LoadBalancers` support "per-pod" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. * `{podName}` expands to the name of the pod corresponding to the resource * `{podOrdinal}` the ordinal assigned to the pod corresponding to the resource Notably, `Pods` and `PersistentVolumeClaims` do not support any template expansions, due to how `StatefulSets` work. #### ExtraMetadataSpec | Field | Required | Description |-------------------------|-----------|-----------------------------------| | `labels` *object* | No | Extra labels for a resource. | | `annotations` *object* | No | Extra annotations for a resource. | #### KurrentDBNetwork | Field | Required | Description | |----------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer][n1]* | Yes | Defines a load balancer to use with the database | | `fqdnTemplate` *string* | No | The template string used to define the external advertised address of a node. See below. | | `internodeTrafficStrategy` *string* | No | How servers dial each other. One of `"ServiceName"` (default), `"FQDN"`, or `"SplitDNS"`. See [details][n2]. | | `clientTrafficStrategy` *string* | No | How clients dial servers. One of `"ServiceName"` or `"FQDN"` (default). See [details][n2]. | | `splitDNSExtraRules` *list of [DNSRule][n3]* | No | Advanced configuration for when `internodeTrafficStrategy` is set to `"SplitDNS"`. | | `nodePort` *integer* | No | The HTTP port that KurrentDB listens on. Defaults to 2113. For priviliged ports, see below. | | `replicationPort` *integer* | No | The TCP port for replication traffic from other nodes. Defaults to 1112. For priviliged ports, see below. | | `nodeTcpPort` *integer* | No | The TCP port for legacy TCP client traffic. Defaults to 1113. For priviliged ports, see below. | [n0]: #KurrentDBNetwork [n1]: #kurrentdbloadbalancer [n2]: ../operations/advanced-networking.md#traffic-strategy-options [n3]: #dnsrule Note that `fqdnTemplate` supports the following expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurretnDB.metadata.namespace * `{domain}` expands to the KurrnetDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. * `{podName}` expands to the name of the pod When `fqdnTemplate` is empty, it defaults to `{podName}.{name}{nodeTypeSuffix}.{domain}`. The ports for `nodePort`, `replicationPort`, and `nodeTcpPort` may be chosen arbitrarily, but note that the Operator always runs nodes as non-root. Therefore, to utilize priviliged ports (port numbers less than 1024), you will need to use images with `setcap cap_net_bind_service+ep` applied to the `kurrentd` binary inside the image. Kurrent offers Red Hat-certified images which meet this criteria, see [Selecting An Image][img], below. #### DNSRule | Field | Required | Description | |--------------------|----------|----------------------------------------------------------------------------------------| | `host` *string* | Yes | A host name that should be intercepted. | | `result` *string* | Yes | An IP address to return, or another hostname to look up for the final IP address. | | `regex` *boolean* | No | Whether `host` and `result` should be treated as regex patterns. Defaults to `false`. | Note that when `regex` is `true`, the regex support is provided by the [go standard regex library](https://pkg.go.dev/regexp/syntax), and [referencing captured groups](https://pkg.go.dev/regexp#Regexp.Expand) differs from some other regex implementations. For example, to redirect lookups matching the pattern ``` .my-db.my-namespace.svc.cluster.local ``` to ``` .my-domain.com ``` you could use the following dns rule: ```yaml host: ([a-z0-9-]*)\.my-db\.my-namespace\.svc\.cluster\.local result: ${1}.my-domain.com regex: true ``` #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIps` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | | `loadBalancerClass` *string* | No | The `Service.spec.loadBalancerClass` to use. Defaults to empty. | Note that changing the `loadBalancerClass` will require deleting the old load balancer Service completely and recreating it (which make take a while) because `loadBalancerClass` is an immutable field of a Service. #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------| | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (this maps directly to the database property `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateSecret](#certificatesecret)* | No | Secret containing the CA TLS certificate. Updates trigger a config reload. Only `.name` is required; See below. | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use. Updates trigger a config reload. | | `certificateSubjectName` *string* | No | Deprecated field. The value of this field is always ignored. | Note that in `certificateAuthoritySecret`, only `.name` needs to be non-empty. If `.keyName` is non-empty, only that Secret key will be mounted into the pod as a CA. If it is set to the empty string (`""`), all Secret keys will be mounted as CAs, which allows for rotating CAs without downtime, by trusting both old and new CAs for a period of time. `.privateKeyName` is deprecated and ignored. #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the secret holding the certificate details | | `keyName` *string* | Yes | Key within the secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the secret containing the TLS certificate private key | #### KurrentDBUsersSpec | Field | Required | Description | |------------------------------------------------|----------|------------------------------------------------------| | adminPasswordSecret *[SecretKeySelector][u1]* | Yes | Secret containing initial password for `admin` user. | | opsPasswordSecret *[SecretKeySelector][u1]* | Yes | Secret containing initial password for `ops` user. | | customUsers *[KurrentDBUserSpec][u2] array* | No | Custom users to add to the database. | The `admin` and `ops` passwords are required if users are configured at all. Those paswords are set by initial database creation; when set, the database will never accept the default password (`changeit)`. No deployment should be considered secure without configuring these two passwords. The additioanl users described in `customUsers` are optional, and are configured by the Operator after the first successful health check. The Operator does not currently support updates to the intial user configuration. The Secrets referenced here are not read after the first time the KurrentDB cluster reaches a healhty state, and may safely be deleted. [u1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core [u2]: #kurrentdbuserspec #### KurrentDBUserSpec | Field | Required | Description | |------------------------------------------|----------|----------------------------------------------------| | loginName *string* | Yes | The login name of the user. | | fullName *string* | Yes | The display name of the user. | | passwordSecret *[SecretKeySelector][u1]* | Yes | The Secret from which the password should be read. | | groups *string array* | No | Additional groups to add user to, see below. | Note that KurrentDB always adds every new user to a group matching its login name, so the groups listed in `.groups` are in addition to that default behavior. The Operator does not currently support updates to the intial user configuration. The Secrets referenced here are not read after the first time the KurrentDB cluster reaches a healhty state, and may safely be deleted. ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API #### KurrentDBBackupSpec | Field | Required | Description | |----------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If unspecified, the leader is used. | | `volumeSnapshotClassName` *string* | Yes | The name of the underlying volume snapshot class to use. | | `extraMetadata` *[KurrentDBBackupExtraMetadataSpec][b1]* | No | Additional annotations and labels for child resources. | | `ttl` *string* | No | A time-to-live for this backup. If unspecified, the TTL is treated as infinite. | [b1]: #kurrentdbbackupextrametadataspec The format of the `ttl` may be in years (`y`), weeks (`w`), days (`d`), hours (`h`), or seconds (`s`), or a combination like `1d12h` #### KurrentDBBackupExtraMetadataSpec | Field | Required | Description | |------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------| | All *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for all child resource types (currently only VolumeSnapshots). | | VolumeSnapshots *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for VolumeSnapshots. | ## KurrentDBBackupSchedule This resource type is used to define a schedule for creating database backups and retention policies. #### KurrentDBBackupScheduleSpec | Field | Required | Description | |------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------| | `schedule` *string* | Yes | A CronJob-style schedule. See [Writing a CronJob Spec][s2]. | | `timeZone` *string* | No | A timezone specification. Defaults to `Etc/UTC`. | | `template` *[KurrentDBBackup][s1]* | Yes | A `KurrentDBBackup` template. | | `keep` *integer* | No | The maximum of complete backups this schedule will accumulate before it prunes the oldes ones. If unset, there is no limit. | | `suspend` *boolean* | No | [s1]: #kurrentdbbackupspec [s2]: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#writing-a-cronjob-spec Note that the only metadata allowed in `template.metadata` is `name`, `labels`, and `annotations`. If `name` is provided, it will be extended with an index like `my-name-1` when creating backups, otherwise created backups will be based on the name of the schedule resource. ## Selecting an Image When selecting a KurrentDB image, you may choose from one of Kurrent's standard images: | Versions | Image | Link | |--------------------|----------------------------------------------------------|-------------| | 23.10.x to 24.10.x | `docker.eventstore.com/eventstore/eventstoredb-ee:X.Y.Z` | [link][old] | | 25.0.0 and greater | `docker.kurrent.io/kurrent-latest/kurrentdb:X.Y.Z` | [link][std] | Additionally, Kurrent offers Red Hat-certified KurrentDB images. These images have the additional property that they have `setcap cap_net_bind_service+ep` applied to the `kurrentd` binary inside the image, which allows them to be used in conjunction with setting `.spec.network.nodePort` to a privileged port, like 443. These same images without the Red Hat Certification (or official Red Hat sha256 checks) are available without a Red Hat account directly from Kurrent. This is useful if you want the `setcap`-enabled image but don't care about the Red Hat Certification. | Versions | Certified | Image | Link | |--------------------|-----------|----------------------------------------------------------|-------------| | 25.0.0 and greater | Yes | `registry.connect.redhat.com/kurrent-io/kurrentdb:X.Y.Z` | [link][rhc] | | 25.0.0 and greater | No | `docker.kurrent.io/kurrent-latest/kurrentdb-rhel8:X.Y.Z` | [link][pre] | [old]: https://cloudsmith.io/~eventstore/repos/eventstore/packages/?q=format%3Adocker+name%3Aeventstoredb-ee [std]: https://cloudsmith.io/~eventstore/repos/kurrent-latest/packages/?q=format%3Adocker+name%3Akurrentdb [rhc]: https://catalog.redhat.com/en/software/containers/kurrent-io/kurrentdb/690a11e9b1dedf2b2890ef75 [pre]: https://cloudsmith.io/~eventstore/repos/kurrent-latest/packages/?q=format%3Adocker+name%3Akurrentdb-rhel8 ## Configuring KurrentDB The [`KurrentDB.spec.configuration` yaml field](#kurrentdbspec) may contain any valid configuration values for Kurrent DB. However, some values may be unnecessary, as the Operator provides some defaults, while other values may be ignored, as the Operator may override them. The Operator-defined default configuration values, which may be overridden by the user's `KurrentDB.spec.configuration` are: | Default Field | Default Value | |------------------------------|---------------| | DisableLogFile | true | | EnableAtomPubOverHTTP | true | | Insecure | false | | PrepareTimeoutMs | 3000 | | CommitTimeoutMs | 3000 | | GossipIntervalMs | 2000 | | GossipTimeoutMs | 5000 | | LeaderElectionTimeoutMs | 2000 | | ReplicationHeartbeatInterval | 1000 | | ReplicationHeartbeatTimeout | 2500 | | NodeHeartbeatInterval | 1000 | | NodeHeartbeatTimeout | 2500 | The Operator-managed configuration values, which take precedence over the user's `KurrentDB.spec.configuration`, are: | Managed Field | Value | |------------------------------| -------------------------------------------------------------| | Db | hard-coded volume mount point | | Index | hard-coded volume mount point | | Log | hard-coded volume mount point | | Insecure | true if `KurrentDB.spec.security.certificateSecret` is empty | | DiscoverViaDns | false (`GossipSeed` is used instead) | | AllowAnonymousEndpointAccess | true | | AllowUnknownOptions | true | | NodeIp | 0.0.0.0 (to accept traffic from outside pod) | | ReplicationIp | 0.0.0.0 (to accept traffic from outside pod) | | NodeHostAdvertiseAs | Derived from pod name | | ReplicationHostAdvertiseAs | Derived from pod name | | AdveritseHostToClientAs | Derived from `KurrentDB.spec.newtork.fqdnTemplate` | | ClusterSize | Derived from `KurrentDB.spec.replicas` | | GossipSeed | Derived from pod list | | ReadOnlyReplica | Automatically set for ReadOnlyReplica and Archiver pods | | NodePort | Derived from `KurrentDB.spec.network.nodePort` | | ReplicationPort | Derived from `KurrentDB.spec.network.replicationPort` | | NodeTcpPort | Derived from `KurrentDB.spec.network.nodeTcpPort` | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/advanced-networking.md --- # Advanced Networking KurrentDB is a clustered database, and all official KurrentDB clients are cluster-aware. As a result, there are times when a client will find out from one server how to connect to another server. To make this work, each server advertises how clients and other servers should contact it. The Operator lets you customize these advertisements. Such customizations are influenced by your cluster topology, where your KurrentDB clients will run, and also your security posture. This page will help you select the right networking and security configurations for your needs. ## Configuration Options This document is intended to help pick appropriate traffic strategies and certificate options for your situation. Let us first examine the range of possible settings for each. ### Traffic Strategy Options Servers advertise how they should be dialed by other servers according to the `KurrentDB.spec.network.internodeTrafficStrategy` setting, which is one of: * `"ServiceName"` (default): servers use each other's Kubernetes service name to contact each other. * `"FQDN"`: servers use each other's fully-qualified domain name (FQDN) to contact each other. * `"SplitDNS"`: servers advertise FQDNs to each other, but a tiny sidecar DNS resolver in each server pod intercepts the lookup of FQDNs for local pods and returns their actual pod IP address instead (the same IP address returned by the `"ServiceName"` setting). Servers advertise how they should be dialed by clients according to the `KurrentDB.spec.network.clientTrafficStrategy` setting, which is one of: * `"ServiceName"`: clients dial servers using the server's Kubernetes service name. * `"FQDN"` (default): clients dial servers using the server's FQDN. Note that the `"SplitDNS"` settings is not an option for the `clientTrafficStrategy`, simply because the KurrentDB Operator does not deploy your clients and so cannot inject a DNS sidecar container into your client pods. However, it is possible to write a [CoreDNS rewrite rule][rr] to accomplish a similar effect as `"SplitDNS"` but for client-to-server traffic. [rr]: https://coredns.io/2017/05/08/custom-dns-entries-for-kubernetes/ ### Certificate Options Except for test deployments, you always want to provide TLS certificates to your KurrentDB deployments. The reason is that insecure deployments disable not only TLS, but also all authentication and authorization features of the database. There are three basic options for how to obtain certificates: * Use self-signed certs: you can put any name in your self-signed certs, including Kubernetes service names, which enables `"ServiceName"` traffic strategies. A common technique is to use [cert-manager][cm] to manage the self-signed certificates and to use [trust-manager][tm] to distribute trust of those self-signed certificates to clients. * Use a publicly-trusted certificate provider: you can only put FQDNs on your certificate, which limits your traffic strategies to FQDN-based connections (`"FQDN"` or `"SplitDNS"`). * Use both: self-signed certs on the servers, plus an Ingress using certificates from a public certificate provider and configured for TLS termination. Note that at this time, the Operator does not assist with the creation of Ingresses. [cm]: https://cert-manager.io/ [tm]: https://cert-manager.io/docs/trust/trust-manager/ ## Considerations Now let us consider a few different aspects of your situation to help guide the selection of options. ### What are your security requirements? The choice of certificate provider has a security aspect to it. The KurrentDB servers use the certificate to authenticate each other, so anybody who has read access to the certificate or who can produce a matching, trusted certificate, can impersonate another server, and obtain full access to the database. The obvious implication of this is that access to the Kubernetes Secrets which contain server certificates should be limited to those who are authorized to administer the database. But it may not be obvious that if control of your domain's DNS configuration is shared by many business units in your organization, it may be the case that self-signed certificates with `internodeTrafficStrategy` of `"ServiceName"` provides the tightest control over database access. So your security posture may require that you choose one of: * self-signed certs and `"ServiceName"` traffic strategies, if all your clients are inside the Kubernetes cluster * self-signed certs on servers with `internodeTrafficStrategy` of `"ServiceName"` plus Ingresses configured with publicly-trusted certificate providers and `clientTrafficStrategy` of `"FQDN"` ### Where will your KurrentDB servers run? If any servers are not in the same Kubernetes cluster, for instance, if you are using the [standalone read-only-replica feature](database-deployment.md#deploying-standalone-read-only-replicas) to run a read-only replica in a second Kubernetes cluster from the quorum nodes, then you will need to pick from a few options to ensure internode connectivity: * `internodeTrafficStrategy` of `"SplitDNS"`, so every server connects to others by their FQDN, but when a connection begins to another pod in the same cluster, the SplitDNS feature will direct the traffic along direct pod-to-pod network interfaces. This solution assumes FQDNs on certificates, which enables you to use publicly trusted certificate authorities to generate certificates for each cluster, which can also ease certificate management. * `internodeTrafficStrategy` of `"ServiceName"`, plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster. This solution requires self-signed certificates, and also that the certificates on servers in both clusters are signed by the same self-signed Certificate Authority. [ens]: https://kubernetes.io/docs/concepts/services-networking/service/#externalname ### Where will your KurrentDB clients run? If any of your KurrentDB clients will run outside of Kubernetes, your `clientTrafficStrategy` must be `"FQDN"` to ensure connectivity. If your KurrentDB clients are all within Kubernetes, but spread through more than one Kubernetes cluster, you may use one of: * `clientTrafficStrategy` of `"FQDN"`. * `clientTrafficStrategy` of `"ServiceName"` plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster(s), as described above. ### How bad are hairpin traffic patterns for your deployment? Hairpin traffic patterns occur when a pod inside a Kubernetes cluster connects to another pod in the same Kubernetes cluster through its public IP address rather than its pod IP address. The traffic moves outside of Kubernetes to the public IP then "hairpin" turns back into the cluster. For example, with `clientTrafficStrategy` of `"FQDN"`, clients connecting to a server inside the same cluster will not automatically connect directly to the server pod, even though they are both inside the Kubernetes cluster and that would be the most direct possible connection. Hairpin traffic patterns are never good, but they're also not always bad. You will need to evaluate the impact in your own environment. Consider some of the following possibilities: * In a cloud environment, sometimes internal traffic is cheaper than traffic through a public IP, so there could be a financial impact. * If the FQDN connects to, for example, an nginx ingress, then pushing Kubernetes-internal traffic through nginx may either over-burden your nginx instance or it may slow down your traffic unnecessarily. Between servers, hairpin traffic can always be avoided with an `internodeTrafficStrategy` of `"SplitDNS"`. For clients, one solution is to prefer a `clientTrafficStrategy` of `"ServiceName"`, or you may consider adding a [CoreDNS rewrite rule][rr]. ## Common Solutions With the above considerations in mind, let us consider a few common solutions. ### Everything in One Kubernetes Cluster When all your KurrentDB servers and clients are within a single Kubernetes cluster, life is easy: * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Use cert-manager to configure a certificate based on the KurrentDB based around service names. * Use trust-manager to configure clients to trust the self-signed certificates. This solution provides the highest possible security, avoids hairpin traffic patterns, and leverages Kubernetes-native tooling to ease the pain of self-signed certificate management. ### Servers Anywhere, Clients Anywhere If using publicly trusted certificates is acceptable (see [above](#what-are-your-security-requirements)), almost every need can be met with one of the simplest configurations: * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Use cert-manager to automatically create certificates through an ACME provider like LetsEncrypt. * If clients may be outside of Kubernetes or multiple Kubernetes clusters are in play, set `KurrentDB.spec.network.loadBalancer.enable` to `true`, making your servers publicly accessible. This solution is still highly secure, provided your domain's DNS management is tightly controlled. It also supports virtually every server and client topology. Server hairpin traffic never occurs and client hairpin traffic — if a problem — can be addressed with a [CoreDNS rewrite rule][rr]. ### Multiple Kubernetes Clusters and a VPC Peering If you want all your KurrentDB resources within private networking for extra security, but also need to support multiple Kubernetes clusters in different regions, you can set up a VPC Peering between your clusters and configure your inter-cluster traffic to use it. There could be many variants of this solution; we'll describe one based on ServiceNames and one based on FQDNs. #### ServiceName-based Variant * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Ensure that each server has an IP address in the VPC Peering. * In each Kubernetes cluster, manually configure [ExternalName Services][ens] for each server not in that cluster. ExternalName Services can only redirect to hostnames, not bare IP addresses, so you may need to ensure that there is a DNS name to resolve each server's IP address in the VPC Peering. * Use self-signed certificates, and make sure to use the same certificate authority to sign certificates in each cluster. #### FQDN-based Variant * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Ensure that each server has an IP address in the VPC Peering. * Ensure that each server's FQDN resolves to the IP address of that server in the VPC peering. * If client-to-server hairpin traffic within each Kubernetes cluster is a problem, add a [CoreDNS rewrite rule][rr] to each cluster to prevent it. * Use a publicly-trusted certificate authority to create certificates based on the FQDN. They may be generated per-Kubernetes cluster independently, since the certificate trust will be automatic. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/database-backup.md --- # Database Backup The sections below detail how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Backing up the leader Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. ## Backing up a specific node Assuming there is a cluster called `kurrentdb-cluster` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster nodeName: kurrentdb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `kurrentdb-cluster-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 sourceBackup: kurrentdb-cluster-backup resources: requests: cpu: 1000m memory: 1Gi network: domain: kurrent.test loadBalancer: enabled: true ``` ## Automatically delete backups with a TTL A TTL can be set on a backup to delete the backup after a certain amount of time has passed since its creation. For example, to delete the backup 5 days after it was created: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: kurrentdb-cluster spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster ttl: 5d ``` ## Scheduling Backups A `KurrentDBBackupSchedule` can be created with a CronJob-like schedule. Schedules also support a `.spec.keep` setting to automatically limit how many backups created by that schedule are retained. Using a schedule with `.keep` is slightly safer than using TTLs on the individual backups. This is because if, for some reason, you ceased to be able to create new backups, the TTL will continue to delete backups until you have none left, while in the same situation .keep would leave all your old snapshots in place until a new one could be created. For example, to create a new backup every midnight (UTC), and to keep the last 7 such backups at any time, you could create a `KurrentDBBackupSchedule` resource like this: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-backup-schedule spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: metadata: name: my-backup spec: volumeSnapshotClassName: ebs-vs clusterName: kurrentdb-cluster keep: 7 ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/database-deployment.md --- # Example Deployments This page shows various deployment examples of KurrentDB. Each example assumes the that the Operator has been installed in a way that it can at least control KurrentDB resources in the `kurrent` namespace. Each example is designed to illustrate specific techniques: * [Single Node Insecure Cluster](#single-node-insecure-cluster) is the "hello world" example that illustrates the most basic features possible. An insecure cluster should not be used in production. * [Three Node Insecure Cluster with Two Read-Only Replicas](#three-node-insecure-cluster-with-two-read-only-replicas) illustrates how to deploy a clustered KurrentDB deployment and how to add read-only replicas to it. * [Three Node Insecure Cluster with Archiving](#three-node-insecure-cluster-with-archiving) illustrates how to deploy a clustered KurrentDB deployment and how to add an archiver node to it. * [Three Node Secure Cluster (using self-signed certificates)](#three-node-secure-cluster-using-self-signed-certificates) illustrates how to secure a cluster with self-signed certificates using cert-manager. * [Three Node Secure Cluster (using LetsEncrypt)](#three-node-secure-cluster-using-letsencrypt) illustrates how to secure a cluster with LetsEncrypt. * [Deploying Standalone Read-only Replicas](#deploying-standalone-read-only-replicas) illustrates an advanced topology where a pair of read-only replicas is deployed in a different Kubernetes cluster than where the quorum nodes are deployed. * [Deploying With Scheduling Constraints](#deploying-with-scheduling-constraints): illustrates how to deploy a cluster with customized scheduling constraints for the KurrentDB pods. * [Custom Database Configuration](#custom-database-configuration) illustrates how to make direct changes to the KurrentDB configuration file. ## Single Node Insecure Cluster The following `KurrentDB` resource type defines a single node cluster with the following properties: * The database will be deployed in the `kurrent` namespace with the name `kurrentdb-cluster` * Security is not enabled * KurrentDB v26.x will be used * 1 vCPU will be requested as the minimum (upper bound is unlimited) * 1 GB of memory will be used * 512 MB of storage will be allocated for the data disk * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-0.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' ``` ## Three Node Insecure Cluster with Two Read-Only Replicas Note that read-only replicas are only supported by KurrentDB in clustered configurations, that is, with multiple quorum nodes. The following `KurrentDB` resource type defines a four node cluster (three quorum nodes plus a read-only replica) with the following properties: * Security is not enabled * 1 GB of memory will be used per quorum node, but read-only replicas will have 2 GB of memory * The quorum nodes will be exposed as `kurrentdb-{idx}.kurrent.test` * The read-only replicas will be exposed as `kurrentdb-replica-{idx}.kurrent.test` ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' readOnlyReplicas: replicas: 2 ``` ## Three Node Insecure Cluster with Archiving Note that archiver nodes are a special kind of read-only replica node. So, like read-only replicas, archving can only be enabled in clustered configurations. Also note that Archiving is an enterprise feature requiring a KurrentDB license, which is distinct from the Operator license provided during the Helm installation. We provide the KurrentDB license as a secret. Archiving requires access to cloud storage. This example assumes that you have configured IRSA for your cloud (see docs for [AWS][irsaaws], [Azure][irsaazure], and [GCP][irsagcp]), as this provides the best security and lets the cloud provider manage the credentials, but in test clusters it may be sufficient to use the `KurrentDB.spec.environmentSecret` to pass cloud credentials into the KurrentDB pods as environment variables. The following `KurrentDB` resource type defines a four node cluster (three quorum nodes plus an archiving read-only replica) with the following properties: * Security is not enabled * All pods run as `my-irsa-service-account`, which is annotated for an AWS Role ARN that is [configured on the AWS side][irsaaws] to allow IRSA from this service account. * The quorum nodes will be exposed as `kurrentdb-{idx}.kurrent.test` * The archiver node (there cannot be multiple) will be exposed as `kurrentdb-archiver-0.kurrent.test`. * All nodes have 2GiB of disk size requested, and are configured to retain 1GiB of data locally; older data will be read from cloud storage. [irsaaws]: https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html [irsaazure]: https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster [irsagcp]: https://docs.cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#kubernetes-sa-to-iam ```yaml apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: ServiceAccount metadata: name: my-irsa-service-account namespace: kurrent annotations: eks.amazonaws.com/role-arn: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 2Gi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' licenseSecret: name: my-license-secret key: licenseKey serviceAccountName: my-irsa-service-account configuration: Archive: Enabled: true RetainAtLeast: Days: 0 LogicalBytes: 1073741824 # 1GiB StorageType: S3 S3: Region: us-west-1 Bucket: my-bucket archiver: enabled: true ``` ## Three Node Secure Cluster (using self-signed certificates) The following `KurrentDB` resource type defines a three node cluster with the following properties: * Security is enabled using self-signed certificates * The KurrentDB servers will be exposed as `kurrentdb-{idx}.kurrent.test` * Servers will dial each other by Kubernetes service name (`*.kurrent.svc.cluster.local`) * Clients will dial servers by the FQDN (`*.kurrent.test`) * The self-signed certificate is valid for both service name and FQDN. * The `admin`, `ops`, are configured at database creation, so at no point is there a deployment with the default `admin:changeit` credentials. * A custom user (named `custom`) is also created the first time the db appears healthy. ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrentdb-cluster.kurrent.svc.cluster.local' - '*.kurrentdb-cluster-replica.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: ca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: ca-tls keyName: ca.crt certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins ``` Before deploying this cluster, be sure to follow the steps in [Using Self-Signed Certificates](managing-certificates.md#using-self-signed-certificates). ## Three Node Secure Cluster (using LetsEncrypt) The following `KurrentDB` resource type defines a three node cluster with the following properties: * Security is enabled using certificates from LetsEncrypt * The KurrentDB instance that is provisioned will be exposed as `kurrentdb-{idx}.kurrent.test` * The LetsEncrypt certificate is only valid for the FQDN (`*.kurrent.test`) * Clients will dial servers by FQDN * Server will dial each other by FQDN but because of the `SplitDNS` feature, they will still connect via direct pod-to-pod networking, as if they had dialed each other by Kubernetes service name. * The `admin`, `ops`, are configured at database creation, so at no point is there a deployment with the default `admin:changeit` credentials. * A custom user (named `custom`) is also created the first time the db appears healthy. ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: kurrentdb-cluster namespace: kurrent spec: secretName: kurrentdb-cluster-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.kurrent.test' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: kurrentdb-cluster-tls keyName: tls.crt privateKeyName: tls.key users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins ``` Before deploying this cluster, be sure to follow the steps in [Using LetsEncrypt Certificates](managing-certificates.md#using-trusted-certificates-via-letsencrypt). ## Deploying Standalone Read-only Replicas This example illustrates an advanced topology where a pair of read-only replicas is deployed in a different Kubernetes cluster than where the quorum nodes are deployed. We make the following assumptions: * LetsEncrypt certificates are used everywhere, to ease certificate management * LoadBalancers are enabled to ensure each node is accessible through its FQDN * `internodeTrafficStrategy` is `"SplitDNS"` to avoid hairpin traffic patterns between servers * the quorum nodes will have `-qn` suffixes in their FQDN while the read-only replicas will have `-rr` suffixes This `Certificate` should be deployed in **both** clusters: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.kurrent.test' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.kurrent.test' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer ``` This `KurrentDB` resource defines the quorum nodes in one cluster: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}-qn.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` And this `KurrentDB` resource defines the standalone read-only replica in another cluster. Notice that: * `.replicas` is 0, but `.quorumNodes` is set instead * `.readOnlyReplicas.replicas` is set * `fqdnTemplate` differs slightly from above ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 0 quorumNodes: - mydb-0-qn.kurrent.test:2113 - mydb-1-qn.kurrent.test:2113 - mydb-2-qn.kurrent.test:2113 readOnlyReplicas: replicas: 2 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}-sa.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.kurrent.test' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` ## Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector) * [Affinity and Anti-Affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) * [Topology Spread Constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) * [Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) * [Node Name](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename) For example, in cloud deployments, you may want to maximize uptime by asking each replica of a KurrentDB cluster to be deployed in a different availability zone. The following KurrentDB resource does that, and also requires KurrentDB to schedule pods onto nodes labeled with `machine-size:large`: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: my-kurrentdb-cluster namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' constraints: nodeSelector: machine-size: large topologySpreadConstraints: - maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: my-kurrentdb-cluster whenUnsatisfiable: DoNotSchedule ``` If no scheduling constraints are configured, the operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. ## Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. The parameters which are defaulted or overridden by the operator are listed [in the CRD reference](../getting-started/resource-types.md#configuring-kurrent-db). For example, to enable projections, the deployment configuration looks as follows: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: kurrentdb-cluster namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 1000m memory: 1Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true fqdnTemplate: '{podName}.{domain}' ``` ## Accessing Deployments ### External The Operator will create one service of type `LoadBalancer` per KurrentDB node when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each service is annotated with `external-dns.alpha.kubernetes.io/hostname: {external cluster endpoint}` to allow the third-party tool [ExternalDNS](https://github.com/kubernetes-sigs/external-dns) to configure external access. ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a thirdparty tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Picking certificate names Each node in each KurrentDB cluster you create will advertise a fully-qualified domain name (FQDN). Clients will expect those advertised names to match the names you configure on your TLS certificates. You will need to understand how the FQDN is calculated for each node in order to request a TLS certificate that is valid for each node of your kurrentdb cluster. By default, the [network.fqdnTemplate field of your KurrentDB spec](../getting-started/resource-types.md#kurrentdbnetwork) is `{podName}.{name}{nodeTypeSuffix}.{domain}`, which may require multiple wildcard names on your certificate, like both `*.myName.myDomain.com` and `*.myName-replica.myDomain.com`. You may prefer to instead configure an `fqdnTemplate` like `{podName}.{domain}`, which could be covered by a single wildcard: `*.myDomain.com`. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/) ### Using trusted certificates via LetsEncrypt To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer The following example shows how a LetsEncrypt issuer can be deployed that leverages [AWS Route53](https://cert-manager.io/docs/configuration/acme/dns01/route53/): ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt spec: acme: privateKeySecretRef: name: letsencrypt-issuer-key email: { email } preferredChain: "" server: https://acme-v02.api.letsencrypt.org/directory solvers: - dns01: route53: region: { region } hostedZoneID: { hostedZoneId } accessKeyID: { accessKeyId } secretAccessKeySecretRef: name: aws-route53-credentials key: secretAccessKey selector: dnsZones: - { domain } - "*.{ domain }" ``` This can be deployed using the following steps: * Replace the variables `{...}` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certficates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-issuer spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl -n kurrent apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl -n kurrent apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.5.0/operations/modify-deployments.md --- # Modify Deployments Updating KurrentDB deployments through the Operator is done by modifying the KurrentDB Custom Resources (CRs) using standard Kubernetes tools. Most updates are processed almost immediately, but there is special logic in place around resizing the number of replicas in a cluster. ## Applying Updates `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `kurrentdb-cluster`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"replicas": 3}}' ``` Note that the actual count of replicas in a cluster may take time to update. See [Updating Replica Count](#updating-replica-count), below. ### Configuration ```bash kubectl -n kurrent patch kurrentdb kurrentdb-cluster --type=merge -p '{"spec":{"configuration": {"ProjectionsLevel": "all", "StartStandardProjections": "true"}}}' ``` ## Updating Primary Replica Count A user configures the KurrentDB cluster by setting the `.spec.replicas` setting of a KurrentDB resource. The current actual number of replicas can be observed as `.status.replicas`. The process to grow or shrink the replicas in a cluster safely requires carefully stepping the KurrentDB cluster through a series of consensus states, which the Operator handles automatically. In both cases, if the resizing flow gets stuck for some reason, you can cancel the resize by setting `.spec.replicas` back to its original value. ### Upsizing a KurrentDB Cluster The steps that the Operator takes to go from 1 to 3 nodes in a KurrentDB cluster are: * Take a VolumeSnapshot of pod 0 (the initial pod). * Reconfigure pod 0 to expect a three-node cluster. * Start a new pod 1 from the VolumeSnapshot. * Wait for pod 0 and pod 1 to establish quorum. * Start a new pod 2 from the VolumeSnapshot. Note that the database cannot process writes between the time that the Operator reconfigures pod 0 for a three-node cluster and when pod 0 and pod 1 establish quorum. The purpose of the VolumeSnapshot is to greatly reduce the amount of replication pod 1 must do from pod 0 before quorum is established, which greatly reduce the amount of downtime during the resize. ### Downsizing a KurrentDB Cluster The steps that the Operator takes to go from 3 nodes to 1 in a KurrentDB cluster are: * Make sure pod 0 and pod 1 are caught up with the leader (which may be one of them). * Stop pod 2. * Wait for quorum to be re-established between pods 0 and 1. * Stop pod 1. * Reconfigure pod 0 as a one-node cluster. Note that the database cannot process writes briefly after the Operator stops pod 2, and again briefly after the Operator reconfigures pod 0. :::important It is technically possible for data loss to occur when the Operator stops pod 2 if there are active writes against the database, and either of the other two pods happen to fail at approximately the same time pod 2 stops. The frequency of an environment failure should hopefully be low enough that this is not a realistic concern. However, to reduce the risk to truly zero, you must ensure that there are no writes against the database at the time when you downsize your cluster. ::: ## Updating Read-Only Replica Count Since Read-Only Replica nodes are not electable as leaders, it is simpler to increase or decrease the number of running read-only replicas. Still, when adding new read-only replicas, the Operator uses VolumeSnapshots to expedite the initial catch-up reads for new read-only replicas. The steps that the Operator takes to increase the number of read-only replicas are: * Take a VolumeSnapshot of a primary node. * Start new read-only replica node(s) based on that snapshot. Since an archiver node is a special kind of read-only replica, the logic for enabling or disabling an archiver is identical to the read-only replica logic. ## Manually Triggering Reload or Restart The Operator will automatically trigger a configuration reload, a rolling restart, or a full restart (where all nodes are brought down before bringing them back up) whenever it is required. For example, a configuration reload happens automatically when the database log level is changed or a TLS certificate or certificate authority is changed, since those are the only hot-reloadable configurations. Full restarts are triggered any time that a rolling restart would not be possible, such as converting an insecure cluster to a secure cluster, when old nodes and new nodes would not be able to talk to each other. Additionally, the Operator allows you to manually trigger any of those operations by altering one of: * `KurrentDB.spec.configReloadKey` * `KurrentDB.spec.rollingRestartKey` * `KurrentDB.spec.fullRestartKey` Each key starts as an empty string and each time a key is changed, the corresponding operation will be executed. Administrators may safely ignore these settings. Still, they may prove useful in specific scenarios, such as evaluating the Operator, or testing application performance while the database executes a rolling restart in a staging environment, before applying a reconfiguration to the production environment. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/getting-started/index.md --- # *** Welcome to the **KurrentDB Kubernetes Operator** guide. In this guide, we’ll refer to the KurrentDB Kubernetes Operator simply as “the Operator.” Use the Operator to simplify backup, scaling, and upgrades of KurrentDB clusters on Kubernetes. :::important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Why run KurrentDB on Kubernetes? Kubernetes is the modern enterprise standard for deploying containerized applications at scale. The Operator streamlines deployment and management of KurrentDB clusters. ## Features * Deploy single-node or multi-node KurrentDB clusters, even across multiple Kubernetes clusters * Back up and restore KurrentDB clusters * Automate backups with a schedule and retention policies * Automatically detect and load TLS certificate updates * Configure KurrentDB initial users and passwords * Perform rolling upgrades and update configurations ### New in 1.6.0 * Fix downtime during node pool upgrades. The operator now deploys and maintains a `PodDisruptionBudget` to ensure that external tooling knows when it is safe to drain a pod from the cluster. See [`podDisruptionBudgets.disable`][pdbs] if you need to disable them for some reason. * Improved health check logic. Before, health checks were done once every 60 seconds, in the `database-health-check` state. A normal cluster would bounce between that state and `database-healthy`. Now, pod-level health issues are detected immediately, and gossip checks are executed every 10 seconds, all while staying in the `database-healthy` state. This change improves both the freshness of the reported status and the ease for external monitoring, since a healthy cluster should always stay in `database-healthy`. * Support online license checks in addition to offline license checks. If your operator license file is missing or expired, the operator can continue as long as your license key is valid and your internet connection is working. * Expose operator license status through Kubernetes API. Now you can set up automated monitoring or reminders for license renewals by examining your KurrentDB's `.status.operatorLicense`. * Fix PVC selection logic during disk resize handling. Previously, clusters with PVCs that were migrated manually from non-operator-environments could sometimes fail to get resized. * Fix health check bug after scaling down read-only replicas, which caused the operator to wrongly label post-scaledown clusters as `database-unhealthy` for a long while. * Fix various bugs identified by an AI code audit. Most were associated with rarely-used options, and none had been reported by customers. [pdbs]: resource-types.md#poddisruptionbudgetsspec ## Supported KurrentDB Versions The Operator supports running the following major versions of KurrentDB: * v26.x * v25.x * v24.x * v23.10+ ## Supported Hardware Architectures The Operator is packaged for the following hardware architectures: * x86\_64 * arm64 ## Technical Support For support questions, please [contact us](https://www.kurrent.io/contact). ## First Steps Ready to install? Head over to the [installation](installation.md) section. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/getting-started/installation.md --- This section covers the various aspects of installing the Operator. The Operator supports installation [via Helm](#install-using-helm), [via Red Hat's OperatorHub](#install-on-openshift) and [via the Operator Lifecycle Manager (OLM)](#manual-olm-install). ::: important The Operator is an Enterprise-only feature, please [contact us](https://www.kurrent.io/contact) for more information. ::: ## Install Using Helm ### Prerequisites * A Kubernetes cluster running any [non-EOL version of Kubernetes](https://kubernetes.io/releases/). * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * The following CLI tools installed, on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [Helm 3 CLI](https://helm.sh/docs/intro/install/) * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Helm Repository Add the Kurrent Helm repository to your local environment: ```bash helm repo add kurrent-latest \ 'https://packages.kurrent.io/basic/kurrent-latest/helm/charts/' ``` ### Basic Installation The most basic installation will: * install necessary CRDs * run the Operator with a `ClusterRole` * configure the Operator to watch all namespaces To install in this way, run: ```bash helm install kurrentdb-operator kurrent-latest/kurrentdb-operator \ --version 1.6.0 \ --create-namespace \ --namespace kurrent-system \ --set-file operator.license.key=/path/to/license.key \ --set-file operator.license.file=/path/to/license.lic ``` This command: * Creates `kurrent-system` and deploys the Operator into it. * Deploys CRDs. * Applies the Operator license. * Populates a new Helm release called `kurrentdb-operator` in the `kurrent-system` namespace. *Expected Output*: ``` NAME: kurrentdb-operator LAST DEPLOYED: Thu Mar 20 14:51:42 2025 NAMESPACE: kurrent-system STATUS: deployed REVISION: 1 TEST SUITE: None ``` Additional customizations are described in the following sections. ### Option: Targeting Specific Namespaces The Operator may be installed to only control resources in specific namespaces. When installed in this way, it uses a `Role` in each target namespace rather than a `ClusterRole`. For example, to configure the Operator to control resources in namespaces `foo` and `bar`, add a flag like `--set operator.namespaces='{foo,bar}'` to the installation steps described in [Basic Installation](#basic-installation). ::: important Make sure the namespaces listed as part of the `operator.namespaces` parameter already exist before running the Helm installation. ::: Note that there is no requirement that namespace used for the Helm installation overlap with the namespaces controlled by the Operator. ### Option: Manual CRD Installation Some prefer to deploy CRDs manually, rather than through Helm. In this case, you must manually install the CRDs before the Helm installation (and again with each upgrade): ```bash # Download the kurrentdb-operator Helm chart helm pull kurrent-latest/kurrentdb-operator --version 1.6.0 --untar # Install the CRDs kubectl apply -f kurrentdb-operator/templates/crds ``` *Expected Output*: ``` customresourcedefinition.apiextensions.k8s.io/kurrentdbs.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackups.kubernetes.kurrent.io created customresourcedefinition.apiextensions.k8s.io/kurrentdbbackupschedules.kubernetes.kurrent.io created ``` Then, follow the installation steps described in [Basic Installation](#basic-installation) with the additional flag `--set crds.enabled=false`. Due to the extra steps at both installation and upgrade, we recommend letting the Helm chart automatically manage your CRDs. ::: caution If you set the value of `crds.keep` to `false` (the default is `true`), helm upgrades and rollbacks can result in data loss. If `crds.keep` is `false` and `crds.enabled` transitions from `true` to `false` during an upgrade or rollback, the CRDs will be removed from the cluster, deleting all `KurrentDBs` and `KurrentDBBackups` and their associated child resources, including the PVCs and VolumeSnapshots containing your data! ::: ### Upgrading A Helm Installation The Operator can be upgraded using the following `helm` commands: ```bash helm repo update kurrent-latest helm upgrade kurrentdb-operator kurrent-latest/kurrentdb-operator \ --namespace kurrent-system \ --version {version} \ --reset-then-reuse-values ``` Here's what these commands do: * Refresh the local Helm repository index * Locate an existing operator installation in namespace `kurrent-system` * Select the target upgrade version `{version}` e.g. `1.6.0` * Perform the upgrade, preserving values that were set during installation ## Install on OpenShift ### Prerequisites * An OpenShift cluster (version 4.17 or newer). * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * `oc` (or `kubectl`) installed on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Namespaces Choose the namespace into which you will install your operator, and also the namespaces you want your operator to control. Create the namespaces now: ```bash # a namespace into which we will install the operator oc create namespace kurrent-system # namespaces we want the operator to control oc create namespace foo oc create namespace bar ``` ### Create A `Secret` The Operator requires a `Secret` in its namespace containing a valid license. For OLM installations, the name of the secret must be `kurrentdb-operator`. ```bash oc create secret generic -n kurrent-system kurrentdb-operator \ --from-file=licenseKey=/path/to/license.key \ --from-file=licenseFile=/path/to/license.lic ``` ### Install via OperatorHub Finish the installation using Red Hat's OperatorHub to install an operator into `kurrent-system`, targeting namespaces `foo` and `bar`. ## Manual OLM Installation ### Prerequisites * Kubernetes with OLM installed. * Permission to create resources, deploy the Operator and install CRDs in the target cluster. * `oc` (or `kubectl`) installed on your shell’s `$PATH`, with `$KUBECONFIG` pointing to your cluster * A valid Operator license. Please [contact us](https://www.kurrent.io/contact) for more information. ### Configure Namespaces and Create A `Secret` Follow the first two steps described above for [Install On OpenShift](#install-on-openshift). However, instead of using Red Hat's OperatorHub, we'll manually create the `CatalogSource`, `OperatorGroup`, and `Subscription` resources (below). ### Create A `CatalogSource` A `CatalogSource` is the resource that tells OLM where to look for available operator versions. ```bash oc apply -f - << EOF apiVersion: operators.coreos.com/v1alpha1 kind: CatalogSource metadata: name: kurrentdb-operator namespace: olm spec: sourceType: grpc image: docker.kurrent.io/kurrent-latest/kurrentdb-operator-catalog:latest displayName: KurrentDB Operator publisher: "Kurrent, Inc" grpcPodConfig: securityContextConfig: restricted EOF ``` ### Create An `OperatorGroup` An `OperatorGroup` is the resource that tells OLM which namespaces an operator should target. ```bash oc apply -f - <- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/getting-started/resource-types.md --- # Configuration Reference The Operator supports the following resource types (known as `Kind`'s): * `KurrentDB` * `KurrentDBBackup` * `KurrentDBBackupSchedule` ## KurrentDB This resource type is used to define a database deployment. ### API #### KurrentDBSpec | Field | Required | Description | |----------------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of nodes in a database cluster. May be 1, 3, 5, or, for [standalone read-only replicas][ror], it may be 0. | | `image` *string* | Yes | KurrentDB container image URL. See [Selecting An Image][img], below. | | `resources` *[ResourceRequirements][d1]* | Yes | Database container resource limits and requests | | `storage` *[PersistentVolumeClaim][d2]* | Yes | Persistent volume claim settings for the underlying data volume | | `network` *[KurrentDBNetwork][d3]* | Yes | Defines the network configuration to use with the database | | `configuration` *yaml* | No | Additional configuration to use with the database, see [below](#configuring-kurrentdb) | | `environmentSecret` *string* | No | The name of a Secret to populate environment variables. If the secret changes a rolling restart occurs. | | `sourceBackup` *string* | No | Backup name to restore a cluster from | | `security` *[KurrentDBSecurity][d4]* | No | Security configuration to use for the database. This is optional, if not specified the cluster will be created without security enabled. | | `licenseSecret` *[SecretKeySelector][d5]* | No | A secret that contains the Enterprise license for the database | | `constraints` *[KurrentDBConstraints][d6]* | No | Scheduling constraints for the KurrentDB pod. | | `readOnlyReplicas` *[KurrentDBReadOnlyReplicasSpec][d7]* | No | Read-only replica configuration for the KurrentDB cluster. | | `archiver` *[KurrentDBArchiverSpec][d8]* | No | Archiver replica configuration for the KurrentDB cluster. | | `volumeSnapshotClassName` *string* | No | The volume snapshot class used when snapshotting this cluster. See [Volume Snapshot Class Selection][vsc]. | | `extraMetadata` *[KurrentDBExtraMetadataSpec][d9]* | No | Additional annotations and labels for child resources. | | `quorumNodes` *string array* | No | A list of endpoints (in host:port notation) to reach the quorum nodes when .Replicas is zero, see [standalone read-only replicas][ror] | | `serviceAccountName` *string* | No | A ServiceAccount for pods to run as (defaults to `default` in the current namespace). Useful for IRSA, see [archiver example][arx]. | | `telemetryOptOut` *boolean* | No | Opt-out of telemetry in the KurrentDB cluster. | | `users` *KurrentDBUsersSpec* | No | Initial user configuration. No deployment should be considered secure without configuring initial user passwords. | | `podDisruptionBudgets` *[PodDisruptionBudgetsSpec][dA]* | No | Configure PodDisruptionBudget that the operator creates to protect the database during Kubernetes-level maintenance. | | `configReloadKey` *string* | No | Has no effect, except a change to this value triggers a config reload. See [Manually Triggering Reload or Restart][trg]. | | `rollingRestartKey` *string* | No | Has no effect, except a change to this value triggers a rolling restart. See [Manually Triggering Reload or Restart][trg]. | | `fullRestartKey` *string* | No | Has no effect, except a change to this value triggers a full restart. See [Manually Triggering Reload or Restart][trg]. | [img]: #selecting-an-image [d1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [d2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [d3]: #kurrentdbnetwork [d4]: #kurrentdbsecurity [d5]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core [d6]: #kurrentdbconstraints [d7]: #kurrentdbreadonlyreplicasspec [d8]: #kurrentdbarchiverspec [d9]: #kurrentdbextrametadataspec [dA]: #poddisruptionbudgetsspec [ror]: ../operations/database-deployment.md#deploying-standalone-read-only-replicas [arx]: ../operations/database-deployment.md#three-node-insecure-cluster-with-archiving [trg]: ../operations/modify-deployments.md#manually-triggering-reload-or-restart [vsc]: #volume-snapshot-class-selection #### KurrentDBReadOnlyReplicasSpec Other than `replicas`, each of the fields in `KurrentDBReadOnlyReplicasSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |----------------------------------------------|----------|------------------------------------------------------------------| | `replicas` *integer* | Yes | Number of read-only replicas in the cluster. | | `resources` *[ResourceRequirements][r1]* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim][r2]* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints][r3]* | No | Scheduling constraints for the KurrentDB pod. | [r1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [r2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [r3]: #kurrentdbconstraints #### KurrentDBArchiverSpec Other than `enabled`, each of the fields in `KurrentDBArchiverSpec` default to the corresponding values from the main KurrentDBSpec. | Field | Required | Description | |----------------------------------------------|----------|--------------------------------------------------------------------------| | `enabled` *bool* | No | If an Archiver node should be added to the cluster. Defaults to False. | | `resources` *[ResourceRequirements][a1]* | No | Database container resource limits and requests. | | `storage` *[PersistentVolumeClaim][a2]* | No | Persistent volume claim settings for the underlying data volume. | | `configuration` *yaml* | No | Additional configuration to use with the database. | | `constraints` *[KurrentDBConstraints][a3]* | No | Scheduling constraints for the KurrentDB pod. | [a1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#resourcerequirements-v1-core [a2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#persistentvolumeclaimspec-v1-core [a3]: #kurrentdbconstraints #### KurrentDBConstraints | Field | Required | Description | |----------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------| | `nodeSelector` *yaml* | No | Identifies nodes that the KurrentDB pod may consider during scheduling. | | `affinity` *[Affinity][c1]* | No | The node affinity, pod affinity, and pod anti-affinity for scheduling the KurrentDB pod. | | `tolerations` *list of [Toleration][c2]* | No | The tolerations for scheduling the KurrentDB pod. | | `topologySpreadConstraints` *list of [TopologySpreadConstraint][c3]* | No | The topology spread constraints for scheduling the KurrentDB pod. | [c1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#affinity-v1-core [c2]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#toleration-v1-core [c3]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#topologyspreadconstraint-v1-core #### KurrentDBExtraMetadataSpec | Field | Required | Description | |----------------------------------------------------|----------|---------------------------------------------------------------------| | `all` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for all child resource types. | | `configMaps` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for ConfigMaps. | | `statefulSets` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for StatefulSets. | | `pods` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for Pods. | | `persistentVolumeClaims` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for PersistentVolumeClaims. | | `headlessServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-cluster headless Services. | | `headlessPodServices` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for the per-pod headless Services. | | `loadBalancers` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for LoadBalancer-type Services. | | `podDisruptionBudgets` *[ExtraMetadataSpec][m1]* | No | Extra annotations and labels for PodDisruptionBudgets. | [m1]: #extrametadataspec Note that select kinds of extra metadata support template expansion to allow multiple instances of a child resource to be distinguished from one another. In particular, `ConfigMaps`, `StatefulSets`, and `HeadlessServices` support "per-node-kind" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurrentDB.metadata.namespace * `{domain}` expands to the KurrentDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. Additionally, `HeadlessPodServices` and `LoadBalancers` support "per-pod" template expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurrentDB.metadata.namespace * `{domain}` expands to the KurrentDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. * `{podName}` expands to the name of the pod corresponding to the resource * `{podOrdinal}` the ordinal assigned to the pod corresponding to the resource PodDisruptionBudgets are per-KurrentDB and so support only the following template extensions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurrentDB.metadata.namespace * `{domain}` expands to the KurrentDBNetwork.domain Notably, `Pods` and `PersistentVolumeClaims` do not support any template expansions, due to how `StatefulSets` work. #### ExtraMetadataSpec | Field | Required | Description |-------------------------|-----------|-----------------------------------| | `labels` *object* | No | Extra labels for a resource. | | `annotations` *object* | No | Extra annotations for a resource. | #### KurrentDBNetwork | Field | Required | Description | |----------------------------------------------|----------|---------------------------------------------------------------------------------------------------------------------| | `domain` *string* | Yes | Domain used for external DNS e.g. advertised address exposed in the gossip state | | `loadBalancer` *[KurrentDBLoadBalancer][n1]* | No | Defines a load balancer to use with the database | | `fqdnTemplate` *string* | No | The template string used to define the external advertised address of a node. See below. | | `internodeTrafficStrategy` *string* | No | How servers dial each other. One of `"ServiceName"` (default), `"FQDN"`, or `"SplitDNS"`. See [details][n2]. | | `clientTrafficStrategy` *string* | No | How clients dial servers. One of `"ServiceName"` or `"FQDN"` (default). See [details][n2]. | | `splitDNSExtraRules` *list of [DNSRule][n3]* | No | Advanced configuration for when `internodeTrafficStrategy` is set to `"SplitDNS"`. | | `nodePort` *integer* | No | The HTTP port that KurrentDB listens on. Defaults to 2113. For privileged ports, see below. | | `replicationPort` *integer* | No | The TCP port for replication traffic from other nodes. Defaults to 1112. For privileged ports, see below. | | `nodeTcpPort` *integer* | No | The TCP port for legacy TCP client traffic. Defaults to 1113. For privileged ports, see below. | [n0]: #KurrentDBNetwork [n1]: #kurrentdbloadbalancer [n2]: ../operations/advanced-networking.md#traffic-strategy-options [n3]: #dnsrule Note that `fqdnTemplate` supports the following expansions: * `{name}` expands to KurrentDB.metadata.name * `{namespace}` expands to KurrentDB.metadata.namespace * `{domain}` expands to the KurrentDBNetwork.domain * `{nodeTypeSuffix}` expands to `""` for a quorum node, `"-replica"` for a read-only replica node, or `"-archiver"` for an archiver node. * `{podName}` expands to the name of the pod When `fqdnTemplate` is empty, it defaults to `{podName}.{name}{nodeTypeSuffix}.{domain}`. The ports for `nodePort`, `replicationPort`, and `nodeTcpPort` may be chosen arbitrarily, but note that the Operator always runs nodes as non-root. Therefore, to utilize privileged ports (port numbers less than 1024), you will need to use images with `setcap cap_net_bind_service+ep` applied to the `kurrentd` binary inside the image. Kurrent offers Red Hat-certified images which meet this criteria, see [Selecting An Image][img], below. #### DNSRule | Field | Required | Description | |--------------------|----------|----------------------------------------------------------------------------------------| | `host` *string* | Yes | A host name that should be intercepted. | | `result` *string* | Yes | An IP address to return, or another hostname to look up for the final IP address. | | `regex` *boolean* | No | Whether `host` and `result` should be treated as regex patterns. Defaults to `false`. | Note that when `regex` is `true`, the regex support is provided by the [go standard regex library](https://pkg.go.dev/regexp/syntax), and [referencing captured groups](https://pkg.go.dev/regexp#Regexp.Expand) differs from some other regex implementations. For example, to redirect lookups matching the pattern ``` .my-db.my-namespace.svc.cluster.local ``` to ``` .my-domain.com ``` you could use the following dns rule: ```yaml host: ([a-z0-9-]*)\.my-db\.my-namespace\.svc\.cluster\.local result: ${1}.my-domain.com regex: true ``` #### KurrentDBLoadBalancer | Field | Required | Description | |------------------------------|----------|--------------------------------------------------------------------------------| | `enabled` *boolean* | Yes | Determines if a load balancer should be deployed for each node | | `allowedIPs` *string array* | No | List of IP ranges allowed by the load balancer (default will allow all access) | | `loadBalancerClass` *string* | No | The `Service.spec.loadBalancerClass` to use. Defaults to empty. | Note that changing the `loadBalancerClass` will require deleting the old load balancer Service completely and recreating it (which may take a while) because `loadBalancerClass` is an immutable field of a Service. #### KurrentDBSecurity | Field | Required | Description | |------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------------| | `certificateReservedNodeCommonName` *string* | No | Common name for the TLS certificate (same as database config `CertificateReservedNodeCommonName`) | | `certificateAuthoritySecret` *[CertificateAuthoritySecret](#certificateauthoritysecret)* | No | Secret containing the CA TLS certificate. See below. | | `certificateSecret` *[CertificateSecret](#certificatesecret)* | Yes | Secret containing the TLS certificate to use. See below. | | `certificateSubjectName` *string* | No | Deprecated field. The value of this field is always ignored. | The operator takes special care when monitoring changes to security-related specs and values. Changes that disrupt inter-node communication, such as turning TLS on or off, require full restarts and unavoidable cluster down time. Changes to the subfields of `certificateAuthoritySecret` or `certificateSecret` typically require changing how pods are deployed and so require at least a rolling restart. Changes to the Secret named at `certificateAuthoritySecret.name` or `certificateSecret.name` may often be applied as config reloads, causing no downtime at all. This is how automatic TLS renewal always works. It is also possible to migrate CAs without triggering a full restart but you need to follow [this procedure][migrateca]. [migrateca]: ../operations/managing-certificates.md#migrating-certificate-authorities #### CertificateAuthoritySecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------| | `name` *string* | Yes | Name of the Secret holding the certificate details | | `keyName` *string* | No | Key within the Secret containing the CA certificate. If missing or empty, all keys in the Secret must be CA certs and will be trusted. | | `privateKeyName` *string* | No | Deprecated field. The value of this field is always ignored. | #### CertificateSecret | Field | Required | Description | |---------------------------|----------|------------------------------------------------------------------| | `name` *string* | Yes | Name of the Secret holding the certificate details | | `keyName` *string* | Yes | Key within the Secret containing the TLS certificate | | `privateKeyName` *string* | No | Key within the Secret containing the TLS certificate private key | #### KurrentDBUsersSpec | Field | Required | Description | |------------------------------------------------|----------|------------------------------------------------------| | adminPasswordSecret *[SecretKeySelector][u1]* | Yes | Secret containing initial password for `admin` user. | | opsPasswordSecret *[SecretKeySelector][u1]* | Yes | Secret containing initial password for `ops` user. | | customUsers *[KurrentDBUserSpec][u2] array* | No | Custom users to add to the database. | The `admin` and `ops` passwords are required if users are configured at all. Those passwords are set by initial database creation; when set, the database will never accept the default password (`changeit`). No deployment should be considered secure without configuring these two passwords. The additional users described in `customUsers` are optional, and are configured by the Operator after the first successful health check. The Operator does not currently support updates to the initial user configuration. The Secrets referenced here are not read after the first time the KurrentDB cluster reaches a healthy state, and may safely be deleted. [u1]: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#secretkeyselector-v1-core [u2]: #kurrentdbuserspec #### KurrentDBUserSpec | Field | Required | Description | |------------------------------------------|----------|----------------------------------------------------| | loginName *string* | Yes | The login name of the user. | | fullName *string* | Yes | The display name of the user. | | passwordSecret *[SecretKeySelector][u1]* | Yes | The Secret from which the password should be read. | | groups *string array* | No | Additional groups to add user to, see below. | Note that KurrentDB always adds every new user to a group matching its login name, so the groups listed in `.groups` are in addition to that default behavior. The Operator does not currently support updates to the initial user configuration. The Secrets referenced here are not read after the first time the KurrentDB cluster reaches a healthy state, and may safely be deleted. #### PodDisruptionBudgetsSpec A `PodDisruptionBudget` is created by the operator to protect the database pods from external tools, such as a node pool upgrade that might otherwise evict all KurrentDB nodes simultaneously, resulting in database downtime. Presently, the only configuration available is to disable it entirely, which is not recommended. | Field | Required | Description | |---------------------|----------|---------------------------------------------------------------------------------------------| | `disable` *boolean* | No | Disable the PodDisruptionBudget for this KurrentDB (not recommended). Defaults to `false`. | ## KurrentDBBackup This resource type is used to define a backup for an existing database deployment. :::important Resources of this type must be created within the same namespace as the target database cluster to backup. ::: ### API #### KurrentDBBackupSpec | Field | Required | Description | |----------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------| | `clusterName` *string* | Yes | Name of the source database cluster | | `nodeName` *string* | No | Specific node name within the database cluster to use as the backup. If unspecified, the leader is used. | | `volumeSnapshotClassName` *string* | No | The volume snapshot class to use. See [Volume Snapshot Class Selection][vsc]. | | `extraMetadata` *[KurrentDBBackupExtraMetadataSpec][b1]* | No | Additional annotations and labels for child resources. | | `ttl` *string* | No | A time-to-live for this backup. If unspecified, the TTL is treated as infinite. | [b1]: #kurrentdbbackupextrametadataspec The format of the `ttl` may be in years (`y`), weeks (`w`), days (`d`), hours (`h`), or seconds (`s`), or a combination like `1d12h` #### Volume Snapshot Class Selection The Operator creates VolumeSnapshots when creating backups and when scaling up a cluster. The Operator chooses the VolumeSnapshotClass by checking the following places, in order of decreasing precedence: 1. The backup-specific setting (for backups only): `KurrentDBBackup.spec.volumeSnapshotClassName` 2. The KurrentDB-wide setting: `KurrentDB.spec.volumeSnapshotClassName` 3. The Operator-wide setting: `operator.volumeSnapshotClassName` (in the helm chart) 4. The Kubernetes-wide [default VolumeSnapshotClass][k8s-default-vsc] If none of those are configured, operations requiring a VSC will fail. [k8s-default-vsc]: https://kubernetes.io/docs/concepts/storage/volume-snapshot-classes/ #### KurrentDBBackupExtraMetadataSpec | Field | Required | Description | |------------------------------------------------------------------|----------|---------------------------------------------------------------------------------------------| | All *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for all child resource types (currently only VolumeSnapshots). | | VolumeSnapshots *[ExtraMetadataSpec](#extrametadataspec)* | No | Extra annotations and labels for VolumeSnapshots. | ## KurrentDBBackupSchedule This resource type is used to define a schedule for creating database backups and retention policies. #### KurrentDBBackupScheduleSpec | Field | Required | Description | |------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| | `schedule` *string* | Yes | A CronJob-style schedule. See [Writing a CronJob Spec][s2]. | | `timeZone` *string* | No | A timezone specification. Defaults to `Etc/UTC`. | | `template` *[KurrentDBBackup][s1]* | Yes | A `KurrentDBBackup` template. | | `keep` *integer* | No | The maximum number of complete backups this schedule will accumulate before it prunes the oldest ones. If unset, there is no limit. | | `suspend` *boolean* | No | While true, pauses the creation of new backups for this schedule. | [s1]: #kurrentdbbackupspec [s2]: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#writing-a-cronjob-spec Note that the only metadata allowed in `template.metadata` is `name`, `labels`, and `annotations`. If `name` is provided, it will be extended with an index like `my-name-1` when creating backups, otherwise created backups will be based on the name of the schedule resource. ## Selecting an Image When selecting a KurrentDB image, you may choose from one of Kurrent's standard images: | Versions | Image | Link | |--------------------|----------------------------------------------------------|-------------| | 23.10.x to 24.10.x | `docker.eventstore.com/eventstore/eventstoredb-ee:X.Y.Z` | [link][old] | | 25.0.0 and greater | `docker.kurrent.io/kurrent-latest/kurrentdb:X.Y.Z` | [link][std] | Additionally, Kurrent offers Red Hat-certified KurrentDB images. These images have the additional property that they have `setcap cap_net_bind_service+ep` applied to the `kurrentd` binary inside the image, which allows them to be used in conjunction with setting `.spec.network.nodePort` to a privileged port, like 443. These same images without the Red Hat Certification (or official Red Hat sha256 checks) are available without a Red Hat account directly from Kurrent. This is useful if you want the `setcap`-enabled image but don't care about the Red Hat Certification. | Versions | Certified | Image | Link | |--------------------|-----------|----------------------------------------------------------|-------------| | 25.0.0 and greater | Yes | `registry.connect.redhat.com/kurrent-io/kurrentdb:X.Y.Z` | [link][rhc] | | 25.0.0 and greater | No | `docker.kurrent.io/kurrent-latest/kurrentdb-rhel8:X.Y.Z` | [link][pre] | [old]: https://cloudsmith.io/~eventstore/repos/eventstore/packages/?q=format%3Adocker+name%3Aeventstoredb-ee [std]: https://cloudsmith.io/~eventstore/repos/kurrent-latest/packages/?q=format%3Adocker+name%3Akurrentdb [rhc]: https://catalog.redhat.com/en/software/containers/kurrent-io/kurrentdb/690a11e9b1dedf2b2890ef75 [pre]: https://cloudsmith.io/~eventstore/repos/kurrent-latest/packages/?q=format%3Adocker+name%3Akurrentdb-rhel8 ## Configuring KurrentDB The [`KurrentDB.spec.configuration` yaml field](#kurrentdbspec) may contain any valid configuration values for KurrentDB. However, some values may be unnecessary, as the Operator provides some defaults, while other values may be ignored, as the Operator may override them. The Operator-defined default configuration values, which may be overridden by the user's `KurrentDB.spec.configuration` are: | Default Field | Default Value | |------------------------------|---------------| | DisableLogFile | true | | EnableAtomPubOverHTTP | true | | PrepareTimeoutMs | 3000 | | CommitTimeoutMs | 3000 | | GossipIntervalMs | 2000 | | GossipTimeoutMs | 5000 | | LeaderElectionTimeoutMs | 2000 | | ReplicationHeartbeatInterval | 1000 | | ReplicationHeartbeatTimeout | 2500 | | NodeHeartbeatInterval | 1000 | | NodeHeartbeatTimeout | 2500 | The Operator-managed configuration values, which take precedence over the user's `KurrentDB.spec.configuration`, are: | Managed Field | Value | |------------------------------| -------------------------------------------------------------| | Db | hard-coded volume mount point | | Index | hard-coded volume mount point | | Log | hard-coded volume mount point | | Insecure | true if `KurrentDB.spec.security.certificateSecret` is empty | | DiscoverViaDns | false (`GossipSeed` is used instead) | | AllowAnonymousEndpointAccess | true | | AllowUnknownOptions | true | | NodeIp | 0.0.0.0 (to accept traffic from outside pod) | | ReplicationIp | 0.0.0.0 (to accept traffic from outside pod) | | NodeHostAdvertiseAs | Derived from pod name | | ReplicationHostAdvertiseAs | Derived from pod name | | AdvertiseHostToClientAs | Derived from `KurrentDB.spec.network.fqdnTemplate` | | ClusterSize | Derived from `KurrentDB.spec.replicas` | | GossipSeed | Derived from pod list | | ReadOnlyReplica | Automatically set for ReadOnlyReplica and Archiver pods | | NodePort | Derived from `KurrentDB.spec.network.nodePort` | | ReplicationPort | Derived from `KurrentDB.spec.network.replicationPort` | | NodeTcpPort | Derived from `KurrentDB.spec.network.nodeTcpPort` | --- --- url: 'https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/index.md' --- # A number of operations can be performed with the Operator which are catalogued below: --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/advanced-networking.md --- # Advanced Networking KurrentDB is a clustered database, and all official KurrentDB clients are cluster-aware. As a result, there are times when a client will find out from one server how to connect to another server. To make this work, each server advertises how clients and other servers should contact it. The Operator lets you customize these advertisements. Such customizations are influenced by your cluster topology, where your KurrentDB clients will run, and also your security posture. This page will help you select the right networking and security configurations for your needs. ## Configuration Options This document is intended to help pick appropriate traffic strategies and certificate options for your situation. Let us first examine the range of possible settings for each. ### Traffic Strategy Options Servers advertise how they should be dialed by other servers according to the `KurrentDB.spec.network.internodeTrafficStrategy` setting, which is one of: * `"ServiceName"` (default): servers use each other's Kubernetes service name to contact each other. * `"FQDN"`: servers use each other's fully-qualified domain name (FQDN) to contact each other. * `"SplitDNS"`: servers advertise FQDNs to each other, but a tiny sidecar DNS resolver in each server pod intercepts the lookup of FQDNs for local pods and returns their actual pod IP address instead (the same IP address returned by the `"ServiceName"` setting). Servers advertise how they should be dialed by clients according to the `KurrentDB.spec.network.clientTrafficStrategy` setting, which is one of: * `"ServiceName"`: clients dial servers using the server's Kubernetes service name. * `"FQDN"` (default): clients dial servers using the server's FQDN. Note that the `"SplitDNS"` setting is not an option for the `clientTrafficStrategy`, simply because the KurrentDB Operator does not deploy your clients and so cannot inject a DNS sidecar container into your client pods. However, it is possible to write a [CoreDNS rewrite rule][rr] to accomplish a similar effect as `"SplitDNS"` but for client-to-server traffic. [rr]: https://coredns.io/2017/05/08/custom-dns-entries-for-kubernetes/ ### Certificate Options Except for test deployments, you always want to provide TLS certificates to your KurrentDB deployments. The reason is that insecure deployments disable not only TLS, but also all authentication and authorization features of the database. There are three basic options for how to obtain certificates: * Use self-signed certs: you can put any name in your self-signed certs, including Kubernetes service names, which enables `"ServiceName"` traffic strategies. A common technique is to use [cert-manager][cm] to manage the self-signed certificates and to use [trust-manager][tm] to distribute trust of those self-signed certificates to clients. * Use a publicly-trusted certificate provider: you can only put FQDNs on your certificate, which limits your traffic strategies to FQDN-based connections (`"FQDN"` or `"SplitDNS"`). * Use both: self-signed certs on the servers, plus an Ingress using certificates from a public certificate provider and configured for TLS termination. Note that at this time, the Operator does not assist with the creation of Ingresses. [cm]: https://cert-manager.io/ [tm]: https://cert-manager.io/docs/trust/trust-manager/ ## Considerations Now let us consider a few different aspects of your situation to help guide the selection of options. ### What are your security requirements? The choice of certificate provider has a security aspect to it. The KurrentDB servers use the certificate to authenticate each other, so anybody who has read access to the certificate or who can produce a matching, trusted certificate, can impersonate another server, and obtain full access to the database. The obvious implication of this is that access to the Kubernetes Secrets which contain server certificates should be limited to those who are authorized to administer the database. But it may not be obvious that if control of your domain's DNS configuration is shared by many business units in your organization, it may be the case that self-signed certificates with `internodeTrafficStrategy` of `"ServiceName"` provides the tightest control over database access. So your security posture may require that you choose one of: * self-signed certs and `"ServiceName"` traffic strategies, if all your clients are inside the Kubernetes cluster * self-signed certs on servers with `internodeTrafficStrategy` of `"ServiceName"` plus Ingresses configured with publicly-trusted certificate providers and `clientTrafficStrategy` of `"FQDN"` ### Where will your KurrentDB servers run? If any servers are not in the same Kubernetes cluster, for instance, if you are using the [standalone read-only-replica feature](database-deployment.md#deploying-standalone-read-only-replicas) to run a read-only replica in a second Kubernetes cluster from the quorum nodes, then you will need to pick from a few options to ensure internode connectivity: * `internodeTrafficStrategy` of `"SplitDNS"`, so every server connects to others by their FQDN, but when a connection begins to another pod in the same cluster, the SplitDNS feature will direct the traffic along direct pod-to-pod network interfaces. This solution assumes FQDNs on certificates, which enables you to use publicly trusted certificate authorities to generate certificates for each cluster, which can also ease certificate management. * `internodeTrafficStrategy` of `"ServiceName"`, plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster. This solution requires self-signed certificates, and also that the certificates on servers in both clusters are signed by the same self-signed Certificate Authority. [ens]: https://kubernetes.io/docs/concepts/services-networking/service/#externalname ### Where will your KurrentDB clients run? If any of your KurrentDB clients will run outside of Kubernetes, your `clientTrafficStrategy` must be `"FQDN"` to ensure connectivity. If your KurrentDB clients are all within Kubernetes, but spread through more than one Kubernetes cluster, you may use one of: * `clientTrafficStrategy` of `"FQDN"`. * `clientTrafficStrategy` of `"ServiceName"` plus manually-created [ExternalName Services][ens] in each Kubernetes cluster for each server in the other cluster(s), as described above. ### How bad are hairpin traffic patterns for your deployment? Hairpin traffic patterns occur when a pod inside a Kubernetes cluster connects to another pod in the same Kubernetes cluster through its public IP address rather than its pod IP address. The traffic moves outside of Kubernetes to the public IP then "hairpin" turns back into the cluster. For example, with `clientTrafficStrategy` of `"FQDN"`, clients connecting to a server inside the same cluster will not automatically connect directly to the server pod, even though they are both inside the Kubernetes cluster and that would be the most direct possible connection. Hairpin traffic patterns are never good, but they're also not always bad. You will need to evaluate the impact in your own environment. Consider some of the following possibilities: * In a cloud environment, sometimes internal traffic is cheaper than traffic through a public IP, so there could be a financial impact. * If the FQDN connects to, for example, an nginx ingress, then pushing Kubernetes-internal traffic through nginx may either over-burden your nginx instance or it may slow down your traffic unnecessarily. Between servers, hairpin traffic can always be avoided with an `internodeTrafficStrategy` of `"SplitDNS"`. For clients, one solution is to prefer a `clientTrafficStrategy` of `"ServiceName"`, or you may consider adding a [CoreDNS rewrite rule][rr]. ## Common Solutions With the above considerations in mind, let us consider a few common solutions. ### Everything in One Kubernetes Cluster When all your KurrentDB servers and clients are within a single Kubernetes cluster, life is easy: * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Use cert-manager to configure a certificate based on the KurrentDB service names. * Use trust-manager to configure clients to trust the self-signed certificates. This solution provides the highest possible security, avoids hairpin traffic patterns, and leverages Kubernetes-native tooling to ease the pain of self-signed certificate management. ### Servers Anywhere, Clients Anywhere If using publicly trusted certificates is acceptable (see [above](#what-are-your-security-requirements)), almost every need can be met with one of the simplest configurations: * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Use cert-manager to automatically create certificates through an ACME provider like LetsEncrypt. * If clients may be outside of Kubernetes or multiple Kubernetes clusters are in play, set `KurrentDB.spec.network.loadBalancer.enabled` to `true`, making your servers publicly accessible. This solution is still highly secure, provided your domain's DNS management is tightly controlled. It also supports virtually every server and client topology. Server hairpin traffic never occurs and client hairpin traffic — if a problem — can be addressed with a [CoreDNS rewrite rule][rr]. ### Multiple Kubernetes Clusters and a VPC Peering If you want all your KurrentDB resources within private networking for extra security, but also need to support multiple Kubernetes clusters in different regions, you can set up a VPC Peering between your clusters and configure your inter-cluster traffic to use it. There could be many variants of this solution; we'll describe one based on ServiceNames and one based on FQDNs. #### ServiceName-based Variant * Set `internodeTrafficStrategy` to `"ServiceName"`. * Set `clientTrafficStrategy` to `"ServiceName"`. * Ensure that each server has an IP address in the VPC Peering. * In each Kubernetes cluster, manually configure [ExternalName Services][ens] for each server not in that cluster. ExternalName Services can only redirect to hostnames, not bare IP addresses, so you may need to ensure that there is a DNS name to resolve each server's IP address in the VPC Peering. * Use self-signed certificates, and make sure to use the same certificate authority to sign certificates in each cluster. #### FQDN-based Variant * Set `internodeTrafficStrategy` to `"SplitDNS"`. * Set `clientTrafficStrategy` to `"FQDN"`. * Ensure that each server has an IP address in the VPC Peering. * Ensure that each server's FQDN resolves to the IP address of that server in the VPC peering. * If client-to-server hairpin traffic within each Kubernetes cluster is a problem, add a [CoreDNS rewrite rule][rr] to each cluster to prevent it. * Use a publicly-trusted certificate authority to create certificates based on the FQDN. They may be generated per-Kubernetes cluster independently, since the certificate trust will be automatic. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/database-backup.md --- # Database Backup The sections below detail how database backups can be performed. Refer to the [KurrentDBBackup API](../getting-started/resource-types.md#kurrentdbbackup) for detailed information. ## Backing up the leader Assuming there is a cluster called `mydb` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: mydb namespace: kurrent spec: volumeSnapshotClassName: ebs-vs clusterName: mydb ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster/Cloud provider, please consult with your Kubernetes administrator to determine this value. The `KurrentDBBackup` type takes an optional `nodeName`. If left blank, the leader will be derived based on the gossip state of the database cluster. ## Backing up a specific node Assuming there is a cluster called `mydb` that resides in the `kurrent` namespace, the following `KurrentDBBackup` resource can be defined: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: mydb namespace: kurrent spec: volumeSnapshotClassName: ebs-vs clusterName: mydb nodeName: mydb-1 ``` In the example above, the backup definition leverages the `ebs-vs` volume snapshot class to perform the underlying volume snapshot. This class name will vary per Kubernetes cluster, please consult with your Kubernetes administrator to determine this value. ## Restoring from a backup A `KurrentDB` cluster can be restored from a backup by specifying an additional field `sourceBackup` as part of the cluster definition. For example, if an existing `KurrentDBBackup` exists called `mydb-backup`, the following snippet could be used to restore it: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 sourceBackup: mydb-backup resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 512Mi network: domain: kurrent.test loadBalancer: enabled: true ``` ## Automatically delete backups with a TTL A TTL can be set on a backup to delete the backup after a certain amount of time has passed since its creation. For example, to delete the backup 5 days after it was created: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackup metadata: name: mydb namespace: kurrent spec: volumeSnapshotClassName: ebs-vs clusterName: mydb ttl: 5d ``` ## Scheduling Backups A `KurrentDBBackupSchedule` can be created with a CronJob-like schedule. Schedules also support a `.spec.keep` setting to automatically limit how many backups created by that schedule are retained. Using a schedule with `.keep` is slightly safer than using TTLs on the individual backups. This is because if, for some reason, you ceased to be able to create new backups, the TTL will continue to delete backups until you have none left, while in the same situation .keep would leave all your old snapshots in place until a new one could be created. For example, to create a new backup every midnight (UTC), and to keep the last 7 such backups at any time, you could create a `KurrentDBBackupSchedule` resource like this: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-backup-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: metadata: name: my-backup spec: volumeSnapshotClassName: ebs-vs clusterName: mydb keep: 7 ``` --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/database-deployment.md --- # Example Deployments This page shows various deployment examples of KurrentDB, starting with a [full example](#recommended-production-settings) showing Kurrent's recommended production settings, followed by [incremental examples](#from-zero-to-prod) to build up to that, followed by [additional examples](#additional-examples) illustrating other supported features and topologies. ## Recommended Production Settings Kurrent recommends all of the following settings for a production KurrentDB deployment: * Multiple quorum nodes for high availability. * TLS enabled with self-signed or LetsEncrypt certificates (self-signed is shown here) * `admin` and `ops` users are configured with non-default passwords * Enterprise features of KurrentDB are enabled. * A backup schedule automates regular backups. * A `StorageClass` with `reclaimPolicy: Retain` is configured for the database * A `VolumeSnapshotClass` exists for backups and scale-up operations. Also see [From Zero To Prod](#from-zero-to-prod) for incremental examples to get here. ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ## From Zero to Prod This section illustrates how to build up from the simplest possible "Hello, World" deployment in incremental steps. Each step shows the diff that is applied in that step as well as the full manifest up to that point. ### Configure a `StorageClass` *This is step 1 of 8 in the "From Zero To Prod" series of examples.* Configure `StorageClass` first because you can't change the StorageClass for the PersistentVolumes behind your database without deleting and recreating it. This example illustrates a `StorageClass` in Azure: * It uses `reclaimPolicy: Retain` to preserve your database disks as a layer of protection against data loss (in case you delete the `KurrentDB` accidentally). * It uses `allowVolumeExpansion: true` to allow you to grow disks without downtime if KurrentDB runs out of disk space in the future. * (Azure-specific): It uses `parameters.skuName: Premium_LRS` to select high-performance, low-latency disk for your database. ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer ``` ### "Hello, World" (single node insecure cluster) *This is step 2 of 8 in the "From Zero To Prod" series of examples.* This section illustrates a single-node insecure cluster. It uses the `StorageClass` you just created. ::: tabs @tab Diff Add a `KurrentDB` and reference the `StorageClass` you created above. ```diff apiVersion: storage.k8s.io/v1 kind: StorageClass ... +--- +apiVersion: kubernetes.kurrent.io/v1 +kind: KurrentDB +metadata: + name: mydb + namespace: kurrent +spec: + replicas: 1 + image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 + resources: + requests: + cpu: 4000m + memory: 16Gi + storage: + volumeMode: "Filesystem" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi + storageClassName: my-storage-class + network: + domain: mydomain.tld + fqdnTemplate: '{podName}.{domain}' ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' ``` ::: ### Configuring Your `VolumeSnapshotClass` *This is step 3 of 8 in the "From Zero To Prod" series of examples.* You will also want to configure a `VolumeSnapshotClass` (unless your Kubernetes cluster has a default configured and you want to use that). The `VolumeSnapshotClass` is used for both backups and when increasing the number of nodes in the cluster. ::: tabs @tab Diff Add a `VolumeSnapshotClass` and reference it in your `KurrentDB`. ```diff apiVersion: storage.k8s.io/v1 kind: StorageClass ... +--- +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshotClass +metadata: + name: my-volume-snapshot-class +driver: disk.csi.azure.com +deletionPolicy: Delete --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... + volumeSnapshotClassName: my-volume-snapshot-class ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' volumeSnapshotClassName: my-volume-snapshot-class ``` ::: ### Unlocking Enterprise Features *This is step 4 of 8 in the "From Zero To Prod" series of examples.* Take advantage of the awesome enterprise features of our database, like auto-scavenging and archiving! Note that this is a distinct license from the Operator license you provided during the Helm installation. ::: tabs @tab Diff Add a `Secret` with your database license key and reference it in your `KurrentDB`. ```diff apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass ... +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-license-secret + namespace: kurrent +type: Opaque +stringData: + licenseKey: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... + licenseSecret: + name: my-license-secret + key: licenseKey ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 1 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' licenseSecret: name: my-license-secret key: licenseKey volumeSnapshotClassName: my-volume-snapshot-class ``` ::: ### Enabling High Availability *This is step 5 of 8 in the "From Zero To Prod" series of examples.* Change your `KurrentDB.spec.replicas` to multiple nodes (3 or 5) to enable high availability. The Operator can even make this change safely to an existing cluster with minimal downtime. ::: tabs @tab Diff Just change your `.replicas`! ```diff apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... - replicas: 1 + replicas: 3 ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' licenseSecret: name: my-license-secret key: licenseKey volumeSnapshotClassName: my-volume-snapshot-class ``` ::: ### Enabling TLS (self-signed) *This is step 6 of 8 in the "From Zero To Prod" series of examples.* Before deploying this cluster, be sure to follow the steps in [Using Self-Signed Certificates](managing-certificates.md#using-self-signed-certificates). Most examples on this page assume self-signed certificates, but if you prefer a publicly-trusted certificate authority like LetsEncrypt, you can follow [the LetsEncrypt example](#enabling-tls-letsencrypt) instead of this step. ::: tabs @tab Diff Add the `cert-manager`-related objects to create a CA and leaf certificates. Reference the CA and TLS key Secrets in your `KurrentDB`. This example uses Service names as `dnsNames` on the `Certificate`, so it also configures both `internodeTrafficStrategy` and `clientTrafficStrategy` to `ServiceName`. See [Advanced Networking](./advanced-networking.md) for details. ```diff +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer + namespace: kurrent +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: selfsigned-ca + namespace: kurrent +spec: + isCA: true + commonName: myca + subject: + organizations: + - Kurrent + organizationalUnits: + - Cloud + secretName: myca-tls + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + issuerRef: + name: selfsigned-issuer + kind: Issuer + group: cert-manager.io +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: myca-issuer + namespace: kurrent +spec: + ca: + secretName: myca-tls +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: mydb + namespace: kurrent +spec: + secretName: mydb-tls + isCA: false + usages: + - client auth + - server auth + - digital signature + - key encipherment + commonName: kurrentdb-node + subject: + organizations: + - Kurrent + organizationalUnits: + - Cloud + dnsNames: + - '*.mydb.kurrent.svc.cluster.local' + - '*.mydb-replica.kurrent.svc.cluster.local' + - '*.mydb-archiver.kurrent.svc.cluster.local' + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + issuerRef: + name: myca-issuer + kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... network: ... + internodeTrafficStrategy: ServiceName + clientTrafficStrategy: ServiceName + security: + certificateReservedNodeCommonName: kurrentdb-node + certificateAuthoritySecret: + name: myca-tls + keyName: ca.crt + certificateSecret: + name: mydb-tls + keyName: tls.crt + privateKeyName: tls.key ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey volumeSnapshotClassName: my-volume-snapshot-class ``` ::: ### Configuring Users *This is step 7 of 8 in the "From Zero To Prod" series of examples.* ::: tabs @tab Diff Add a secret with the desired `admin` and `ops` passwords (and any other custom users you wish to create) and reference it in your `KurrentDB`. ```diff +--- +apiVersion: v1 +kind: Secret +metadata: + name: my-passwords + namespace: kurrent +type: Opaque +stringData: + admin: + ops: + custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... + users: + adminPasswordSecret: + name: my-passwords + key: admin + opsPasswordSecret: + name: my-passwords + key: ops + customUsers: + - loginName: custom + fullName: Custom + passwordSecret: + name: my-passwords + key: custom + groups: + - $admins ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class ``` ::: ### Scheduling Backups *This is the final step in the "From Zero To Prod" series of examples.* ::: tabs @tab Diff Just add a `KurrentDBBackupSchedule` with a `CronJob`-like time specification. Here's an example of that takes nightly backups (at midnight UTC) and keeps the latest 7 backups. ```diff --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent +--- +apiVersion: kubernetes.kurrent.io/v1 +kind: KurrentDBBackupSchedule +metadata: + name: my-schedule + namespace: kurrent +spec: + schedule: "0 0 * * *" + timeZone: Etc/UTC + template: + spec: + clusterName: mydb + keep: 7 ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ::: ## Additional Examples This section illustrates additional Operator features to help you reach your desired topology and configuration. Diffs below are relative to the [Recommended Production Settings](#recommended-production-settings) unless otherwise stated. ### Deploying Read-Only Replica Nodes Additional read-only replica nodes can increase the read throughput of your cluster. Note that read-only replicas are only allowed in clustered configurations (where `spec.replicas > 1`). Read-only replica nodes may be configured with different settings than the quorum nodes. See [KurrentDBReadOnlyReplicasSpec](../getting-started/resource-types.md#kurrentdbreadonlyreplicasspec) for details. ::: tabs @tab Diff Just add readOnlyReplicas.replicas! ```diff apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... replicas: 3 + readOnlyReplicas: + replicas: 2 ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 readOnlyReplicas: replicas: 2 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ::: ### Deploying An Archiver Node Enabling an Archiver node in your cluster allows you to keep your "hot" recent data on local, high-performance disk, while sending your "cold" older data to cheap blob storage. This can be a boon for certain applications. Archiver nodes are a special case of read-only replicas, so like all read-only replicas, an archiver can only be enabled in a clustered configuration (`.spec.replicas > 1`). Also, all nodes in the cluster (not just the archiver) need access to the blob storage backend. For production deployments, we recommend configuring IRSA for your cloud (see docs for [AWS][irsaaws], [Azure][irsaazure], and [GCP][irsagcp]), as this provides the best security and lets the cloud provider manage the credentials, but in test clusters it may be sufficient to use the `KurrentDB.spec.environmentSecret` to pass cloud credentials into the KurrentDB pods as environment variables. [irsaaws]: https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html [irsaazure]: https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster [irsagcp]: https://docs.cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#kubernetes-sa-to-iam ::: tabs @tab Diff * Enable archiving for all nodes (in `.configuration`) * Enable the archiver node itself (`.archiver.enabled`) * Deploy with suitable blob storage credentials (this example assumes [Azure IRSA][irsaazure]) ```diff +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: my-irsa-service-account + namespace: kurrent + annotations: + azure.workload.identity/client-id: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 + archiver: + enabled: true ... + configuration: + Archive: + Enabled: true + RetainAtLeast: + Days: 0 + LogicalBytes: 1073741824 # 1GiB + StorageType: S3 + S3: + Region: us-west-1 + Bucket: my-bucket + serviceAccountName: my-irsa-service-account + extraMetadata: + pods: + labels: + azure.workload.identity/use: "true" ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: v1 kind: ServiceAccount metadata: name: my-irsa-service-account namespace: kurrent annotations: azure.workload.identity/client-id: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 archiver: enabled: true image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey configuration: Archive: Enabled: true RetainAtLeast: Days: 0 LogicalBytes: 1073741824 # 1GiB StorageType: S3 S3: Region: us-west-1 Bucket: my-bucket serviceAccountName: my-irsa-service-account extraMetadata: pods: labels: azure.workload.identity/use: "true" users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ::: ### Deploying With Scheduling Constraints The pods created for a KurrentDB resource can be configured with any of the constraints commonly applied to pods: * [Node Selectors][ns] * [Affinity and Anti-Affinity][aa] * [Topology Spread Constraints][ts] * [Tolerations][tl] * [Node Name][nn] If no scheduling constraints are configured, the Operator sets a default soft constraint configuring pod anti-affinity such that multiple replicas will prefer to run on different nodes, for better fault tolerance. In cloud deployments, you may want to maximize uptime by asking each replica of a KurrentDB cluster to be deployed in a different availability zone. The following KurrentDB resource does that, and also requires KurrentDB to schedule pods onto nodes labeled with `machine-size:large`: [ns]: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector [aa]: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity [ts]: https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/ [tl]: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ [nn]: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename ::: tabs @tab Diff Just add constraints to your `KurrentDB`: ```diff apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... + constraints: + nodeSelector: + machine-size: large + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: zone + labelSelector: + matchLabels: + app.kubernetes.io/part-of: kurrentdb-operator + app.kubernetes.io/name: mydb + whenUnsatisfiable: DoNotSchedule ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class constraints: nodeSelector: machine-size: large topologySpreadConstraints: - maxSkew: 1 topologyKey: zone labelSelector: matchLabels: app.kubernetes.io/part-of: kurrentdb-operator app.kubernetes.io/name: mydb whenUnsatisfiable: DoNotSchedule network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ::: ### Custom Database Configuration If custom parameters are required in the underlying database configuration then these can be specified using the `configuration` YAML block within a `KurrentDB`. The parameters which are defaulted or overridden by the operator are listed [in the CRD reference](../getting-started/resource-types.md#configuring-kurrentdb). ::: tabs @tab Diff Just add a `configuration` to your `KurrentDB`: ```diff --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... + configuration: + RunProjections: all + StartStandardProjections: true ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: myca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: myca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: myca-issuer namespace: kurrent spec: ca: secretName: myca-tls --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: kurrentdb-node subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydb.kurrent.svc.cluster.local' - '*.mydb-replica.kurrent.svc.cluster.local' - '*.mydb-archiver.kurrent.svc.cluster.local' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: myca-issuer kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 configuration: RunProjections: all StartStandardProjections: true resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' internodeTrafficStrategy: ServiceName clientTrafficStrategy: ServiceName security: certificateReservedNodeCommonName: kurrentdb-node certificateAuthoritySecret: name: myca-tls keyName: ca.crt certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` ::: ### Enabling TLS (LetsEncrypt) Instead of self-signed certificates, you may wish to configure certificates through a public Certificate Authority, like LetsEncrypt. ::: note You will need to configure your `cert-manager` installation to authenticate with your DNS provider for automatic certificate issuance. `cert-manager` documents how to integrate with [each of their supported DNS providers][cm-dns01], and this example assumes you have followed their [integration with AzureDNS][cm-az]. ::: [cm-dns01]: https://cert-manager.io/docs/configuration/acme/dns01/ [cm-az]: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers :::: tabs @tab Diff ::: note For readability, this diff omits the removal of lines from the [Enabling TLS (self-signed)](#enabling-tls-self-signed) step. ::: Add an `Issuer` that connects cert-manager to your DNS provider and to LetsEncrypt. Add a `Certificate` to be generated by LetsEncrypt. Reference the TLS key Secret in your `KurrentDB`. Configure `.network.loadBalancer.enabled: true` so your external clients can access your KurrentDB according to its `fqdnTemplate`. Configure `internodeTrafficStrategy` to `SplitDNS` so inter-node traffic avoids hairpin traffic patterns (see [Advanced Networking](./advanced-networking.md) for detail). Also configure `clientTrafficStrategy` to `FQDN` so that server nodes advertise themselves to clients according to the `fqdnTemplate` setting. ```diff +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt + namespace: kurrent +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: + privateKeySecretRef: + name: letsencrypt-issuer-key + solvers: + - dns01: + azureDNS: + hostedZoneName: + resourceGroupName: + subscriptionID: + environment: AzurePublicCloud + managedIdentity: + clientID: +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: mydb + namespace: kurrent +spec: + secretName: mydb-tls + isCA: false + usages: + - client auth + - server auth + - digital signature + - key encipherment + commonName: '*.mydomain.tld' + subject: + organizations: + - Kurrent + organizationalUnits: + - Cloud + dnsNames: + - '*.mydomain.tld' + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + issuerRef: + name: letsencrypt + kind: Issuer --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: ... network: ... + loadBalancer: + enabled: true + internodeTrafficStrategy: SplitDNS + clientTrafficStrategy: FQDN + security: + certificateReservedNodeCommonName: '*.mydomain.tld' + certificateSecret: + name: mydb-tls + keyName: tls.crt + privateKeyName: tls.key ``` @tab Full Manifest ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class provisioner: disk.csi.azure.com reclaimPolicy: Retain allowVolumeExpansion: true parameters: skuName: Premium_LRS volumeBindingMode: WaitForFirstConsumer --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: my-volume-snapshot-class driver: disk.csi.azure.com deletionPolicy: Delete --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt namespace: kurrent spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: privateKeySecretRef: name: letsencrypt-issuer-key solvers: - dns01: azureDNS: hostedZoneName: resourceGroupName: subscriptionID: environment: AzurePublicCloud managedIdentity: clientID: --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.mydomain.tld' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydomain.tld' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer --- apiVersion: v1 kind: Secret metadata: name: my-license-secret namespace: kurrent type: Opaque stringData: licenseKey: --- apiVersion: v1 kind: Secret metadata: name: my-passwords namespace: kurrent type: Opaque stringData: admin: ops: custom: --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi storageClassName: my-storage-class network: domain: mydomain.tld fqdnTemplate: '{podName}.{domain}' loadBalancer: enabled: true internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.mydomain.tld' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key licenseSecret: name: my-license-secret key: licenseKey users: adminPasswordSecret: name: my-passwords key: admin opsPasswordSecret: name: my-passwords key: ops customUsers: - loginName: custom fullName: Custom passwordSecret: name: my-passwords key: custom groups: - $admins volumeSnapshotClassName: my-volume-snapshot-class --- apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDBBackupSchedule metadata: name: my-schedule namespace: kurrent spec: schedule: "0 0 * * *" timeZone: Etc/UTC template: spec: clusterName: mydb keep: 7 ``` :::: ### Deploying Standalone Read-Only Replicas ::: note This is an advanced example. Also read: [Advanced Networking](./advanced-networking.md). ::: This example illustrates a topology where a pair of read-only replicas is deployed in a different Kubernetes cluster than where the quorum nodes are deployed. We make the following assumptions: * LetsEncrypt certificates are used everywhere, to ease certificate management * LoadBalancers are enabled to ensure each node is accessible through its FQDN * `internodeTrafficStrategy` is `"SplitDNS"` to avoid hairpin traffic patterns between servers * the quorum nodes will have `-qn` suffixes in their FQDN while the read-only replicas will have `-rr` suffixes A LetsEncrypt `Issuer` and `Certificate` should be deployed in **both** clusters. This example assumes you have followed their [integration with AzureDNS][cm-az]: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt namespace: kurrent spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: privateKeySecretRef: name: letsencrypt-issuer-key solvers: - dns01: azureDNS: hostedZoneName: resourceGroupName: subscriptionID: environment: AzurePublicCloud managedIdentity: clientID: --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: mydb namespace: kurrent spec: secretName: mydb-tls isCA: false usages: - client auth - server auth - digital signature - key encipherment commonName: '*.mydomain.tld' subject: organizations: - Kurrent organizationalUnits: - Cloud dnsNames: - '*.mydomain.tld' privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: letsencrypt kind: Issuer ``` This `KurrentDB` resource defines the quorum nodes in one cluster: ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 3 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi network: domain: mydomain.tld loadBalancer: enabled: true fqdnTemplate: '{podName}-qn.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.mydomain.tld' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` And this `KurrentDB` resource defines the standalone read-only replicas in another cluster. Notice that: * `.replicas` is 0, but `.quorumNodes` is set instead * `.readOnlyReplicas.replicas` is set * `fqdnTemplate` differs slightly from above ```yaml apiVersion: kubernetes.kurrent.io/v1 kind: KurrentDB metadata: name: mydb namespace: kurrent spec: replicas: 0 quorumNodes: - mydb-0-qn.mydomain.tld:2113 - mydb-1-qn.mydomain.tld:2113 - mydb-2-qn.mydomain.tld:2113 readOnlyReplicas: replicas: 2 image: docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1 resources: requests: cpu: 4000m memory: 16Gi storage: volumeMode: "Filesystem" accessModes: - ReadWriteOnce resources: requests: storage: 100Gi network: domain: mydomain.tld loadBalancer: enabled: true fqdnTemplate: '{podName}-rr.{domain}' internodeTrafficStrategy: SplitDNS clientTrafficStrategy: FQDN security: certificateReservedNodeCommonName: '*.mydomain.tld' certificateSecret: name: mydb-tls keyName: tls.crt privateKeyName: tls.key ``` ## Accessing Deployments ### External The Operator will create one service of type `LoadBalancer` per KurrentDB node when the `spec.network.loadBalancer.enabled` flag is set to `true`. Each `LoadBalancer`-type Service is annotated with `external-dns.alpha.kubernetes.io/hostname` set according to the `fqdnTemplate` setting to allow the third-party tool [ExternalDNS][xdns] to configure external access. [xdns]: https://github.com/kubernetes-sigs/external-dns ### Internal The Operator will create headless services to access a KurrentDB cluster internally. This includes: * One for the underlying statefulset (selects all pods) * One per pod in the statefulset to support `Ingress` rules that require one target endpoint --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/managing-certificates.md --- # Managing Certificates The Operator expects consumers to leverage a third-party tool to generate TLS certificates that can be wired in to [KurrentDB](../getting-started/resource-types.md#kurrentdb) deployments using secrets. The sections below describe how certificates can be generated using popular vendors. ## Picking certificate names Each node in each KurrentDB cluster you create will advertise a fully-qualified domain name (FQDN). Clients will expect those advertised names to match the names you configure on your TLS certificates. You will need to understand how the FQDN is calculated for each node in order to request a TLS certificate that is valid for each node of your KurrentDB cluster. By default, the [network.fqdnTemplate field of your KurrentDB spec](../getting-started/resource-types.md#kurrentdbnetwork) is `{podName}.{name}{nodeTypeSuffix}.{domain}`, which may require multiple wildcard names on your certificate, like both `*.myName.myDomain.com` and `*.myName-replica.myDomain.com`. You may prefer to instead configure an `fqdnTemplate` like `{podName}.{domain}`, which could be covered by a single wildcard: `*.myDomain.com`. ## Certificate Manager (cert-manager) ### Prerequisites Before following the instructions in this section, these requirements should be met: * [cert-manager](https://cert-manager.io) is installed * You have the required permissions to create/manage new resources on the Kubernetes cluster * The following CLI tools are installed and configured to interact with your Kubernetes cluster. This means the tool must be accessible from your shell's `$PATH`, and your `$KUBECONFIG` environment variable must point to the correct Kubernetes configuration file: * [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl) * [k9s](https://k9scli.io/topics/install/), optional ### Using trusted certificates via LetsEncrypt To use LetsEncrypt certificates with KurrentDB, follow these steps: 1. Create a [LetsEncrypt Issuer](#letsencrypt-issuer) 2. Future certificates should be created using the `letsencrypt` issuer ### LetsEncrypt Issuer `cert-manager` documents how to integrate with [each of their supported DNS providers][cm-dns01], and the following example shows how a LetsEncrypt Issuer can be deployed after you have [integrated cert-manager with AzureDNS][cm-az]: [cm-dns01]: https://cert-manager.io/docs/configuration/acme/dns01/ [cm-az]: https://cert-manager.io/docs/configuration/acme/dns01/#supported-dns01-providers ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: letsencrypt namespace: kurrent spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: privateKeySecretRef: name: letsencrypt-issuer-key solvers: - dns01: azureDNS: hostedZoneName: resourceGroupName: subscriptionID: environment: AzurePublicCloud managedIdentity: clientID: ``` This can be deployed using the following steps: * Replace the variables `<...>` with the appropriate values * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl apply -f issuer.yaml ``` ### Using Self-Signed certificates To use self-signed certificates with KurrentDB, follow these steps: 1. Create a [Self-Signed Issuer](#self-signed-issuer) 2. Create a [Self-Signed Certificate Authority](#self-signed-certificate-authority) 3. Create a [Self-Signed Certificate Authority Issuer](#self-signed-certificate-authority-issuer) 4. Future certificates should be created using the `ca-issuer` issuer ### Self-Signed Issuer The following example shows how a self-signed issuer can be deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: selfsigned-issuer namespace: kurrent spec: selfSigned: {} ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `issuer.yaml` * Run the following command: ```bash kubectl apply -f issuer.yaml ``` ### Self-Signed Certificate Authority The following example shows how a self-signed certificate authority can be generated once a [self-signed issuer](#self-signed-issuer) has been deployed: ```yaml apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: selfsigned-ca namespace: kurrent spec: isCA: true commonName: ca subject: organizations: - Kurrent organizationalUnits: - Cloud secretName: ca-tls privateKey: algorithm: RSA encoding: PKCS1 size: 2048 issuerRef: name: selfsigned-issuer kind: Issuer group: cert-manager.io ``` :::note The values for `subject` should be changed to reflect what you require. ::: This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f ca.yaml ``` ### Self-Signed Certificate Authority Issuer The following example shows how a self-signed certificate authority issuer can be generated once a [CA certificate](#self-signed-certificate-authority) has been created: ```yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: name: ca-issuer namespace: kurrent spec: ca: secretName: ca-tls ``` This can be deployed using the following steps: * Copy the YAML snippet above to a file called `ca-issuer.yaml` * Ensure that the `kurrent` namespace has been created * Run the following command: ```bash kubectl apply -f ca-issuer.yaml ``` Once this step is complete, future certificates can be generated using the self-signed certificate authority. Using k9s, the following issuers should be visible in the `kurrent` namespace: ![Issuers](images/certs/ca-issuer.png) Describing the issuer should yield: ![Issuers](images/certs/ca-issuer-details.png) ## Migrating Certificate Authorities If for some reason you need to migrate Certificate Authorities (for example, switching from one self-signed CA to another), the process follows three steps: 1. Update your `KurrentDB.spec.security.certificateAuthoritySecret`: * First, ensure the named Secret contains only CAs; the database will not start if you have your TLS keys or other contents in the same Secret. * Add your new CA as a new key to the Secret, leaving the old CA in place. * Remove the `.keyName` field to indicate you want all keys of the Secret mounted into database pods as trusted CAs. * Wait for the KurrentDB resource to return to the `database-healthy` state. * Handle possible race condition; see below. 2. Update your `KurrentDB.spec.security.certificateSecret`: * Switch to the TLS keypair signed by the new CA. You may either configure your KurrentDB to reference a new Secret (which will cause a rolling restart) or update the content of the currently-referenced Secret (which will cause a config reload, no node downtime). * Wait for the KurrentDB resource to return to the `database-healthy` state. 3. Update your `KurrentDB.spec.security.certificateAuthoritySecret` again: * Remove the old CA you no longer wish to trust. * Restore the `.keyName` if desired. * Wait for the KurrentDB resource to return to the `database-healthy` state. During step 1, the Operator requests the mounted pod secrets to be resynced, then waits 10 seconds, then requests the database pods to reload their configs (including trusted CA list). If the kubelet is under very heavy load, it may not have actually synced the secrets into the pods by the time the database reloads. For TLS key pair changes, the Operator can detect and remediate that race condition. However, with CA list changes, the Operator cannot tell if the race occurred. The race condition is rare, however, if it occurs and it is unaddressed, then step 2 will result in a full restart, which is unnecessary downtime. To guarantee no unnecessary downtime, you may take one of the following actions: * You can exec into the pods, confirm that your updates appear in `/kurrentdb/ca` directory (or `/eventstore/ca` for older database versions), then trigger another config reload by bumping the `.spec.configReloadKey` string to any new value. * You can simply wait a few minutes then bump the `.spec.configReloadKey`. How long you need to wait is governed by the kubelet's `syncFrequency` and `configMapAndSecretChangeDetectionStrategy` strategy. For default kubelet settings, you need to wait at least one minute. * If you have multiple nodes, you may issue an immediate rolling restart by bumping the `.spec.rollingRestartKey` string to any new value. When nodes restart they are guaranteed to mount the latest secrets. --- --- url: >- https://docs.kurrent.io/server/kubernetes-operator/v1.6.0/operations/modify-deployments.md --- # Modify Deployments Updating KurrentDB deployments through the Operator is done by modifying the KurrentDB Custom Resources (CRs) using standard Kubernetes tools. Most updates are processed almost immediately, but there is special logic in place around resizing the number of replicas in a cluster. ## Applying Updates `KurrentDB` instances support updates to: * Container Image * Memory * CPU * Volume Size (increases only) * Replicas (node count) * Configuration To update the specification of a `KurrentDB` instance, simply issue a patch command via the kubectl tool. In the examples below, the cluster name is `mydb`. Once patched, the Operator will take care of augmenting the underlying resources, which will cause database pods to be recreated. ### Container Image ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"image": "docker.kurrent.io/kurrent-latest/kurrentdb:26.0.1"}}' ``` ### Memory ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"resources": {"requests": {"memory": "2048Mi"}}}}' ``` ### CPU ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"resources": {"requests": {"cpu": "2000m"}}}}' ``` ### Volume Size ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"storage": {"resources": {"requests": {"storage": "2048Mi"}}}}}' ``` ### Replicas ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"replicas": 3}}' ``` Note that the actual count of replicas in a cluster may take time to update. See [Updating Primary Replica Count](#updating-primary-replica-count), below. ### Configuration ```bash kubectl -n kurrent patch kurrentdb mydb --type=merge -p '{"spec":{"configuration": {"RunProjections": "all", "StartStandardProjections": true}}}' ``` ## Updating Primary Replica Count A user configures the KurrentDB cluster by setting the `.spec.replicas` setting of a KurrentDB resource. The current actual number of replicas can be observed as `.status.replicas`. The process to grow or shrink the replicas in a cluster safely requires carefully stepping the KurrentDB cluster through a series of consensus states, which the Operator handles automatically. In both cases, if the resizing flow gets stuck for some reason, you can cancel the resize by setting `.spec.replicas` back to its original value. ### Upsizing a KurrentDB Cluster The steps that the Operator takes to go from 1 to 3 nodes in a KurrentDB cluster are: * Take a VolumeSnapshot of pod 0 (the initial pod). * Reconfigure pod 0 to expect a three-node cluster. * Start a new pod 1 from the VolumeSnapshot. * Wait for pod 0 and pod 1 to establish quorum. * Start a new pod 2 from the VolumeSnapshot. Note that the database cannot process writes between the time that the Operator reconfigures pod 0 for a three-node cluster and when pod 0 and pod 1 establish quorum. The purpose of the VolumeSnapshot is to greatly reduce the amount of replication pod 1 must do from pod 0 before quorum is established, which greatly reduce the amount of downtime during the resize. ### Downsizing a KurrentDB Cluster The steps that the Operator takes to go from 3 nodes to 1 in a KurrentDB cluster are: * Make sure pod 0 and pod 1 are caught up with the leader (which may be one of them). * Stop pod 2. * Wait for quorum to be re-established between pods 0 and 1. * Stop pod 1. * Reconfigure pod 0 as a one-node cluster. Note that the database cannot process writes briefly after the Operator stops pod 2, and again briefly after the Operator reconfigures pod 0. :::important It is technically possible for data loss to occur when the Operator stops pod 2 if there are active writes against the database, and either of the other two pods happen to fail at approximately the same time pod 2 stops. The frequency of an environment failure should hopefully be low enough that this is not a realistic concern. However, to reduce the risk to truly zero, you must ensure that there are no writes against the database at the time when you downsize your cluster. ::: ## Updating Read-Only Replica Count Since read-only replica nodes are not electable as leaders, it is simpler to increase or decrease the number of running read-only replicas. Still, when adding new read-only replicas, the Operator uses VolumeSnapshots to expedite the initial catch-up reads for new read-only replicas. The steps that the Operator takes to increase the number of read-only replicas are: * Take a VolumeSnapshot of a primary node. * Start new read-only replica node(s) based on that snapshot. Since an archiver node is a special kind of read-only replica, the logic for enabling or disabling an archiver is identical to the read-only replica logic. ## Manually Triggering Reload or Restart The Operator will automatically trigger a configuration reload, a rolling restart, or a full restart (where all nodes are brought down before bringing them back up) whenever it is required. For example, a configuration reload happens automatically when the database log level is changed or a TLS certificate or certificate authority is changed, since those are the only hot-reloadable configurations. Full restarts are triggered any time that a rolling restart would not be possible, such as converting an insecure cluster to a secure cluster, when old nodes and new nodes would not be able to talk to each other. Additionally, the Operator allows you to manually trigger any of those operations by altering one of: * `KurrentDB.spec.configReloadKey` * `KurrentDB.spec.rollingRestartKey` * `KurrentDB.spec.fullRestartKey` Each key starts as an empty string and each time a key is changed, the corresponding operation will be executed. Administrators may safely ignore these settings. Still, they may prove useful in specific scenarios, such as evaluating the Operator, or testing application performance while the database executes a rolling restart in a staging environment, before applying a reconfiguration to the production environment. --- --- url: 'https://docs.kurrent.io/server/v22.10/cluster.md' --- # Clustering ## Highly-available cluster EventStoreDB allows you to run more than one node in a cluster for high availability. ### Cluster nodes EventStoreDB clusters follow a "shared nothing" philosophy, meaning that clustering requires no shared disks. Instead, each node has a copy of the data to ensure it is not lost in case of a drive failure or a node crashing. ::: tip Lean more about [node roles](#node-roles). ::: EventStoreDB uses a quorum-based replication model, in which a majority of nodes in the cluster must acknowledge that they have received a copy of the write before the write is acknowledged to the client. This means that to be able to tolerate the failure of *n* nodes, the cluster must be of size *(2n + 1)*. A three node cluster can continue to accept writes if one node is unavailable. A five node cluster can continue to accept writes if two nodes are unavailable, and so forth. ### Cluster size For any cluster configuration, you first need to decide how many nodes you want, provision each node and set the cluster size option on each node. The cluster size is a pre-defined value. The cluster expects the number of nodes to match this predefined number, otherwise the cluster would be incomplete and therefore unhealthy. The cluster cannot be dynamically scaled. If you need to change the number of cluster nodes, the cluster size setting must be changed on all nodes before the new node can join. Use the `ClusterSize` option to tell each cluster node about how many nodes the cluster should have. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--cluster-size` | | YAML | `ClusterSize` | | Environment variable | `EVENTSTORE_CLUSTER_SIZE` | **Default**: `1` (single node, no high-availability). Common values for the `ClusterSize` setting are three or five (to have a majority of two nodes and a majority of three nodes). We recommended setting this to an odd number of nodes to minimise the chance of a tie during elections, which would lengthen the election process. ### Discovering cluster members Cluster nodes use the gossip protocol to discover each other and elect the cluster leader. Cluster nodes need to know about one another to gossip. To start this process, you provide gossip seeds for each node. Configure cluster nodes to discover other nodes in one of two ways: * [via a DNS entry](#cluster-with-dns) and a well-known [gossip port](#gossip-port) * [via a list of addresses](#cluster-with-gossip-seeds) of other cluster nodes The multi-address DNS name cluster discovery only works for clusters that use certificates signed by a private certificate authority, or run insecure. For other scenarios you need to provide the gossip seed using hostnames of other cluster nodes. ### Internal communication When setting up a cluster, the nodes must be able to reach each other over both the HTTP channel, and the internal TCP channel. You should ensure that these ports are open on firewalls on the machines and between the machines. Learn more about [internal TCP configuration](networking.md#tcp-configuration) and [HTTP configuration](networking.md#http-configuration) to set up the cluster properly. ## Cluster with DNS When you tell EventStoreDB to use DNS for its gossip, the server will resolve the DNS name to a list of IP addresses and connect to each of those addresses to find other nodes. This method is very flexible because you can change the list of nodes on your DNS server without changing the cluster configuration. The DNS method is also useful in automated deployment scenarios when you control both the cluster deployment and the DNS server from your infrastructure-as-code scripts. To use DNS discovery, you need to set the `ClusterDns` option to the DNS name that allows making an HTTP call to it. When the server starts, it will attempt to make a gRPC call using the `https://:` URL (`http` if the cluster is insecure). When using a certificate signed by a publicly trusted CA, you'd normally use the wildcard certificate. Ensure that the cluster DNS name fits the wildcard, otherwise the request will fail on SSL check. When using certificates signed by a private CA, the cluster DNS name must be included to the certificate of each node as SAN (subject alternative name). You also need to have the `DiscoverViaDns` option to be set to `true` but it is its default value. | Format | Syntax | |:---------------------|:-------------------------| | Command line | `--cluster-dns` | | YAML | `ClusterDns` | | Environment variable | `EVENTSTORE_CLUSTER_DNS` | **Default**: `fake.dns`, which doesn't resolve to anything. You have to set it to a proper DNS name when used in combination to the DNS discovery (next setting). | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--discover-via-dns` | | YAML | `DiscoverViaDns` | | Environment variable | `EVENTSTORE_DISCOVER_VIA_DNS` | **Default**: `true`, the DNS discovery is enabled by default. It will be used only if the cluster has more than one node. You must set the `ClusterDns` setting to a proper DNS name. When using DNS for cluster gossip, you might need to set the `GossipPort` setting to the HTTP port if the external HTTP port setting is not set to `2113` default port. Refer to [gossip port](#gossip-port) option documentation to learn more. ## Cluster with gossip seeds If you don't want or cannot use the DNS-based configuration, it is possible to tell cluster nodes to call other nodes using their IP addresses. This method is a bit more cumbersome, because each node has to have the list of addresses for other nodes configured, but not its own address. The setting accepts a comma-separated list of IP addresses or host names with their gossip port values. | Format | Syntax | |:---------------------|:-------------------------| | Command line | `--gossip-seed` | | YAML | `GossipSeed` | | Environment variable | `EVENTSTORE_GOSSIP_SEED` | ## Gossip protocol EventStoreDB uses a quorum-based replication model. When working normally, a cluster has one node known as a leader, and the remaining nodes are followers. The leader node is responsible for coordinating writes while it is the leader. Cluster nodes use a consensus algorithm to determine which node should be the leader and which should be followers. EventStoreDB bases the decision as to which node should be the leader on a number of factors. For a cluster node to have this information available to them, the nodes gossip with other nodes in the cluster. Gossip runs over HTTP interfaces of cluster nodes. The gossip protocol configuration can be changed using the settings listed below. Pay attention to the settings related to time, like intervals and timeouts, when running in a cloud environment. ### Gossip port The gossip port is used for constructing the URL for making a gossip request to other nodes that are discovered via DNS. It is not used when using gossip seeds, because in that case the list contains IP addresses and the port. ::: warning Normally, the cluster gossip port is the same as the HTTP port, so you don't need to change this setting. ::: | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--cluster-gossip-port` | | YAML | `ClusterGossipPort` | | Environment variable | `EVENTSTORE_CLUSTER_GOSSIP_PORT` | **Default**: HTTP port ### Gossip interval Cluster nodes try to ensure that the communication with their neighbour nodes is not broken. They use the gossip protocol and call each other after a specified period of time. This period is called the gossip interval. You can change the `GossipInvervalMs` setting so cluster nodes check in with each other more or less frequently. The default value is two seconds (2000 ms). | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--gossip-interval-ms` | | YAML | `GossipIntervalMs` | | Environment variable | `EVENTSTORE_GOSSIP_INTERVAL_MS` | **Default**: `2000` (in milliseconds), which is two seconds. ### Time difference toleration EventStoreDB expects the time on cluster nodes to be in sync within a given tolerance. If different nodes have their clock out of sync for a number of milliseconds that exceeds the value of this setting, the gossip is rejected and the node will not be accepted as a cluster member. | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--gossip-allowed-difference-ms` | | YAML | `GossipAllowedDifferenceMs` | | Environment variable | `EVENTSTORE_GOSSIP_ALLOWED_DIFFERENCE_MS` | **Default**: `60000` (in milliseconds), which is one minute. ### Gossip timeout When nodes call each other using the gossip protocol to understand the cluster status, a busy node might delay the response. When a node is not getting a response from another node, it might consider that other node as dead. Such a situation might trigger the election process. If your cluster network is congested, you might increase the gossip timeout using the `GossipTimeoutMs` setting, so nodes will be more tolerant to delayed gossip responses. The default value is 2.5 seconds (2500 ms). | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--gossip-timeout-ms` | | YAML | `GossipTimeoutMs` | | Environment variable | `EVENTSTORE_GOSSIP_TIMEOUT_MS` | **Default**: `2500` (in milliseconds). ### Gossip on single node You can connect using gossip seeds regardless of whether you have a cluster or not. In the previous versions of EventStoreDB gossip on a single node was disabled. Starting from 21.2 it is enabled by default. ::: warning Please note that the `GossipOnSingleNode` option is deprecated and will be removed in a future version. The gossip endpoint is now unconditionally available for any deployment topology. ::: | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--gossip-on-single-node` | | YAML | `GossipOnSingleNode` | | Environment variable | `EVENTSTORE_GOSSIP_ON_SINGLE_NODE` | **Default**: `true` ### Leader election timeout The leader elections are separate to the node gossip, and are used to elect a node as Leader. In some cases the leader election messages may be delayed, which can result in elections taking longer than they should. If you start seeing election timeouts in the logs or if you have needed to increase the gossip timeout due to a congested network, then you should consider increasing the leader election timeout as well. | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--leader-election-timeout-ms` | | YAML | `LeaderElectionTimeoutMs` | | Environment variable | `EVENTSTORE_LEADER_ELECTION_TIMEOUT_MS` | **Default**: `1000` (in milliseconds). ## Node roles Every node in a stable EventStoreDB deployment settles into one of three roles: Leader, Follower, and ReadOnlyReplica. The cluster is composed of the Leader and Followers. ### Leader The leader ensures that writes are persisted to its own disk, replicated to a majority of cluster nodes, and indexed on the leader so that they can be read from the leader, before acknowledging the write as successful to the client. ### Follower A cluster assigns the follower role based on an election process. A cluster uses one or more nodes with the follower role to form the quorum, or the majority of nodes necessary to confirm that the write is persisted. ### Read-only replica You can add read-only replica nodes, which will not become cluster members and will not take part in elections. Read-only replicas can be used for scaling up reads if you have many catch-up subscriptions and want to off-load cluster members. A cluster asynchronously replicates data one way to a node with the read-only replica role. The read-only replica node is not part of the cluster, so does not add to the replication requirements needed to acknowledge a write. For this reason a node with a read-only replica role does not add much overhead to the other nodes. You need to explicitly configure the node as a read-only replica using this setting: | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--read-only-replica` | | YAML | `ReadOnlyReplica` | | Environment variable | `EVENTSTORE_READ_ONLY_REPLICA` | **Default**: `false`, set to `true` for a read-only replica node. The replica node needs to have the cluster gossip DNS or seed configured. For the gossip seed, use DNS names or IP addresses of all other cluster nodes, except read-only replicas. ### Node priority You can control which clones the cluster promotes with the `NodePriority` setting. The default value is `0`, and the cluster is more likely to promote nodes with higher values. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--node-priority` | | YAML | `NodePriority` | | Environment variable | `EVENTSTORE_NODE_PRORITY` | **Default**: `0`. ::: tip Changing `NodePriority` does not guarantee that the cluster will not promote the node. It is only one of the criteria that the Election Service considers. ::: --- --- url: 'https://docs.kurrent.io/server/v22.10/configuration.md' --- # Configuration ## Configuration options EventStoreDB has a number of configuration options that can be changed. You can find all the options described in details in this section. When you don't change the configuration, EventStoreDB will use sensible defaults, but they might not suit your needs. You can always instruct EventStoreDB to use a different set of options. There are multiple ways to configure EventStoreDB server, described below. ### Version and help You can check what version of EventStoreDB you have installed by using the `--version` parameter in the command line. For example: ::: tabs#os @tab Linux ````bash ```bash:no-line-numbers $ eventstored --version EventStoreDB version 22.10.0.0 (tags/oss-v22.10.0/242b35056, Thu, 10 Nov 2022 13:45:56 +0000) ```` @tab Windows ``` > EventStore.ClusterNode.exe --version EventStoreDB version 22.10.0.0 (tags/oss-v22.10.0/242b35056, Thu, 10 Nov 2022 13:45:56 +0000) ``` ::: The full list of available options is available from the currently installed server by using the `--help` option in the command line. ### Configuration file You would use the configuration file when you want the server to run with the same set of options every time. YAML files are better for large installations as you can centrally distribute and manage them, or generate them from a configuration management system. The configuration file has YAML-compatible format. The basic format of the YAML configuration file is as follows: ```yaml:no-line-numbers --- Db: "/volumes/data" Log: "/esdb/logs" ``` ::: tip You need to use the three dashes and spacing in your YAML file. ::: The default configuration file name is `eventstore.conf`. It is located in `/etc/eventstore/` on Linux and the server installation directory on Windows. You can either change this file or create another file and instruct EventStoreDB to use it. To tell the EventStoreDB server to use a different configuration file, you pass the file path on the command line with `--config=filename`, or use the `CONFIG` environment variable. ### Environment variables You can also set all arguments with environment variables. All variables are prefixed with `EVENTSTORE_` and normally follow the pattern `EVENTSTORE_{option}`. For example, setting the `EVENTSTORE_LOG` variable would instruct the server to use a custom location for log files. Environment variables override all the options specified in configuration files. ### Command line You can also override options from both configuration files and environment variables using the command line. For example, starting EventStoreDB with the `--log` option will override the default log files location: ::: tabs#os @tab Linux ```bash:no-line-numbers eventstored --log /tmp/eventstore/logs ``` @tab Windows ```bash:no-line-numbers EventStore.ClusterNode.exe --log C:\Temp\EventStore\Logs ``` ::: ### Testing the configuration If more than one method is used to configure the server, it might be hard to find out what the effective configuration will be when the server starts. To help to find out just that, you can use the `--what-if` option. When you run EventStoreDB with this option, it will print out the effective configuration applied from all available sources (default and custom configuration file, environment variables and command line parameters) and print it out to the console. ::: details Click here to see a WhatIf example ``` $ eventstored --what-if [ 131, 1,13:05:59.721,INF] "ES VERSION:" "22.10.0.0" ("tags/oss-v22.10.0"/"242b35056", "Thu, 10 Nov 2022 13:45:56 +0000") [ 131, 1,13:05:59.732,INF] "OS ARCHITECTURE:" X64 [ 131, 1,13:05:59.749,INF] "OS:" Linux ("Unix 4.19.128.0") [ 131, 1,13:05:59.752,INF] "RUNTIME:" ".NET 6.0.11/943474ca1" (64-bit) [ 131, 1,13:05:59.752,INF] "GC:" "3 GENERATIONS" "IsServerGC: False" "Latency Mode: Interactive" [ 131, 1,13:05:59.754,INF] "LOGS:" "/var/log/eventstore" [ 131, 1,13:05:59.795,INF] MODIFIED OPTIONS: CLUSTER SIZE: 1 (Yaml) RUN PROJECTIONS: None (Yaml) WHAT IF: true (Command Line) DEFAULT OPTIONS: ADVERTISE HOST TO CLIENT AS: () ADVERTISE HTTP PORT TO CLIENT AS: 0 () ADVERTISE TCP PORT TO CLIENT AS: 0 () ALLOW UNKNOWN OPTIONS: False () ALWAYS KEEP SCAVENGED: False () AUTHENTICATION CONFIG: () AUTHENTICATION TYPE: internal () AUTHORIZATION CONFIG: () AUTHORIZATION TYPE: internal () CACHED CHUNKS: -1 () CERTIFICATE FILE: () CERTIFICATE PASSWORD: () CERTIFICATE PRIVATE KEY FILE: () CERTIFICATE RESERVED NODE COMMON NAME: eventstoredb-node () CERTIFICATE STORE LOCATION: () CERTIFICATE STORE NAME: () CERTIFICATE SUBJECT NAME: () CERTIFICATE THUMBPRINT: () CHUNK INITIAL READER COUNT: 5 () CHUNK SIZE: 268435456 () CHUNKS CACHE SIZE: 536871424 () CLUSTER DNS: fake.dns () CLUSTER GOSSIP PORT: 2113 () COMMIT ACK COUNT: 1 () COMMIT COUNT: -1 () COMMIT TIMEOUT MS: 2000 () CONFIG: /etc/eventstore/eventstore.conf () CONNECTION PENDING SEND BYTES THRESHOLD: 10485760 () CONNECTION QUEUE SIZE THRESHOLD: 50000 () DB: /var/lib/eventstore () DB LOG FORMAT: V2 () DEAD MEMBER REMOVAL PERIOD SEC: 1800 () DEV: False () DISABLE ADMIN UI: False () DISABLE EXTERNAL TCP TLS: False () DISABLE FIRST LEVEL HTTP AUTHORIZATION: False () DISABLE GOSSIP ON HTTP: False () DISABLE HTTP CACHING: False () DISABLE INTERNAL TCP TLS: False () DISABLE LOG FILE: False () DISABLE SCAVENGE MERGING: False () DISABLE STATS ON HTTP: False () DISCOVER VIA DNS: True () ENABLE ATOM PUB OVER HTTP: False () ENABLE EXTERNAL TCP: False () ENABLE HISTOGRAMS: False () ENABLE TRUSTED AUTH: False () EXT HOST ADVERTISE AS: () EXT IP: 127.0.0.1 () EXT TCP HEARTBEAT INTERVAL: 2000 () EXT TCP HEARTBEAT TIMEOUT: 1000 () EXT TCP PORT: 1113 () EXT TCP PORT ADVERTISE AS: 0 () FAULT OUT OF ORDER PROJECTIONS: False () GOSSIP ALLOWED DIFFERENCE MS: 60000 () GOSSIP INTERVAL MS: 2000 () GOSSIP ON SINGLE NODE: () GOSSIP SEED: () GOSSIP TIMEOUT MS: 2500 () HASH COLLISION READ LIMIT: 100 () HELP: False () HTTP PORT: 2113 () HTTP PORT ADVERTISE AS: 0 () INDEX: () INDEX CACHE DEPTH: 16 () INDEX CACHE SIZE: 0 () INITIALIZATION THREADS: 1 () INSECURE: False () INT HOST ADVERTISE AS: () INT IP: 127.0.0.1 () INT TCP HEARTBEAT INTERVAL: 700 () INT TCP HEARTBEAT TIMEOUT: 700 () INT TCP PORT: 1112 () INT TCP PORT ADVERTISE AS: 0 () KEEP ALIVE INTERVAL: 10000 () KEEP ALIVE TIMEOUT: 10000 () LEADER ELECTION TIMEOUT MS: 1000 () LOG: /var/log/eventstore () LOG CONFIG: logconfig.json () LOG CONSOLE FORMAT: Plain () LOG FAILED AUTHENTICATION ATTEMPTS: False () LOG FILE INTERVAL: Day () LOG FILE RETENTION COUNT: 31 () LOG FILE SIZE: 1073741824 () LOG HTTP REQUESTS: False () LOG LEVEL: Default () MAX APPEND SIZE: 1048576 () MAX AUTO MERGE INDEX LEVEL: 2147483647 () MAX MEM TABLE SIZE: 1000000 () MAX TRUNCATION: 268435456 () MEM DB: False () MIN FLUSH DELAY MS: 2 () NODE PRIORITY: 0 () OPTIMIZE INDEX MERGE: False () PREPARE ACK COUNT: 1 () PREPARE COUNT: -1 () PREPARE TIMEOUT MS: 2000 () PROJECTION COMPILATION TIMEOUT: 500 () PROJECTION EXECUTION TIMEOUT: 250 () PROJECTION THREADS: 3 () PROJECTIONS QUERY EXPIRY: 5 () QUORUM SIZE: 1 () READ ONLY REPLICA: False () READER THREADS COUNT: 0 () REDUCE FILE CACHE PRESSURE: False () REMOVE DEV CERTS: False () SCAVENGE BACKEND CACHE SIZE: 67108864 () SCAVENGE BACKEND PAGE SIZE: 16384 () SCAVENGE HASH USERS CACHE CAPACITY: 100000 () SCAVENGE HISTORY MAX AGE: 30 () SKIP DB VERIFY: False () SKIP INDEX SCAN ON READS: False () SKIP INDEX VERIFY: False () START STANDARD PROJECTIONS: False () STATS PERIOD SEC: 30 () STATS STORAGE: File () STREAM EXISTENCE FILTER SIZE: 256000000 () STREAM INFO CACHE CAPACITY: 0 () TRUSTED ROOT CERTIFICATE STORE LOCATION: () TRUSTED ROOT CERTIFICATE STORE NAME: () TRUSTED ROOT CERTIFICATE SUBJECT NAME: () TRUSTED ROOT CERTIFICATE THUMBPRINT: () TRUSTED ROOT CERTIFICATES PATH: () UNBUFFERED: False () UNSAFE ALLOW SURPLUS NODES: False () UNSAFE DISABLE FLUSH TO DISK: False () UNSAFE IGNORE HARD DELETE: False () USE INDEX BLOOM FILTERS: True () VERSION: False () WORKER THREADS: 0 () WRITE STATS TO DB: False () WRITE THROUGH: False () WRITE TIMEOUT MS: 2000 () ``` ::: ::: note Version 21.6 introduced a stricter configuration check: the server will *not start* when an unknown configuration options is passed in either the configuration file, environment variable or command line. E.g: the following will prevent the server from starting: * `--UnknownConfig` on the command line * `EVENTSTORE_UnknownConfig` through environment variable * `UnknownConfig: value` in the config file And will output on `stdout` only: *Error while parsing options: The option UnknownConfig is not a known option. (Parameter 'UnknownConfig')*. ::: ## Autoconfigured options Some options are configured at startup to make better use of the available resources on larger instances or machines. These options are `StreamInfoCacheCapacity`, `ReaderThreadsCount`, and `WorkerThreads`. ### StreamInfoCacheCapacity This option sets the maximum number of entries to keep in the stream info cache. This is the lookup that contains the information of any stream that has recently been read or written to. Having entries in this cache significantly improves write and read performance to cached streams on larger databases. By default, the cache dynamically resizes according to the amount of free memory. The minimum that it can be set to is 100,000 entries. | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--stream-info-cache-capacity` | | YAML | `StreamInfoCacheCapacity` | | Environment variable | `STREAM_INFO_CACHE_CAPACITY` | The option is set to 0 by default, which enables dynamic resizing. The default on previous versions of EventStoreDb was 100,000 entries. ### ReaderThreadsCount This option configures the number of reader threads available to EventStoreDb. Having more reader threads allows more concurrent reads to be processed. The reader threads count will be set at startup to twice the number of available processors, with a minimum of 4 and a maximum of 16 threads. | Format | Syntax | |:---------------------|:-------------------------| | Command line | `--reader-threads-count` | | YAML | `ReaderThreadsCount` | | Environment variable | `READER_THREADS_COUNT` | The option is set to 0 by default, which enables autoconfiguration. The default on previous versions of EventStoreDb was 4 threads. ::: warning Increasing the reader threads count too high can cause read timeouts if your disk cannot handle the increased load. ::: ### WorkerThreads The `WorkerThreads` option configures the number of threads available to the pool of worker services. At startup the number of worker threads will be set to 10 if there are more than 4 reader threads. Otherwise, it will be set to have 5 threads available. | Format | Syntax | |:---------------------|:-------------------| | Command line | `--worker-threads` | | YAML | `WorkerThreads` | | Environment variable | `WORKER_THREADS` | The option is set to 0 by default, which enables autoconfiguration. The default on previous versions of EventStoreDb was 5 threads. --- --- url: 'https://docs.kurrent.io/server/v22.10/diagnostics.md' --- # Diagnostics EventStoreDB provides several ways to diagnose and troubleshoot issues. * [Logging](#logging): structured or plain-text logs on the console and in log files. * [Stats](#statistics): stats collection and HTTP endpoint. * [Histograms](#histograms): metrics collection and HTTP endpoints. You can also use external tools to measure the performance of EventStoreDB and monitor the cluster health. * [Vector](#vector): collect metrics and logs to your APM tool using Vector. * [Prometheus exporter](#prometheus): collect metrics in Prometheus. * [Datadog integration](#datadog): monitor and measure the cluster with Datadog. ## Logging EventStoreDB logs its internal operations to the console (stdout) and to log files. The default location of the log files and the way to change it is described [below](#logs-location). There are a few options to change the way how EventStoreDB produces logs and how detailed the logs should be. ::: warning The EventStoreDB logs may contain sensitive information such as stream names, user names, and projection definitions. ::: ### Log format EventStoreDB uses the structured logging in JSON format that is more machine-friendly and can be ingested by vendor-specific tools like Logstash or Datadog agent. Here is how the structured log looks like: ```json { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Debug", "Logger": "ProjectionManager", "Message": "PROJECTIONS: Starting Projections Manager. (Node State : {state})", "EventProperties": { "state": "Master" } } { "PID": "6940", "ThreadID": "15", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Info", "Logger": "ClusterVNodeController", "Message": "========== [{internalHttp}] Sub System '{subSystemName}' initialized.", "EventProperties": { "internalHttp": "127.0.0.1:2112", "subSystemName": "Projections" } } { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Debug", "Logger": "MultiStreamMessageWriter", "Message": "PROJECTIONS: Resetting Worker Writer", "EventProperties": {} } { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.055000Z", "Level": "Debug", "Logger": "ProjectionCoreCoordinator", "Message": "PROJECTIONS: SubComponent Started: {subComponent}", "EventProperties": { "subComponent": "EventReaderCoreService" } } ``` This format is aligned with [Serilog Compact JSON format](https://github.com/serilog/serilog-formatting-compact). ### Logs location Log files are located in `/var/log/eventstore` for Linux and macOS, and in the `logs` subdirectory of the EventStoreDB installation directory on Windows. You can change the log files location using the `Log` configuration option. ::: tip Moving logs to a separate storage might improve the database performance if you keep the default verbose log level. ::: | Format | Syntax | | :------------------- | :--------------- | | Command line | `--log` | | YAML | `Log` | | Environment variable | `EVENTSTORE_LOG` | For example, adding this line to the `eventstore.conf` file will force writing logs to the `/tmp/eventstore/logs` directory: ```text:no-line-numbers Log: /tmp/eventstore/logs ``` ### Log level You can change the level using the `LogLevel` setting: | Format | Syntax | | :------------------- | :--------------------- | | Command line | `--log-level` | | YAML | `LogLevel` | | Environment variable | `EVENTSTORE_LOG_LEVEL` | Acceptable values are: `Default`, `Verbose`, `Debug`, `Information`, `Warning`, `Error`, and `Fatal`. ### Logging options You can tune the EventStoreDB logging further by using the logging options described below. #### Log configuration file Specifies the location of the file which configures the logging levels of various components. | Format | Syntax | | :------------------- | :---------------------- | | Command line | `--log-config` | | YAML | `LogConfig` | | Environment variable | `EVENTSTORE_LOG_CONFIG` | By default, the application directory (and `/etc/eventstore` on Linux and Mac) are checked. You may specify a full path. #### HTTP requests logging EventStoreDB cal also log all the incoming HTTP requests, like many HTTP servers do. Requests are logged before being processed, so unsuccessful requests are logged too. Use one of the following ways to enable the HTTP requests logging: | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--log-http-requests` | | YAML | `LogHttpRequests` | | Environment variable | `EVENTSTORE_LOG_HTTP_REQUESTS` | **Default**: `false`, logging HTTP requests is disabled by default. #### Log failed authentication For security monitoring, you can enable logging failed authentication attempts by setting `LogFailedAuthenticationAttempts` setting to true. | Format | Syntax | | :------------------- | :---------------------------------------------- | | Command line | `--log-failed-authentication-attempts` | | YAML | `LogFailedAuthenticationAttempts` | | Environment variable | `EVENTSTORE_LOG_FAILED_AUTHENTICATION_ATTEMPTS` | **Default**: `false` #### Log console format The format of the console logger. Use `Json` for structured log output. | Format | Syntax | | :------------------- | :------------------------------ | | Command line | `--log-console-format` | | YAML | `LogConsoleFormat` | | Environment variable | `EVENTSTORE_LOG_CONSOLE_FORMAT` | Acceptable values are: `Plain`, `Json` **Default**: `Plain` #### Log file size The maximum size of each log file, in bytes. | Format | Syntax | | :------------------- | :------------------------- | | Command line | `--log-file-size` | | YAML | `LogFileSize` | | Environment variable | `EVENTSTORE_LOG_FILE_SIZE` | **Default**: `1GB` #### Log file interval How often to rotate logs. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--log-file-interval` | | YAML | `LogFileInterval` | | Environment variable | `EVENTSTORE_LOG_FILE_INTERVAL` | Acceptable values are: `Minute`, `Hour`, `Day`, `Week`, `Month`, `Year` **Default**: `Day` #### Log file retention count How many log files to hold on to. | Format | Syntax | | :------------------- | :------------------------------- | | Command line | `--log-file-retention-count` | | YAML | `LogFileRetentionCount` | | Environment variable | `EVENTSTORE_LOG_RETENTION_COUNT` | **Default**: `31` #### Disable log file You can completely disable logging to a file by changing the `DisableLogFile` option. | Format | Syntax | | :------------------- | :---------------------------- | | Command line | `--disable-log-file` | | YAML | `DisableLogFile` | | Environment variable | `EVENTSTORE_DISABLE_LOG_FILE` | **Default**: `false` ## Statistics EventStoreDB servers collect internal statistics and make it available via HTTP over the `https://:2113/stats` in JSON format. Here, `2113` is the default HTTP port. Monitoring applications and metric collectors can use this endpoint to gather the information about the cluster node. The `stats` endpoint only exposes information about the node where you fetch it from and doesn't contain any cluster information. What you see in the `stats` endpoint response is the last collected state of the server. The server collects this information using events that are appended to the statistics stream. Each node has one. We use a reserved name for the stats stream, `$stats-`. For example, for a single node running locally the stream name would be `$stats-127.0.0.1:2113`. As all other events, stats events are also linked in the `$all` stream. These events have a reserved event type `$statsCollected`. ::: details Click here to see an example of a stats event ```json { "proc-startTime": "2020-06-25T10:13:26.8281750Z", "proc-id": 5465, "proc-mem": 118648832, "proc-cpu": 2.44386363, "proc-cpuScaled": 0.152741477, "proc-threadsCount": 10, "proc-contentionsRate": 0.9012223, "proc-thrownExceptionsRate": 0.0, "sys-cpu": 100.0, "sys-freeMem": 25100288, "proc-gc-allocationSpeed": 0.0, "proc-gc-gen0ItemsCount": 8, "proc-gc-gen0Size": 0, "proc-gc-gen1ItemsCount": 2, "proc-gc-gen1Size": 0, "proc-gc-gen2ItemsCount": 0, "proc-gc-gen2Size": 0, "proc-gc-largeHeapSize": 0, "proc-gc-timeInGc": 0.0, "proc-gc-totalBytesInHeaps": 0, "proc-tcp-connections": 0, "proc-tcp-receivingSpeed": 0.0, "proc-tcp-sendingSpeed": 0.0, "proc-tcp-inSend": 0, "proc-tcp-measureTime": "00:00:19.0534210", "proc-tcp-pendingReceived": 0, "proc-tcp-pendingSend": 0, "proc-tcp-receivedBytesSinceLastRun": 0, "proc-tcp-receivedBytesTotal": 0, "proc-tcp-sentBytesSinceLastRun": 0, "proc-tcp-sentBytesTotal": 0, "es-checksum": 1613144, "es-checksumNonFlushed": 1613144, "sys-drive-/System/Volumes/Data-availableBytes": 545628151808, "sys-drive-/System/Volumes/Data-totalBytes": 2000481927168, "sys-drive-/System/Volumes/Data-usage": "72%", "sys-drive-/System/Volumes/Data-usedBytes": 1454853775360, "es-queue-Index Committer-queueName": "Index Committer", "es-queue-Index Committer-groupName": "", "es-queue-Index Committer-avgItemsPerSecond": 0, "es-queue-Index Committer-avgProcessingTime": 0.0, "es-queue-Index Committer-currentIdleTime": "0:00:00:29.9895180", "es-queue-Index Committer-currentItemProcessingTime": null, "es-queue-Index Committer-idleTimePercent": 100.0, "es-queue-Index Committer-length": 0, "es-queue-Index Committer-lengthCurrentTryPeak": 0, "es-queue-Index Committer-lengthLifetimePeak": 0, "es-queue-Index Committer-totalItemsProcessed": 0, "es-queue-Index Committer-inProgressMessage": "", "es-queue-Index Committer-lastProcessedMessage": "", "es-queue-MainQueue-queueName": "MainQueue", "es-queue-MainQueue-groupName": "", "es-queue-MainQueue-avgItemsPerSecond": 14, "es-queue-MainQueue-avgProcessingTime": 0.0093527972027972021, "es-queue-MainQueue-currentIdleTime": "0:00:00:00.8050567", "es-queue-MainQueue-currentItemProcessingTime": null, "es-queue-MainQueue-idleTimePercent": 99.986616840364917, "es-queue-MainQueue-length": 0, "es-queue-MainQueue-lengthCurrentTryPeak": 3, "es-queue-MainQueue-lengthLifetimePeak": 6, "es-queue-MainQueue-totalItemsProcessed": 452, "es-queue-MainQueue-inProgressMessage": "", "es-queue-MainQueue-lastProcessedMessage": "Schedule", "es-queue-MonitoringQueue-queueName": "MonitoringQueue", "es-queue-MonitoringQueue-groupName": "", "es-queue-MonitoringQueue-avgItemsPerSecond": 0, "es-queue-MonitoringQueue-avgProcessingTime": 1.94455, "es-queue-MonitoringQueue-currentIdleTime": "0:00:00:19.0601186", "es-queue-MonitoringQueue-currentItemProcessingTime": null, "es-queue-MonitoringQueue-idleTimePercent": 99.980537727681721, "es-queue-MonitoringQueue-length": 0, "es-queue-MonitoringQueue-lengthCurrentTryPeak": 0, "es-queue-MonitoringQueue-lengthLifetimePeak": 0, "es-queue-MonitoringQueue-totalItemsProcessed": 14, "es-queue-MonitoringQueue-inProgressMessage": "", "es-queue-MonitoringQueue-lastProcessedMessage": "GetFreshTcpConnectionStats", "es-queue-PersistentSubscriptions-queueName": "PersistentSubscriptions", "es-queue-PersistentSubscriptions-groupName": "", "es-queue-PersistentSubscriptions-avgItemsPerSecond": 1, "es-queue-PersistentSubscriptions-avgProcessingTime": 0.010400000000000001, "es-queue-PersistentSubscriptions-currentIdleTime": "0:00:00:00.8052015", "es-queue-PersistentSubscriptions-currentItemProcessingTime": null, "es-queue-PersistentSubscriptions-idleTimePercent": 99.998954276430226, "es-queue-PersistentSubscriptions-length": 0, "es-queue-PersistentSubscriptions-lengthCurrentTryPeak": 0, "es-queue-PersistentSubscriptions-lengthLifetimePeak": 0, "es-queue-PersistentSubscriptions-totalItemsProcessed": 32, "es-queue-PersistentSubscriptions-inProgressMessage": "", "es-queue-PersistentSubscriptions-lastProcessedMessage": "PersistentSubscriptionTimerTick", "es-queue-Projection Core #0-queueName": "Projection Core #0", "es-queue-Projection Core #0-groupName": "Projection Core", "es-queue-Projection Core #0-avgItemsPerSecond": 0, "es-queue-Projection Core #0-avgProcessingTime": 0.0, "es-queue-Projection Core #0-currentIdleTime": "0:00:00:29.9480513", "es-queue-Projection Core #0-currentItemProcessingTime": null, "es-queue-Projection Core #0-idleTimePercent": 100.0, "es-queue-Projection Core #0-length": 0, "es-queue-Projection Core #0-lengthCurrentTryPeak": 0, "es-queue-Projection Core #0-lengthLifetimePeak": 0, "es-queue-Projection Core #0-totalItemsProcessed": 2, "es-queue-Projection Core #0-inProgressMessage": "", "es-queue-Projection Core #0-lastProcessedMessage": "SubComponentStarted", "es-queue-Projections Master-queueName": "Projections Master", "es-queue-Projections Master-groupName": "", "es-queue-Projections Master-avgItemsPerSecond": 0, "es-queue-Projections Master-avgProcessingTime": 0.0, "es-queue-Projections Master-currentIdleTime": "0:00:00:29.8467445", "es-queue-Projections Master-currentItemProcessingTime": null, "es-queue-Projections Master-idleTimePercent": 100.0, "es-queue-Projections Master-length": 0, "es-queue-Projections Master-lengthCurrentTryPeak": 0, "es-queue-Projections Master-lengthLifetimePeak": 3, "es-queue-Projections Master-totalItemsProcessed": 10, "es-queue-Projections Master-inProgressMessage": "", "es-queue-Projections Master-lastProcessedMessage": "RegularTimeout", "es-queue-Storage Chaser-queueName": "Storage Chaser", "es-queue-Storage Chaser-groupName": "", "es-queue-Storage Chaser-avgItemsPerSecond": 94, "es-queue-Storage Chaser-avgProcessingTime": 0.0043385023898035047, "es-queue-Storage Chaser-currentIdleTime": "0:00:00:00.0002530", "es-queue-Storage Chaser-currentItemProcessingTime": null, "es-queue-Storage Chaser-idleTimePercent": 99.959003031702224, "es-queue-Storage Chaser-length": 0, "es-queue-Storage Chaser-lengthCurrentTryPeak": 0, "es-queue-Storage Chaser-lengthLifetimePeak": 0, "es-queue-Storage Chaser-totalItemsProcessed": 2835, "es-queue-Storage Chaser-inProgressMessage": "", "es-queue-Storage Chaser-lastProcessedMessage": "ChaserCheckpointFlush", "es-queue-StorageReaderQueue #1-queueName": "StorageReaderQueue #1", "es-queue-StorageReaderQueue #1-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #1-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #1-avgProcessingTime": 0.22461000000000003, "es-queue-StorageReaderQueue #1-currentIdleTime": "0:00:00:00.9863988", "es-queue-StorageReaderQueue #1-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #1-idleTimePercent": 99.988756844383616, "es-queue-StorageReaderQueue #1-length": 0, "es-queue-StorageReaderQueue #1-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #1-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #1-totalItemsProcessed": 15, "es-queue-StorageReaderQueue #1-inProgressMessage": "", "es-queue-StorageReaderQueue #1-lastProcessedMessage": "ReadStreamEventsBackward", "es-queue-StorageReaderQueue #2-queueName": "StorageReaderQueue #2", "es-queue-StorageReaderQueue #2-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #2-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #2-avgProcessingTime": 8.83216, "es-queue-StorageReaderQueue #2-currentIdleTime": "0:00:00:00.8051068", "es-queue-StorageReaderQueue #2-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #2-idleTimePercent": 99.557874170777851, "es-queue-StorageReaderQueue #2-length": 0, "es-queue-StorageReaderQueue #2-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #2-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #2-totalItemsProcessed": 16, "es-queue-StorageReaderQueue #2-inProgressMessage": "", "es-queue-StorageReaderQueue #2-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageReaderQueue #3-queueName": "StorageReaderQueue #3", "es-queue-StorageReaderQueue #3-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #3-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #3-avgProcessingTime": 6.4189888888888893, "es-queue-StorageReaderQueue #3-currentIdleTime": "0:00:00:02.8228372", "es-queue-StorageReaderQueue #3-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #3-idleTimePercent": 99.710808119472517, "es-queue-StorageReaderQueue #3-length": 0, "es-queue-StorageReaderQueue #3-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #3-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #3-totalItemsProcessed": 14, "es-queue-StorageReaderQueue #3-inProgressMessage": "", "es-queue-StorageReaderQueue #3-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageReaderQueue #4-queueName": "StorageReaderQueue #4", "es-queue-StorageReaderQueue #4-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #4-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #4-avgProcessingTime": 0.36447, "es-queue-StorageReaderQueue #4-currentIdleTime": "0:00:00:01.8144419", "es-queue-StorageReaderQueue #4-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #4-idleTimePercent": 99.981747643099709, "es-queue-StorageReaderQueue #4-length": 0, "es-queue-StorageReaderQueue #4-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #4-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #4-totalItemsProcessed": 14, "es-queue-StorageReaderQueue #4-inProgressMessage": "", "es-queue-StorageReaderQueue #4-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageWriterQueue-queueName": "StorageWriterQueue", "es-queue-StorageWriterQueue-groupName": "", "es-queue-StorageWriterQueue-avgItemsPerSecond": 0, "es-queue-StorageWriterQueue-avgProcessingTime": 0.0, "es-queue-StorageWriterQueue-currentIdleTime": "0:00:00:29.9437790", "es-queue-StorageWriterQueue-currentItemProcessingTime": null, "es-queue-StorageWriterQueue-idleTimePercent": 100.0, "es-queue-StorageWriterQueue-length": 0, "es-queue-StorageWriterQueue-lengthCurrentTryPeak": 0, "es-queue-StorageWriterQueue-lengthLifetimePeak": 0, "es-queue-StorageWriterQueue-totalItemsProcessed": 6, "es-queue-StorageWriterQueue-inProgressMessage": "", "es-queue-StorageWriterQueue-lastProcessedMessage": "WritePrepares", "es-queue-Subscriptions-queueName": "Subscriptions", "es-queue-Subscriptions-groupName": "", "es-queue-Subscriptions-avgItemsPerSecond": 1, "es-queue-Subscriptions-avgProcessingTime": 0.057019047619047622, "es-queue-Subscriptions-currentIdleTime": "0:00:00:00.8153708", "es-queue-Subscriptions-currentItemProcessingTime": null, "es-queue-Subscriptions-idleTimePercent": 99.993992971356, "es-queue-Subscriptions-length": 0, "es-queue-Subscriptions-lengthCurrentTryPeak": 0, "es-queue-Subscriptions-lengthLifetimePeak": 0, "es-queue-Subscriptions-totalItemsProcessed": 31, "es-queue-Subscriptions-inProgressMessage": "", "es-queue-Subscriptions-lastProcessedMessage": "CheckPollTimeout", "es-queue-Timer-queueName": "Timer", "es-queue-Timer-groupName": "", "es-queue-Timer-avgItemsPerSecond": 14, "es-queue-Timer-avgProcessingTime": 0.038568989547038329, "es-queue-Timer-currentIdleTime": "0:00:00:00.0002752", "es-queue-Timer-currentItemProcessingTime": null, "es-queue-Timer-idleTimePercent": 99.94364205726194, "es-queue-Timer-length": 17, "es-queue-Timer-lengthCurrentTryPeak": 17, "es-queue-Timer-lengthLifetimePeak": 17, "es-queue-Timer-totalItemsProcessed": 419, "es-queue-Timer-inProgressMessage": "", "es-queue-Timer-lastProcessedMessage": "ExecuteScheduledTasks", "es-queue-Worker #1-queueName": "Worker #1", "es-queue-Worker #1-groupName": "Workers", "es-queue-Worker #1-avgItemsPerSecond": 2, "es-queue-Worker #1-avgProcessingTime": 0.076058695652173922, "es-queue-Worker #1-currentIdleTime": "0:00:00:00.8050943", "es-queue-Worker #1-currentItemProcessingTime": null, "es-queue-Worker #1-idleTimePercent": 99.982484504768721, "es-queue-Worker #1-length": 0, "es-queue-Worker #1-lengthCurrentTryPeak": 0, "es-queue-Worker #1-lengthLifetimePeak": 0, "es-queue-Worker #1-totalItemsProcessed": 73, "es-queue-Worker #1-inProgressMessage": "", "es-queue-Worker #1-lastProcessedMessage": "ReadStreamEventsForwardCompleted", "es-queue-Worker #2-queueName": "Worker #2", "es-queue-Worker #2-groupName": "Workers", "es-queue-Worker #2-avgItemsPerSecond": 2, "es-queue-Worker #2-avgProcessingTime": 0.19399347826086957, "es-queue-Worker #2-currentIdleTime": "0:00:00:00.8356863", "es-queue-Worker #2-currentItemProcessingTime": null, "es-queue-Worker #2-idleTimePercent": 99.955350254886739, "es-queue-Worker #2-length": 0, "es-queue-Worker #2-lengthCurrentTryPeak": 0, "es-queue-Worker #2-lengthLifetimePeak": 0, "es-queue-Worker #2-totalItemsProcessed": 69, "es-queue-Worker #2-inProgressMessage": "", "es-queue-Worker #2-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #3-queueName": "Worker #3", "es-queue-Worker #3-groupName": "Workers", "es-queue-Worker #3-avgItemsPerSecond": 2, "es-queue-Worker #3-avgProcessingTime": 0.068475555555555567, "es-queue-Worker #3-currentIdleTime": "0:00:00:00.8356754", "es-queue-Worker #3-currentItemProcessingTime": null, "es-queue-Worker #3-idleTimePercent": 99.984583460721979, "es-queue-Worker #3-length": 0, "es-queue-Worker #3-lengthCurrentTryPeak": 0, "es-queue-Worker #3-lengthLifetimePeak": 0, "es-queue-Worker #3-totalItemsProcessed": 68, "es-queue-Worker #3-inProgressMessage": "", "es-queue-Worker #3-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #4-queueName": "Worker #4", "es-queue-Worker #4-groupName": "Workers", "es-queue-Worker #4-avgItemsPerSecond": 2, "es-queue-Worker #4-avgProcessingTime": 0.040221428571428575, "es-queue-Worker #4-currentIdleTime": "0:00:00:00.8356870", "es-queue-Worker #4-currentItemProcessingTime": null, "es-queue-Worker #4-idleTimePercent": 99.99154911144629, "es-queue-Worker #4-length": 0, "es-queue-Worker #4-lengthCurrentTryPeak": 0, "es-queue-Worker #4-lengthLifetimePeak": 0, "es-queue-Worker #4-totalItemsProcessed": 65, "es-queue-Worker #4-inProgressMessage": "", "es-queue-Worker #4-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #5-queueName": "Worker #5", "es-queue-Worker #5-groupName": "Workers", "es-queue-Worker #5-avgItemsPerSecond": 2, "es-queue-Worker #5-avgProcessingTime": 0.17759268292682928, "es-queue-Worker #5-currentIdleTime": "0:00:00:00.8052165", "es-queue-Worker #5-currentItemProcessingTime": null, "es-queue-Worker #5-idleTimePercent": 99.9635548548067, "es-queue-Worker #5-length": 0, "es-queue-Worker #5-lengthCurrentTryPeak": 0, "es-queue-Worker #5-lengthLifetimePeak": 0, "es-queue-Worker #5-totalItemsProcessed": 70, "es-queue-Worker #5-inProgressMessage": "", "es-queue-Worker #5-lastProcessedMessage": "IODispatcherDelayedMessage", "es-writer-lastFlushSize": 0, "es-writer-lastFlushDelayMs": 0.0134, "es-writer-meanFlushSize": 0, "es-writer-meanFlushDelayMs": 0.0134, "es-writer-maxFlushSize": 0, "es-writer-maxFlushDelayMs": 0.0134, "es-writer-queuedFlushMessages": 0, "es-readIndex-cachedRecord": 676, "es-readIndex-notCachedRecord": 0, "es-readIndex-cachedStreamInfo": 171, "es-readIndex-notCachedStreamInfo": 32, "es-readIndex-cachedTransInfo": 0, "es-readIndex-notCachedTransInfo": 0 } ``` ::: Stats stream has the max time-to-live set to 24 hours, so all the events that are older than 24 hours will be deleted. ### Stats period Using this setting you can control how often stats events are generated. By default, the node will produce one event in 30 seconds. If you want to decrease network pressure on subscribers to the `$all` stream, you can tell EventStoreDB to produce stats less often. | Format | Syntax | | :------------------- | :---------------------------- | | Command line | `--stats-period-sec` | | YAML | `StatsPeriodSec` | | Environment variable | `EVENTSTORE_STATS_PERIOD_SEC` | **Default**: `30` ### Write stats to database As mentioned before, stats events are quite large and whilst it is sometimes beneficial to keep the stats history, it is most of the time not necessary. Therefore, we do not write stats events to the database by default. When this option is set to `true`, all the stats events will be persisted. As mentioned before, stats events have a TTL of 24 hours and when writing stats to the database is enabled, you'd need to scavenge more often to release the disk space. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--write-stats-to-db` | | YAML | `WriteStatsToDb` | | Environment variable | `EVENTSTORE_WRITE_STATS_TO_DB` | **Default**: `false` ## Histograms Histograms give a distribution in percentiles of the time spent on several metrics. This can be used to diagnose issues in the system. It is not recommended enabling this in production environment. When enabled, histogram stats are available at their corresponding http endpoints. For example, you could ask for a stream reader histograms like this: ```bash:no-line-numbers curl http://localhost:2113/histogram/reader-streamrange -u admin:changeit ``` That would give a response with the stats distributed across histogram buckets: ``` Value Percentile TotalCount 1/(1-Percentile) 0.022 0.000000000000 1 1.00 0.044 0.100000000000 30 1.11 0.054 0.200000000000 59 1.25 0.074 0.300000000000 88 1.43 0.092 0.400000000000 118 1.67 0.108 0.500000000000 147 2.00 0.113 0.550000000000 162 2.22 0.127 0.600000000000 176 2.50 0.140 0.650000000000 191 2.86 0.155 0.700000000000 206 3.33 0.168 0.750000000000 220 4.00 0.179 0.775000000000 228 4.44 0.197 0.800000000000 235 5.00 0.219 0.825000000000 242 5.71 0.232 0.850000000000 250 6.67 0.277 0.875000000000 257 8.00 0.327 0.887500000000 261 8.89 0.346 0.900000000000 264 10.00 0.522 0.912500000000 268 11.43 0.836 0.925000000000 272 13.33 0.971 0.937500000000 275 16.00 1.122 0.943750000000 277 17.78 1.153 0.950000000000 279 20.00 1.217 0.956250000000 281 22.86 2.836 0.962500000000 283 26.67 2.972 0.968750000000 284 32.00 3.607 0.971875000000 285 35.56 4.964 0.975000000000 286 40.00 8.536 0.978125000000 287 45.71 11.035 0.981250000000 288 53.33 11.043 0.984375000000 289 64.00 11.043 0.985937500000 289 71.11 34.013 0.987500000000 290 80.00 34.013 0.989062500000 290 91.43 41.812 0.990625000000 292 106.67 41.812 0.992187500000 292 128.00 41.812 0.992968750000 292 142.22 41.812 0.993750000000 292 160.00 41.812 0.994531250000 292 182.86 41.812 0.995312500000 292 213.33 41.812 0.996093750000 292 256.00 41.812 0.996484375000 292 284.44 41.878 0.996875000000 293 320.00 41.878 1.000000000000 293 #[Mean = 0.854, StdDeviation = 4.739] #[Max = 41.878, Total count = 293] #[Buckets = 20, SubBuckets = 2048] ``` ### Reading histograms The histogram response tells you some useful metrics like mean, max, standard deviation and also that in 99% of cases reads take about 41.8ms, as in the example above. ### Using histograms You can enable histograms in a development environment and run a specific task to see how it affects the database, telling you where and how the time is spent. Execute a `GET` HTTP call to a cluster node using the `http://:2113/histogram/` path to get a response. Here `2113` is the default external HTTP port. ### Available metrics | Endpoint | Measures time spent | | :------------------------------- | :-------------------------------------------------------- | | `reader-streamrange` | `ReadStreamEventsForward` and `ReadStreamEventsBackwards` | | `writer-flush` | Flushing to disk in the storage writer service | | `chaser-wait` and `chaser-flush` | Storage chaser | | `reader-readevent` | Checking the stream access and reading an event | | `reader-allrange` | `ReadAllEventsForward` and `ReadAllEventsBackward` | | `request-manager` | --- | | `tcp-send` | Sending messages over TCP | | `http-send` | Sending messages over HTTP | ### Enabling histograms Use the option described below to enable histograms. Because collecting histograms uses CPU resources, they are disabled by default. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--enable-histograms` | | YAML | `EnableHistograms` | | Environment variable | `EVENTSTORE_ENABLE_HISTOGRAMS` | **Default**: `false` ## Vector > Vector is a lightweight and ultra-fast tool for building observability pipelines. > (from Vector website) You can use [Vector] for extracting metrics or logs from your self-managed EventStore server. It's also possible to collect metrics from the Event Store Cloud managed cluster or instance, as long as the Vector agent is running on a machine that has a direct connection to the EventStoreDB server. You cannot, however, fetch logs from Event Store Cloud using your own Vector agent. ### Installation Follow the [installation instructions](https://vector.dev/docs/setup/installation/) provided by Vector to deploy the agent. You can deploy and run it on the same machine where you run EventStoreDB server. If you run EventStoreDB in Kubernetes, you can run Vector as a sidecar for each of the EventStoreDB pods. ### Configuration Each Vector instance needs to be configured with sources and sinks. When configured properly, it will collect information from each source, apply the necessary transformation (if needed), and send the transformed information to the configured sink. [Vector] provides [many different sinks], you most probably will find your preferred monitoring platform among those sinks. #### Collecting metrics There is an official [EventStoreDB source] that you can use to pull relevant metrics from your database. Below you can find an example that you can use in your `vector.toml` configuration file: ```toml [sources.eventstoredb_metrics] type = "eventstoredb_metrics" endpoint = "https://{hostname}:{http_port}/stats" scrape_interval_secs = 3 ``` Here `hostname` is the EventStoreDB node hostname or the cluster DNS name, and `http_port` is the configured HTTP port, which is `2113` by default. #### Collecting logs To collect logs, you can use the [file source] and configure it to target EventStoreDB log file. For log collection, Vector must run on the same machine as EventStoreDB server as it collects the logs from files on the local file system. ```toml [sources.eventstoredb_logs] type = "file" # If you changed the default log location, please update the filepath accordingly. include = ["/var/log/eventstore"] read_from = "end" ``` #### Example In this example, Vector runs on a machine as EventStoreDB, collecting metrics and logs, then sending them to Datadog. Notice that despite the EventStoreDB HTTP is, in theory, accessible via `localhost`, it won't work if the server SSL certificate doesn't have `localhost` in the certificate CN or SAN. ```toml [sources.eventstoredb_metrics] type = "eventstoredb_metrics" endpoint = "https://node1.esdb.acme.company:2113/stats" scrape_interval_secs = 10 [sources.eventstoredb_logs] type = "file" include = ["/var/log/eventstore"] read_from = "end" [sinks.dd_metrics] type = "datadog_metrics" inputs = ["eventstoredb_metrics"] api_key = "${DD_API_KEY}" default_namespace = "service" [sinks.dd_logs] type = "datadog_logs" inputs = ["sources.eventstoredb_logs"] default_api_key = "${DD_API_KEY}" compression = "gzip" ``` ## Prometheus You can export EventStoreDB metrics to Prometheus and configure Grafana dashboards to monitor your deployment. ![Grafana dashboard](images/grafana.png) Event Store v22.10 doesn't provide Prometheus support out of the box, but you can use the community-supported exporter available in the [GitHub repository](https://github.com/marcinbudny/eventstore_exporter). Later versions of EventStoreDB will have Prometheus support built-in. ## Datadog Event Store doesn't provide Datadog integration out of the box, but you can use the community-supported integration to collect EventStoreDB logs and metrics in Datadog. Find out more details about the integration in [Datadog documentation](https://docs.datadoghq.com/integrations/eventstore/). [Vector]: https://vector.dev/docs/ [EventStoreDB source]: https://vector.dev/docs/reference/configuration/sources/eventstoredb_metrics/ [file source]: https://vector.dev/docs/reference/configuration/sources/file/ [many different sinks]: https://vector.dev/docs/reference/configuration/sinks/ [Console]: https://vector.dev/docs/reference/configuration/sinks/console/ ## Elastic Elastic Stack is one of the most popular tools for ingesting and analyzing logs and statistics: * [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/index.html) was built for advanced filtering and text analysis. * [Filebeat](https://www.elastic.co/guide/en/beats/filebeat/8.2/index.html) allow tailing files efficiently. * [Logstash](https://www.elastic.co/guide/en/logstash/current/getting-started-with-logstash.html) enables log transformations and processing pipelines. * [Kibana](https://www.elastic.co/guide/en/kibana/8.2/index.html) is a dashboard and visualization UI for Elasticsearch data. EventStoreDB exposes structured information through its logs and statistics, allowing straightforward integration with mentioned tooling. ### Logstash Logstash is the plugin based data processing component of the Elastic Stack which sends incoming data to Elasticsearch. It's excellent for building a text-based processing pipeline. It can also gather logs from files (although Elastic recommends now Filebeat for that, see more in the following paragraphs). Logstash needs to either be installed on the EventStoreDB node or have access to logs storage. The processing pipeline can be configured through the configuration file (e.g. `logstash.conf`). This file contains the three essential building blocks: * input - source of logs, e.g. log files, system output, Filebeat. * filter - processing pipeline, e.g. to modify, enrich, tag log data, * output - place where we'd like to put transformed logs. Typically that contains Elasticsearch configuration. See the sample Logstash 8.2 configuration file. It shows how to take the EventStoreDB log files, split them based on the log type (regular and stats) and output them to separate indices to Elasticsearch: ```ruby ####################################################### # EventStoreDB logs file input ####################################################### input { file { path => "/var/log/eventstore/*/log*.json" start_position => "beginning" codec => json } } ####################################################### # Filter out stats from regular logs # add respecting field with log type ####################################################### filter { # check if log path includes "log-stats" # so pattern for stats if [log][file][path] =~ "log-stats" { mutate { add_field => { "log_type" => "stats" } } } else { mutate { add_field => { "log_type" => "logs" } } } } ####################################################### # Send logs to Elastic # Create separate indexes for stats and regular logs # using field defined in the filter transformation ####################################################### output { elasticsearch { hosts => [ "elasticsearch:9200" ] index => 'eventstoredb-%{[log_type]}' } } ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/Logstash/docker-compose.yml). ### Filebeat Logstash was the initial Elastic try to provide a log harvester tool. However, it appeared to have performance limitations. Elastic came up with the [Beats family](https://www.elastic.co/beats/), which allows gathering data from various specialized sources (files, metrics, network data, etc.). Elastic recommends Filebeat as the log collection and shipment tool off the host servers. Filebeat uses a backpressure-sensitive protocol when sending data to Logstash or Elasticsearch to account for higher volumes of data. Filebeat can pipe logs directly to Elasticsearch and set up a Kibana data view. Filebeat needs to either be installed on the EventStoreDB node or have access to logs storage. The processing pipeline can be configured through the configuration file (e.g. `filebeat.yml`). This file contains the three essential building blocks: * input - configuration for file source, e.g. if stored in JSON format. * output - place where we'd like to put transformed logs, e.g. Elasticsearch, Logstash, * setup - additional setup and simple transformations (e.g. Elasticsearch indices template, Kibana data view). See the sample Filebeat 8.2 configuration file. It shows how to take the EventStoreDB log files, output them to Elasticsearch prefixing index with `eventstoredb` and create a Kibana data view: ```yml ####################################################### # EventStoreDB logs file input ####################################################### filebeat.inputs: - type: log paths: - /var/log/eventstore/*/log*.json json.keys_under_root: true json.add_error_key: true ####################################################### # ElasticSearch direct output ####################################################### output.elasticsearch: index: "eventstoredb-%{[agent.version]}" hosts: ["elasticsearch:9200"] ####################################################### # ElasticSearch dashboard configuration # (index pattern and data view) ####################################################### setup.dashboards: enabled: true index: "eventstoredb-*" setup.template: name: "eventstoredb" pattern: "eventstoredb-%{[agent.version]}" ####################################################### # Kibana dashboard configuration ####################################################### setup.kibana: host: "kibana:5601" ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/Filebeat/docker-compose.yml). ### Filebeat with Logstash Even though Filebeat can pipe logs directly to Elasticsearch and do a basic Kibana setup, you'd like to have more control and expand the processing pipeline. That's why for production, it's recommended to use both. Multiple Filebeat instances (e.g. from different EventStoreDB clusters) can collect logs and pipe them to Logstash, which will play an aggregator role. Filebeat can output logs to Logstash, and Logstash can receive and process these logs with the Beats input. Logstash can transform and route logs to Elasticsearch instance(s). In that configuration, Filebeat should be installed on the EventStoreDB node (or have access to file logs) and define Logstash as output. See the sample Filebeat 8.2 configuration file. ```yml ####################################################### # EventStoreDB logs file input ####################################################### filebeat.inputs: - type: log paths: - /var/log/eventstore/*/log*.json json.keys_under_root: true json.add_error_key: true ####################################################### # Logstash output to transform and prepare logs ####################################################### output.logstash: hosts: ["logstash:5044"] ``` Then the sample Logstash 8.2 configuration file will look like the below. It shows how to take the EventStoreDB logs from Filebeat, split them based on the log type (regular and stats) and output them to separate indices to Elasticsearch: ```ruby ####################################################### # Filebeat input ####################################################### input { beats { port => 5044 } } ####################################################### # Filter out stats from regular logs # add respecting field with log type ####################################################### filter { # check if log path includes "log-stats" # so pattern for stats if [log][file][path] =~ "log-stats" { mutate { add_field => { "log_type" => "stats" } } } else { mutate { add_field => { "log_type" => "logs" } } } } ####################################################### # Send logs to Elastic # Create separate indexes for stats and regular logs # using field defined in the filter transformation ####################################################### output { elasticsearch { hosts => [ "elasticsearch:9200" ] index => 'eventstoredb-%{[log_type]}' } } ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/FilebeatWithLogstash/docker-compose.yml). --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/index.md' --- # HTTP API Reference documentation of HTTP API for EventStoreDB v22.10. --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/api.md' --- # HTTP API Reference The Base URL used in this documentation is https://eventstore.com, you should replace it with the same url you use to view the administration UI ## Authentication * HTTP Authentication, scheme: basic ## Streams Endpoints for Stream operations ### Read a stream > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream} ``` `GET /streams/{stream}` *Reads a stream* Read a stream, receiving a standard AtomFeed document as a response. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Append to a stream > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/streams/{stream} \ -H 'Content-Type: application/json' \ -H 'ES-ExpectedVersion: 0' \ -H 'ES-EventType: string' \ -H 'ES-EventId: 0' \ -H 'ES-RequiresMaster: true' ``` `POST /streams/{stream}` *Append to a stream* Append to a stream. > Body parameter ```json { "body": {} } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The name of the stream| |ES-ExpectedVersion|header|integer|false|Expected stream version| |ES-EventType|header|string|false|The event type associated to a posted body| |ES-EventId|header|integer|false|Event ID associated to a posted body| |ES-RequiresMaster|header|boolean|false|Wether to run on a master node| |body|body|[streamData](#schemastreamdata)|true|Stream events to create| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New stream created|None| |307|[Temporary Redirect](https://tools.ietf.org/html/rfc7231#section-6.4.7)|Temporary Redirect|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Append request body invalid|None| ### Delete a stream > Code samples ```shell # You can also use wget curl -X DELETE https://eventstore.com/streams/{stream} ``` `DELETE /streams/{stream}` *Deletes a stream* Delete specified stream |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID to delete| |Status|Meaning|Description|Schema| |---|---|---|---| |204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Stream deleted|None| ### Alternative stream URL > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/streams/{stream}/incoming/{guid} ``` `POST /streams/{stream}/incoming/{guid}` *An alternative URL to post events to* A URL generated by EventStoreDB if you don't supply an ID when creating a stream. You then use this URL to post events to. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The name of the stream| |guid|path|string|true|Autogenerated UUID| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New event created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Read stream event > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream}/{event} ``` `GET /streams/{stream}/{event}` *Read a stream event* Reads a single event from a stream. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |event|path|string|true|The event ID| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get {n} events > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream}/{event}/{count} ``` `GET /streams/{stream}/{event}/{count}` *Paginate backwards through stream events* Paginate backwards though stream events by a specified amount. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |event|path|string|true|The event ID| |count|path|integer(int64)|true|How many events to skip backwards from in the request.| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Page back through events > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream}/{event}/backward/{count} ``` `GET /streams/{stream}/{event}/backward/{count}` *Paginate backwards through stream events* Paginate backwards though stream events by a specified amount. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |event|path|string|true|The event ID| |count|path|integer(int64)|true|How many events to skip backwards from in the request.| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Page forward through events > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream}/{event}/forward/{count} ``` `GET /streams/{stream}/{event}/forward/{count}` *Paginate forwards through stream events* Paginate forwards though stream events by a specified amount. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |event|path|string|true|The event ID| |count|path|integer(int64)|true|How many events to skip forwards in the request.| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Read stream metadata > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/{stream}/metadata ``` `GET /streams/{stream}/metadata` *Reads the metadata of a stream* Returns metadata of a stream, typically information associated with an event that is not part of the event. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream ID| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Update stream metadata > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/streams/{stream}/metadata \ -H 'Content-Type: application/json' ``` `POST /streams/{stream}/metadata` *Update stream metadata* Update the metadata of a stream. > Body parameter ```json { "eventId": "string", "eventType": "string", "data": { "maxAge": 0, "maxCount": 0, "truncateBefore": 0, "cacheControl": "string", "acl": { "r": "string", "w": "string", "d": "string", "mr": "string", "mw": "string" } } } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The name of the stream| |body|body|[StreamMetadataItem](#schemastreammetadataitem)|false|Metadata object| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New stream created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Get all events > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/streams/$all ``` `GET /streams/$all` *Returns all events from all streams* Returns all events from all streams, you must provide user details. |Name|In|Type|Required|Description| |---|---|---|---|---| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ## Subscriptions Endpoints for Subscription operations ### Get all subscriptions > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/subscriptions ``` `GET /subscriptions` *Get information for all subscriptions* Returns all subscriptions from all streams. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New persistant subscription|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|bad input parameter|None| ### Get subscription stream information > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/subscriptions/{stream} ``` `GET /subscriptions/{stream}` *Returns information about the subscriptions for a stream* Needed |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream name| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Get subscription information > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/subscriptions/{stream}/{subscription}/info ``` `GET /subscriptions/{stream}/{subscription}/info` *Reads stream information via a persistent subscription* Needed |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Get a stream > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/subscriptions/{stream}/{subscription} ``` `GET /subscriptions/{stream}/{subscription}` *Read a stream* Read a specified stream by a persistent subscription. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |embed|query|string|false|Needed| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Update subscription > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription} \ -H 'Content-Type: application/json' ``` `POST /subscriptions/{stream}/{subscription}` *Update a persistant subscription* You can edit the settings of an existing subscription while it is running. This will drop the current subscribers and will reset the subscription internally. > Body parameter ```json { "minCheckPointCount": 2, "startFrom": 0, "ResolveLinkTos": true, "readBatchSize": 5, "namedConsumerStrategy": "RoundRobin", "extraStatistics": true, "maxRetryCount": 7, "liveBufferSize": 1, "messageTimeoutMilliseconds": 3, "maxCheckPointCount": 2, "maxSubscriberCount": 9, "checkPointAfterMilliseconds": 6, "bufferSize": 5 } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |body|body|[SubscriptionItem](#schemasubscriptionitem)|false|Subscription to create| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Subscription updated|None| ### Create subscription > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/subscriptions/{stream}/{subscription} \ -H 'Content-Type: application/json' ``` `PUT /subscriptions/{stream}/{subscription}` *Create a persistent subscription* Before interacting with a subscription group, you need to create one. You will receive an error if you attempt to create a subscription group more than once. This requires [admin permissions](../security.md#access-control-lists). > Body parameter ```json { "minCheckPointCount": 2, "startFrom": 0, "ResolveLinkTos": true, "readBatchSize": 5, "namedConsumerStrategy": "RoundRobin", "extraStatistics": true, "maxRetryCount": 7, "liveBufferSize": 1, "messageTimeoutMilliseconds": 3, "maxCheckPointCount": 2, "maxSubscriberCount": 9, "checkPointAfterMilliseconds": 6, "bufferSize": 5 } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |body|body|[SubscriptionItem](#schemasubscriptionitem)|false|Subscription to create| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Subscription created|None| ### Delete subscription > Code samples ```shell # You can also use wget curl -X DELETE https://eventstore.com/subscriptions/{stream}/{subscription} ``` `DELETE /subscriptions/{stream}/{subscription}` *Deletes a subscription* Deletes a subscription |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Get {n} subscription events > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/subscriptions/{stream}/{subscription}/{count} ``` `GET /subscriptions/{stream}/{subscription}/{count}` *Reads a stream via a persistent subscription and return a specific number of events* Reads a stream via a persistent subscription and return a specific number of events |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |count|path|integer(int64)|true|How many events to return for the request.| |embed|query|string|false|none| #### Enumerated Values |Parameter|Value| |---|---| |embed|None| |embed|Content| |embed|Rich| |embed|Body| |embed|PrettyBody| |embed|TryHarder| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Acknowledge a single message > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription}/ack/{messageid} ``` `POST /subscriptions/{stream}/{subscription}/ack/{messageid}` *Acknowledge a single message* Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If the client fails to respond in the given timeout period, the message will be retried. You should use the `rel` links in the feed for acknowledgements not bookmark URIs as they are subject to change in future versions. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |messageid|path|string|true|The id of the message that needs to be acked| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New persistant subscription|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|bad input parameter|None| ### Acknowledge multiple messages > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription}/ack ``` `POST /subscriptions/{stream}/{subscription}/ack` *Acknowledge multiple messages* Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If the client fails to respond in the given timeout period, the message will be retried. You should use the `rel` links in the feed for acknowledgements not bookmark URIs as they are subject to change in future versions. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |ids|query|string|false|The ids of the messages that need to be acked separated by commas| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New persistant subscription|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|bad input parameter|None| ### Don't acknowledge a single message > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription}/nack/{messageid} ``` `POST /subscriptions/{stream}/{subscription}/nack/{messageid}` *Negative acknowledge a single message* Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If the client fails to respond in the given timeout period, the message will be retried. You should use the `rel` links in the feed for acknowledgements not bookmark URIs as they are subject to change in future versions. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |messageid|path|string|true|The id of the message that needs to be nacked| |action|query|string|false|**Park** - Don't retry the message, park it until a request is sent to reply the parked messages**Retry** - Retry the message**Skip** - Discard the message**Stop** - Stop the subscription| #### Enumerated Values |Parameter|Value| |---|---| |action|Park| |action|Retyr| |action|Skip| |action|Stop| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New persistant subscription|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|bad input parameter|None| ### Don't acknowledge multiple messages > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription}/nack ``` `POST /subscriptions/{stream}/{subscription}/nack` *Negative acknowledge multiple messages* Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If the client fails to respond in the given timeout period, the message will be retried. You should use the `rel` links in the feed for acknowledgements not bookmark URIs as they are subject to change in future versions. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |ids|query|string|false|The ids of the messages that need to be nacked separated by commas| |action|query|string|false|**Park** - Don't retry the message, park it until a request is sent to reply the parked messages**Retry** - Retry the message**Skip** - Discard the message**Stop** - Stop the subscription| #### Enumerated Values |Parameter|Value| |---|---| |action|Park| |action|Retry| |action|Skip| |action|Stop| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|New persistant subscription|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|bad input parameter|None| ### Replay previously parked messages > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/subscriptions/{stream}/{subscription}/replayParked ``` `POST /subscriptions/{stream}/{subscription}/replayParked` *Replay any previously parked messages in a stream* Replay any previously parked messages in a stream that were parked by a negative acknowledgement action. |Name|In|Type|Required|Description| |---|---|---|---|---| |stream|path|string|true|The stream the persistent subscription is on| |subscription|path|string|true|The name of the subscription group| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ## Projections Endpoints for Projection operations ### Get all projections > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projections/any ``` `GET /projections/any` *Get all projections* Returns all projections defined in EventStoreDB. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get all non-transient projections > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projections/all-non-transient ``` `GET /projections/all-non-transient` *Get all non-transient projections* Returns all known projections except ad-hoc projections. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get all queries > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projections/onetime ``` `GET /projections/onetime` *Get all queries* Returns all queries defined in EventStoreDB. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Create a onetime projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projections/onetime ``` `POST /projections/onetime` *Create a onetime projection* Create a new onetime projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|query|string|false|Name of the projection| |type|query|string|false|The projection type| |enabled|query|boolean|false|Is the projection enabled| |checkpoints|query|boolean|false|Are checkpoints enabled| |emit|query|boolean|false|Is emit enabled| |trackemittedstreams|query|boolean|false|Should your projection create a separate stream and write any streams it emits to that stream.| #### Enumerated Values |Parameter|Value| |---|---| |type|JS| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New projection created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Get all continious projections > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projections/continuous ``` `GET /projections/continuous` *Get all continious projections* Returns all continually running projections defined in EventStoreDB. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Create a continuous projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projections/continuous ``` `POST /projections/continuous` *Create a continious projection* Create a new continious projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|query|string|false|Name of the projection| |enabled|query|boolean|false|Is the projection enabled| |checkpoints|query|boolean|false|Are checkpoints enabled| |emit|query|boolean|false|Is emit enabled| |type|query|string|false|The projection type| |trackemittedstreams|query|boolean|false|Should your projection create a separate stream and write any streams it emits to that stream.| #### Enumerated Values |Parameter|Value| |---|---| |type|JS| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New projection created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Read projection events based on a query > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projections/read-events ``` `POST /projections/read-events` *Read events from projection based on a query definition* Read events from projection based on a query definition, i.e. fromAll, fromStream, fromStreams |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get all transient projections > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projections/transient ``` `GET /projections/transient` *Get all transient projections* Returns all transient projections defined in EventStoreDB. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Create a transient projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projections/transient ``` `POST /projections/transient` *Create a transient projection* Create a new transient projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|query|string|false|Name of the projection| |type|query|string|false|The projection type| |enabled|query|boolean|false|Is the projection enabled| #### Enumerated Values |Parameter|Value| |---|---| |type|JS| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New user created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Get projection definition > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name}/query ``` `GET /projection/{name}/query` *Get projection definition* Returns definition of the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |config|query|boolean|false|Wether to return the projection definition config.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Update projection definition > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/projection/{name}/query ``` `PUT /projection/{name}/query` *Update projection definition* Update the specified projection definition. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |type|query|string|false|The projection type| |emit|query|boolean|false|Is emit enabled| #### Enumerated Values |Parameter|Value| |---|---| |type|JS| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get the projection state > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name}/state ``` `GET /projection/{name}/state` *Get the projection state* Return the current state of the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |partition|query|string|false|The partition name in state| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get result of projection > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name}/result ``` `GET /projection/{name}/result` *Get result of projection* Get the final result of a projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |partition|query|string|false|The partition name in state| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get projection statistics > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name}/statistics ``` `GET /projection/{name}/statistics` *Get projection statistics* Returns the statistics for a projection, such as how many events, the status etc. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Disable projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projection/{name}/command/disable ``` `POST /projection/{name}/command/disable` *Disable projection* Disable the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |enableRunAs|query|boolean|false|Run as the user issuing the command.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Enable projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projection/{name}/command/enable ``` `POST /projection/{name}/command/enable` *Enable projection* Enable the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |enableRunAs|query|boolean|false|Run as the user issuing the command.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Reset projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projection/{name}/command/reset ``` `POST /projection/{name}/command/reset` *Reset projection* Reset the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |enableRunAs|query|boolean|false|Run as the user issuing the command.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Abort projection > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/projection/{name}/command/abort ``` `POST /projection/{name}/command/abort` *Abort projection* Abort the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |enableRunAs|query|boolean|false|Run as the user issuing the command.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get projection config > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name}/config ``` `GET /projection/{name}/config` *Get the config of a projection* Returns the performance configuration of the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Update projection config > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/projection/{name}/config ``` `PUT /projection/{name}/config` *Update the config of a projection* Update the performance configuration of the specified projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get a projection > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/projection/{name} ``` `GET /projection/{name}` *Get a projection* Returns a specific projection. |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The name of the projection| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Delete a projection > Code samples ```shell # You can also use wget curl -X DELETE https://eventstore.com/projection/{name} ``` `DELETE /projection/{name}` *Deletes a projection* Deletes a projection |Name|In|Type|Required|Description| |---|---|---|---|---| |name|path|string|true|The projection to delete| |deleteStateStream|query|boolean|false|TBD| |deleteCheckpointStream|query|boolean|false|TBD| |deleteEmittedStreams|query|boolean|false|TBD| |Status|Meaning|Description|Schema| |---|---|---|---| |204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|Projection deleted|None| ## Admin Endpoints for Admin operations ### Shutdown a node > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/admin/shutdown ``` `POST /admin/shutdown` *Shutdown a node* Issues a shut down command to a node. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| ### Scavenge a node > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/admin/scavenge ``` `POST /admin/scavenge` *Scavenge a node* Scavenge reclaims disk space by rewriting database chunks, minus the events to delete, and then deleting the old chunks. |Name|In|Type|Required|Description| |---|---|---|---|---| |startFromChunk|query|integer|false|The chunk ID to start the scavenge operation from.| |threads|query|integer|false|The number of threads to run the scavenge operation on (max 4).| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| ### Stop a scavenge > Code samples ```shell # You can also use wget curl -X DELETE https://eventstore.com/admin/scavenge/{scavengeId} ``` `DELETE /admin/scavenge/{scavengeId}` *Stop a scavenge operation* Stop a running scavenge operation. |Name|In|Type|Required|Description| |---|---|---|---|---| |scavengeId|path|integer|true|The scavenge ID| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| ### Merge Indexes > Code samples ```shell # You can also use wget curl -X POST -d{} https://eventstore.com/admin/mergeindexes ``` `POST /admin/mergeindexes` *Merge indexes* Manually merge indexes after a scavenge operation |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| Endpoints for Info operations ### Get info for node > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/info ``` `GET /info` *Get info for node* Returns information about node. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| ### Get configuration for node > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/info/options ``` `GET /info/options` *Get configuration for node* Returns configuration details about node. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| ## Users Endpoints for User operations ### Get all users > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/users/ ``` `GET /users/` *Get all users* Returns all users defined in EventStoreDB. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Create a user > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/users/ \ -H 'Content-Type: application/json' ``` `POST /users/` *Create a User* Create a new user. > Body parameter ```json { "LoginName": "admin", "FullName": "EventStore Admin", "Groups": [ "Admin", "DataScience" ], "Password": "aVerySecurePassword" } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |body|body|[UserItem](#schemauseritem)|false|User to create| |Status|Meaning|Description|Schema| |---|---|---|---| |201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|New user created|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| |401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Unauthorized|None| ### Get a user > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/users/{login} ``` `GET /users/{login}` *Get user* Returns the user currently authenticated with the API, or the user specified. |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user passed to the API call.| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Update a user > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/users/{login} \ -H 'Content-Type: application/json' ``` `PUT /users/{login}` *Update specified user* Update the FullName of Groups of the specified user. > Body parameter ```json { "FullName": "EventStore Admin", "Groups": [ "Admin", "DataScience" ] } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |body|body|[UserUpdateItem](#schemauserupdateitem)|false|User to update| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Delete a user > Code samples ```shell # You can also use wget curl -X DELETE https://eventstore.com/users/{login} ``` `DELETE /users/{login}` *Deletes a user* Delete specified user. |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |Status|Meaning|Description|Schema| |---|---|---|---| |204|[No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5)|User deleted|None| ### Enable a user > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/users/{login}/command/enable ``` `PUT /users/{login}/command/enable` *Enable the specified user* Enable the acount of the specified user. |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Disable a user > Code samples ```shell # You can also use wget curl -X PUT https://eventstore.com/users/{login}/command/disable ``` `PUT /users/{login}/command/disable` *Disable the specified user* Disable the acount of the specified user. |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Reset password > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/users/{login}/command/reset-password \ -H 'Content-Type: application/json' ``` `POST /users/{login}/command/reset-password` *Reset user password* Reset the password of the specified user. > Body parameter ```json { "NewPassword": "aNewSecurePassword" } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |body|body|[PasswordResetItem](#schemapasswordresetitem)|true|The new password for the user| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ### Change password > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/users/{login}/command/change-password \ -H 'Content-Type: application/json' ``` `POST /users/{login}/command/change-password` *Change user password* Change the password of the specified user. > Body parameter ```json { "CurrentPassword": "anOldSecurePassword", "NewPassword": "aNewSecurePassword" } ``` |Name|In|Type|Required|Description| |---|---|---|---|---| |login|path|string|true|The user's name| |body|body|[PasswordChangeItem](#schemapasswordchangeitem)|true|The new password for the user| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request|None| ## Stats Endpoints for Statistics operations. ### Get all stats > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/stats \ -H 'Accept: application/json' ``` `GET /stats` *Get all stats* Returns all stats enabled for EventStoreDB. > Example responses > 200 Response ```json { "proc": { "startTime": "string", "id": 0, "mem": 0, "cpu": 0, "cpuScaled": 0, "threadsCount": 0, "contentionsRate": 0, "thrownExceptionsRate": 0, "gc": { "allocationSpeed": 0, "gen0ItemsCount": 0, "gen0Size": 0, "gen1ItemsCount": 0, "gen1Size": 0, "gen2ItemsCount": 0, "gen2Size": 0, "largeHeapSize": 0, "timeInGc": 0, "totalBytesInHeaps": 0 }, "diskIo": { "readBytes": 0, "writtenBytes": 0, "readOps": 0, "writeOps": 0 }, "tcp": { "connections": 0, "receivingSpeed": "string", "sendingSpeed": 0, "inSend": 0, "measureTime": 0, "pendingReceived": 0, "pendingSend": 0, "receivedBytesSinceLastRun": 0, "receivedBytesTotal": 0, "sentBytesSinceLastRun": 0, "sentBytesTotal": 0 } }, "sys": { "cpu": 0, "freeMem": 0, "drive": { "driveName": { "availableBytes": 0, "totalBytes": 0, "usage": 0, "usedBytes": 0 } } }, "es": { "checksum": 0, "checksumNonFlushed": 0, "queue": { "queueName": "string", "groupName": "string", "avgItemsPerSecond": 0, "avgProcessingTime": 0, "currentIdleTime": "string", "currentItemProcessingTime": "string", "idleTimePercent": 0, "length": 0, "lengthCurrentTryPeak": 0, "lengthLifetimePeak": 0, "totalItemsProcessed": 0, "inProgressMessage": "string", "lastProcessedMessage": "string" }, "writer": { "lastFlushSize": 0, "lastFlushDelayMs": 0, "meanFlushSize": 0, "meanFlushDelayMs": 0, "maxFlushSize": 0, "maxFlushDelayMs": 0, "queuedFlushMessages": 0 }, "readIndex": { "cachedRecord": 0, "notCachedRecord": 0, "cachedStreamInfo": 0, "notCachedStreamInfo": 0, "cachedTransInfo": 0, "notCachedTransInfo": 0, "hashCollisions": 0 } } } ``` ```xml string 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 string 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 string string 0 0 string string 0 0 0 0 0 string string 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ``` |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|A list of stats|[Stats](#schemastats)| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Get specified stat > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/stats/{statPath} ``` `GET /stats/{statPath}` *Get stats sub path* Returns the sub path of the EventStoreDB statistics available. |Name|In|Type|Required|Description| |---|---|---|---|---| |statPath|path|string|true|The stats sub path| |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ## Gossip ### Return Gossip details > Code samples ```shell # You can also use wget curl -X GET https://eventstore.com/gossip ``` `GET /gossip` *Return Gossip details for cluster* Return Gossip details for nodes in cluster. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ### Update Gossip details > Code samples ```shell # You can also use wget curl -X POST https://eventstore.com/gossip ``` `POST /gossip` *Update Gossip details for cluster* Update Gossip details for nodes in a cluster. |Status|Meaning|Description|Schema| |---|---|---|---| |200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|OK|None| |404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|Not found|None| ## Schemas ### UserItem ```json { "LoginName": "admin", "FullName": "EventStore Admin", "Groups": [ "Admin", "DataScience" ], "Password": "aVerySecurePassword" } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |LoginName|string|false|none|The new users login name.| |FullName|string|false|none|The full name for the new user.| |Groups|\[string]|false|none|The groups the new user is a member of.| |Password|string|false|none|The password for the new user.| ### UserUpdateItem ```json { "FullName": "EventStore Admin", "Groups": [ "Admin", "DataScience" ] } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |FullName|string|false|none|The full name of the new user.| |Groups|\[string]|false|none|The groups the new user should become a member of.| ### PasswordResetItem ```json { "NewPassword": "aNewSecurePassword" } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |NewPassword|string|false|none|The new password for the user| ### PasswordChangeItem ```json { "CurrentPassword": "anOldSecurePassword", "NewPassword": "aNewSecurePassword" } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |CurrentPassword|string|false|none|The current password for the user| |NewPassword|string|false|none|The new password for the user| ### streamData ```json { "body": {} } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |body|object|true|none|Event data| ### StreamItem ```json { "minCheckPointCount": 2, "startFrom": 0, "ResolveLinkTos": true, "readBatchSize": 5, "namedConsumerStrategy": "RoundRobin", "extraStatistics": true, "maxRetryCount": 7, "liveBufferSize": 1, "messageTimeoutMilliseconds": 3, "maxCheckPointCount": 2, "maxSubscriberCount": 9, "checkPointAfterMilliseconds": 6, "bufferSize": 5 } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |ResolveLinkTos|boolean|false|none|Whether to resolve link events| |startFrom|integer(int64)|false|none|Which event position in the stream the subscription should start from| |extraStatistics|boolean|false|none|Whether to track latency statistics on this subscription| |checkPointAfterMilliseconds|integer(int64)|false|none|The amount of time to try to checkpoint after| |liveBufferSize|integer(int64)|false|none|The size of the buffer (in-memory) listening to live messages as they happen before paging occurs| |readBatchSize|integer(int64)|false|none|The number of events to read per batch when reading the history| |bufferSize|integer(int64)|false|none|The number of events to cache when paging through history| |maxCheckPointCount|integer(int64)|false|none|The maximum number of messages not checkpointed before forcing a checkpoint| |maxRetryCount|integer(int64)|false|none|The maximum number of retries (due to timeout) before a message is considered to be parked| |maxSubscriberCount|integer(int64)|false|none|The maximum number of TCP subscribers allowed| |messageTimeoutMilliseconds|integer(int64)|false|none|The amount of time after which to consider a message as timedout and retried| |minCheckPointCount|integer(int64)|false|none|The minimum number of messages to write to a checkpoint| |namedConsumerStrategy|string|false|none|The strategy to use for distributing events to client consumers| #### Enumerated Values |Property|Value| |---|---| |namedConsumerStrategy|RoundRobin| |namedConsumerStrategy|DispatchToSingle| |namedConsumerStrategy|Pinned| ### StreamMetadataItem ```json { "eventId": "string", "eventType": "string", "data": { "maxAge": 0, "maxCount": 0, "truncateBefore": 0, "cacheControl": "string", "acl": { "r": "string", "w": "string", "d": "string", "mr": "string", "mw": "string" } } } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |eventId|string|false|none|Alphanumeric ID| |eventType|string|false|none|The type of event| |data|[StreamMetadataItem\_data](#schemastreammetadataitem_data)|false|none|none| ### SubscriptionItem ```json { "minCheckPointCount": 2, "startFrom": 0, "ResolveLinkTos": true, "readBatchSize": 5, "namedConsumerStrategy": "RoundRobin", "extraStatistics": true, "maxRetryCount": 7, "liveBufferSize": 1, "messageTimeoutMilliseconds": 3, "maxCheckPointCount": 2, "maxSubscriberCount": 9, "checkPointAfterMilliseconds": 6, "bufferSize": 5 } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |ResolveLinkTos|boolean|false|none|Whether to resolve link events| |startFrom|integer(int64)|false|none|Which event position in the stream the subscription should start from| |extraStatistics|boolean|false|none|Whether to track latency statistics on this subscription| |checkPointAfterMilliseconds|integer(int64)|false|none|The amount of time to try to checkpoint after| |liveBufferSize|integer(int64)|false|none|The size of the buffer (in-memory) listening to live messages as they happen before paging occurs| |readBatchSize|integer(int64)|false|none|The number of events to read per batch when reading the history| |bufferSize|integer(int64)|false|none|The number of events to cache when paging through history| |maxCheckPointCount|integer(int64)|false|none|The maximum number of messages not checkpointed before forcing a checkpoint| |maxRetryCount|integer(int64)|false|none|The maximum number of retries (due to timeout) before a message is considered to be parked| |maxSubscriberCount|integer(int64)|false|none|The maximum number of TCP subscribers allowed| |messageTimeoutMilliseconds|integer(int64)|false|none|The amount of time after which to consider a message as timedout and retried| |minCheckPointCount|integer(int64)|false|none|The minimum number of messages to write to a checkpoint| |namedConsumerStrategy|string|false|none|The strategy to use for distributing events to client consumers| #### Enumerated Values |Property|Value| |---|---| |namedConsumerStrategy|RoundRobin| |namedConsumerStrategy|DispatchToSingle| |namedConsumerStrategy|Pinned| ### StreamMetadataItem\_data ```json { "maxAge": 0, "maxCount": 0, "truncateBefore": 0, "cacheControl": "string", "acl": { "r": "string", "w": "string", "d": "string", "mr": "string", "mw": "string" } } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |maxAge|integer|false|none|The maximum age of events in the stream| |maxCount|integer|false|none|The maximum count of events in the stream| |truncateBefore|integer|false|none|Events prior to this event are truncated and removed| |cacheControl|string|false|none|Period of time to make feed head cacheable| |acl|object|false|none|Access control list for this stream| |» r|string|false|none|Read roles| |» w|string|false|none|Write roles| |» d|string|false|none|Delete roles| |» mr|string|false|none|Metadata read roles| |» mw|string|false|none|Metadata write roles| ### Stats ```json { "proc": { "startTime": "string", "id": 0, "mem": 0, "cpu": 0, "cpuScaled": 0, "threadsCount": 0, "contentionsRate": 0, "thrownExceptionsRate": 0, "gc": { "allocationSpeed": 0, "gen0ItemsCount": 0, "gen0Size": 0, "gen1ItemsCount": 0, "gen1Size": 0, "gen2ItemsCount": 0, "gen2Size": 0, "largeHeapSize": 0, "timeInGc": 0, "totalBytesInHeaps": 0 }, "diskIo": { "readBytes": 0, "writtenBytes": 0, "readOps": 0, "writeOps": 0 }, "tcp": { "connections": 0, "receivingSpeed": "string", "sendingSpeed": 0, "inSend": 0, "measureTime": 0, "pendingReceived": 0, "pendingSend": 0, "receivedBytesSinceLastRun": 0, "receivedBytesTotal": 0, "sentBytesSinceLastRun": 0, "sentBytesTotal": 0 } }, "sys": { "cpu": 0, "freeMem": 0, "drive": { "driveName": { "availableBytes": 0, "totalBytes": 0, "usage": 0, "usedBytes": 0 } } }, "es": { "checksum": 0, "checksumNonFlushed": 0, "queue": { "queueName": "string", "groupName": "string", "avgItemsPerSecond": 0, "avgProcessingTime": 0, "currentIdleTime": "string", "currentItemProcessingTime": "string", "idleTimePercent": 0, "length": 0, "lengthCurrentTryPeak": 0, "lengthLifetimePeak": 0, "totalItemsProcessed": 0, "inProgressMessage": "string", "lastProcessedMessage": "string" }, "writer": { "lastFlushSize": 0, "lastFlushDelayMs": 0, "meanFlushSize": 0, "meanFlushDelayMs": 0, "maxFlushSize": 0, "maxFlushDelayMs": 0, "queuedFlushMessages": 0 }, "readIndex": { "cachedRecord": 0, "notCachedRecord": 0, "cachedStreamInfo": 0, "notCachedStreamInfo": 0, "cachedTransInfo": 0, "notCachedTransInfo": 0, "hashCollisions": 0 } } } ``` #### Properties |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |proc|object|false|none|Stats on the currently active process| |» startTime|string|false|none|Time the associated process started| |» id|integer|false|none|Id of the associated process| |» mem|integer|false|none|Virtual memory used by the associated process| |» cpu|number|false|none|CPU usage of the process| |» cpuScaled|number|false|none|CPU usage of the process scaled by logical processor count| |» threadsCount|integer|false|none|Number of threads used by process| |» contentionsRate|number|false|none|The rate at which threads in the process attempt to acquire a managed lock unsuccessfully| |» thrownExceptionsRate|number|false|none|Number of exceptions thrown per second| |» gc|object|false|none|Stats on garbage collection| |»» allocationSpeed|number|false|none|Memory allocation speed| |»» gen0ItemsCount|number|false|none|Number of generation 0 garbage collections| |»» gen0Size|number|false|none|Generation 0 heap size| |»» gen1ItemsCount|number|false|none|Number of generation 1 garbage collections| |»» gen1Size|number|false|none|Generation 1 heap size| |»» gen2ItemsCount|number|false|none|Number of generation 2 garbage collections| |»» gen2Size|number|false|none|Generation 2 heap size| |»» largeHeapSize|number|false|none|Large object heap size| |»» timeInGc|number|false|none|Percentage of time in garbage collection| |»» totalBytesInHeaps|number|false|none|Total bytes in all heaps| |» diskIo|object|false|none|Disk input and output stats| |»» readBytes|number|false|none|The number of bytes read by EventStoreDB since server start| |»» writtenBytes|number|false|none|The number of bytes written by EventStoreDB since server start| |»» readOps|number|false|none|The number of read operations by EventStoreDB since server start| |»» writeOps|number|false|none|The number of write operations by EventStoreDB since server start| |» tcp|object|false|none|TCP connection stats| |»» connections|integer|false|none|Number of TCP connections to EventStoreDB| |»» receivingSpeed|string|false|none|Receiving speed in bytes per second| |»» sendingSpeed|number|false|none|Sending speed in bytes per second| |»» inSend|number|false|none|Number of bytes sent to connections but not yet acknowledged by the receiving party| |»» measureTime|number|false|none|Time elapsed since last stats read| |»» pendingReceived|number|false|none|Number of bytes waiting to be received by connections| |»» pendingSend|number|false|none|Number of bytes waiting to be sent to connections| |»» receivedBytesSinceLastRun|number|false|none|Total bytes received by TCP connections since last run| |»» receivedBytesTotal|number|false|none|Total bytes received by TCP connections| |»» sentBytesSinceLastRun|number|false|none|Total bytes sent to TCP connections since last run| |»» sentBytesTotal|number|false|none|Total bytes sent from TCP connections| |sys|object|false|none|System usage stats| |» cpu|number|false|none|Total CPU usage in percentage| |» freeMem|number|false|none|Free memory in bytes| |» drive|object|false|none|Drive usage stats| |»» driveName|object|false|none|Drive path| |»»» availableBytes|number|false|none|Remaining bytes of space available to EventStoreDB| |»»» totalBytes|number|false|none|Total bytes of space available to EventStoreDB| |»»» usage|number|false|none|Percentage usage of space used by EventStoreDB| |»»» usedBytes|number|false|none|Total bytes of space used by EventStoreDB| |es|object|false|none|none| |» checksum|number|false|none|none| |» checksumNonFlushed|number|false|none|none| |» queue|object|false|none|Multiple queue instance stats| |»» queueName|string|false|none|Queue name| |»» groupName|string|false|none|Group queue is a member of| |»» avgItemsPerSecond|integer|false|none|The average number of items processed per second by the queue| |»» avgProcessingTime|number|false|none|Average number of items processed per second| |»» currentIdleTime|string|false|none|Time elapsed since queue went idle| |»» currentItemProcessingTime|string|false|none|Time elapsed processing the current item| |»» idleTimePercent|number|false|none|Percentage of time queue spent idle| |»» length|integer|false|none|Number of items in the queue| |»» lengthCurrentTryPeak|number|false|none|The highest number of items in the queue within the past 100ms| |»» lengthLifetimePeak|number|false|none|The highest number of items in the queue| |»» totalItemsProcessed|number|false|none|The total number of items processed by the queue| |»» inProgressMessage|string|false|none|Current message type queue is processing| |»» lastProcessedMessage|string|false|none|Last message type processed| |» writer|object|false|none|Storage writing stats| |»» lastFlushSize|number|false|none|Last flush size| |»» lastFlushDelayMs|number|false|none|Last flush delay in ms| |»» meanFlushSize|number|false|none|Average flush size| |»» meanFlushDelayMs|number|false|none|Average flush delay in ms| |»» maxFlushSize|number|false|none|Max flush size| |»» maxFlushDelayMs|number|false|none|Max flush delay in ms| |»» queuedFlushMessages|integer|false|none|Queued flush messages| |» readIndex|object|false|none|none| |»» cachedRecord|number|false|none|Number of cached record reads| |»» notCachedRecord|number|false|none|Number of uncached record reads| |»» cachedStreamInfo|number|false|none|none| |»» notCachedStreamInfo|number|false|none|none| |»» cachedTransInfo|number|false|none|none| |»» notCachedTransInfo|number|false|none|none| |»» hashCollisions|number|false|none|none| --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/introduction.md' --- # Introduction ## Overview EventStoreDB provides a native interface of AtomPub over HTTP. AtomPub is a RESTful protocol that can reuse many existing components, for example reverse proxies and a client's native HTTP caching. Since events stored in EventStoreDB are immutable, cache expiration can be infinite. EventStoreDB leverages content type negotiation and you can access appropriately serialised events can as JSON or XML according to the request headers. ### Compatibility with AtomPub EventStoreDB v5 is fully compatible with the [1.0 version of the Atom Protocol](https://datatracker.ietf.org/doc/html/rfc4287). EventStoreDB adds extensions to the protocol, such as headers for control and custom `rel` links. ::: warning The latest versions of EventStoreDB (v20+) have the AtomPub protocol disabled by default. We do not advise creating new applications using AtomPub as we plan to deprecate it. Please explore our new gRPC protocol available in v20. It provides more reliable real-time event streaming with wide range of platforms and language supported. ::: #### Existing implementations Many environments have already implemented the AtomPub protocol, which simplifies the process. | Library | Description | |-----------|----------------------------------------------------| | NET (BCL) | `System.ServiceModel.SyndicationServices` | | JVM | | | PHP | | | Ruby | | | Clojure | | | Python | | | node.js | | ::: warning These are not officially supported by EventStoreDB. ::: #### Content types The preferred way of determining which content type responses EventStoreDB serves is to set the `Accept` header on the request. As some clients do not deal well with HTTP headers when caching, appending a format parameter to the URL is also supported, for example, `?format=xml`. The accepted content types for POST requests are: * `application/xml` * `application/vnd.eventstore.events+xml` * `application/json` * `application/vnd.eventstore.events+json` * `text/xml` The accepted content types for GET requests are: * `application/xml` * `application/atom+xml` * `application/json` * `application/vnd.eventstore.atom+json` * `text/xml` * `text/html` * `application/vnd.eventstore.streamdesc+json` ## Appending Events You append to a stream over HTTP using a `POST` request to the resource of the stream. If the stream does not exist then the stream is implicitly created. ### EventStoreDB media types EventStoreDB supports a custom media type for posting events, `application/vnd.eventstore.events+json` or `application/vnd.eventstore.events+xml`. This format allows for extra functionality that posting events as `application/json` or `application/xml` does not. For example it allows you to post multiple events in a single batch. The format represents data with the following jschema (`eventId` must be a UUID). ```json [ { "eventId" : "string", "eventType" : "string", "data" : "object", "metadata" : "object" } ] ``` ### Appending a single event to a new stream If you issue a `POST` request with data to a stream and the correct content type set it appends the event to the stream, and generates a `201` response from the server, giving you the location of the event. Using the following event, which [you can also download as a file](https://raw.githubusercontent.com/EventStore/EventStore/c948d32302414b456b42a73e0ce212f264ccb30a/samples/http-api/event.json): @[code](@samples/http-api/event.json) `POST` the following request to create a stream and add an event to it: ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-to-new-stream.sh) @tab Response @[code{response}](@samples/http-api/append-event-to-new-stream.sh) ::: Some clients may not be able to generate a unique identifier (or may not want to) for the event ID. You need this ID for idempotence purposes and EventStoreDB can generate it for you. If you leave off the `ES-EventId` header you see different behavior: ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-no-id.sh) @tab Response @[code{response}](@samples/http-api/append-event-no-id.sh) ::: In this case EventStoreDB has responded with a `307 Temporary Redirect`. The location points to another URI that you can post the event to. This new URI is idempotent for posting, even without an event ID. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-follow.sh) @tab Response @[code{response}](@samples/http-api/append-event-follow.sh) ::: It's generally recommended to include an event ID if possible as it results in fewer round trips between the client and the server. When posting to either the stream or to the returned redirect, clients must include the `EventType` header. If you forget to include the header you receive an error. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-no-type.sh) @tab Response @[code{response}](@samples/http-api/append-event-no-type.sh) ::: ### Batch append operation You can append more than one event in a single post by placing multiple events inside the array representing the events, including metadata. For example, the below has two events: @[code](@samples/http-api/multiple-events.json) When you append multiple events in a single post, EventStoreDB treats them as one transaction, it appends all events together or fails. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-multiple-events.sh) @tab Response @[code{response}](@samples/http-api/append-multiple-events.sh) ::: #### Appending events To append events, issue a `POST` request to the same resource with a new `eventId`: @[code](@samples/http-api/event-append.json) ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event.sh) @tab Response @[code{curl}](@samples/http-api/append-event.sh) ::: ### Data-only events Version 3.7.0 of EventStoreDB added support for the `application/octet-stream` content type to support data-only binary events. When creating these events, you need to provide the `ES-EventType` and `ES-EventId` headers and cannot have metadata associated with the event. In the example below `SGVsbG8gV29ybGQ=` is the data you `POST` to the stream: ::: tabs @tab Request @[code{curl}](@samples/http-api/append-data-event.sh) @tab Response @[code{response}](@samples/http-api/append-data-event.sh) ::: ### Expected version header The expected version header represents the version of the stream you expect. For example if you append to a stream at version 1, then you expect it to be at version 1 next time you append. This can allow for optimistic locking when multiple applications are reading/appending to streams. If your expected version is not the current version you receive an HTTP status code of 400. ::: warning See the idempotence section below, if you post the same event twice it is idempotent and won't return a version error. ::: First append an event to a stream, setting a version: @[code](@samples/http-api/event-version.json) ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-version.sh) @tab Response @[code{response}](@samples/http-api/append-event-version.sh) ::: If you now append to the stream with the incorrect version, you receive an HTTP status code 400 error. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-wrong-version.sh) @tab Response @[code{response}](@samples/http-api/append-event-wrong-version.sh) ::: There are special values you can use in the expected version header: * `-2` states that this append operation should never conflict and should **always** succeed. * `-1` states that the stream should not exist at the time of the appending (this append operation creates it). * `0` states that the stream should exist but should be empty. ### Idempotence Appends to streams are idempotent based upon the `EventId` assigned in your post. If you were to re-run the last command it returns the same value again. This is important behaviour as it's how you implement error handling. If you receive a timeout, broken connection, no answer, etc from your HTTP `POST` then it's your responsibility to retry the post. You must also keep the same UUID that you assigned to the event in the first `POST`. If you are using the expected version parameter with your post, then EventStoreDB is 100% idempotent. If you use `-2` as your expected version value, EventStoreDB does its best to keep events idempotent but cannot assure that everything is fully idempotent and you end up in 'at-least-once' messaging. [Read this guide](#idempotence) for more details on idempotence. ## Reading streams and events ### Reading a stream EventStoreDB exposes streams as a resource located at `http(s)://{yourdomain.com}:{port}/streams/{stream}`. If you issue a simple `GET` request to this resource, you receive a standard AtomFeed document as a response. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream.sh) @tab Response @[code{response}](@samples/http-api/read-stream.sh) ::: ### Reading an event from a stream The feed has one item in it, and if there are more than one, then items are sorted from newest to oldest. For each entry, there are a series of links to the actual events, [we cover embedding data into a stream later](#embedding-data-into-streams-in-json-format). To `GET` an event, follow the `alternate` link and set your `Accept` headers to the mime type you would like the event in. The accepted content types for `GET` requests are: * `application/xml` * `application/atom+xml` * `application/json` * `application/vnd.eventstore.atom+json` * `text/xml` * `text/html` The non-atom version of the event has fewer details about the event. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-event.sh) @tab Response @[code{response}](@samples/http-api/read-event.sh) ::: ### Feed paging The next step in understanding how to read a stream is the `first`/`last`/`previous`/`next` links within a stream. EventStoreDB supplies these links, so you can read through a stream, and they follow the pattern defined in [RFC 5005](https://datatracker.ietf.org/doc/html/rfc5005). In the example above the server returned the following `links` as part of its result: ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream.sh) @tab Response @[code{response}](@samples/http-api/read-stream.sh) ::: This shows that there is not a `next` URL as all the information is in this request and that the URL requested is the first link. When dealing with these URLs, there are two ways of reading the data in the stream. * You `GET` the `last` link and move backwards following `previous` links, or * You `GET` the `first` link and follow the `next` links, and the final item will not have a `next` link. If you want to follow a live stream, then you keep following the `previous` links. When you reach the end of a stream, you receive an empty document with no entries or `previous` link. You then continue polling this URI (in the future a document will appear). You can see this by trying the `previous` link from the above feed. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream-forwards.sh) @tab Response @[code{response}](@samples/http-api/read-stream-forwards.sh) ::: When parsing an atom subscription, the IDs of events always stay the same. This is important for figuring out if you are referring to the same event. ### Paging through events Let's now try an example with more than a single page. First create the multiple events: ::: tabs @tab Request @[code{curl}](@samples/http-api/append-paging-events.sh) @tab Response @[code{response}](@samples/http-api/append-paging-events.sh) ::: If you request the stream of events, you see a series of links above the events: ::: tabs @tab Request @[code{curl}](@samples/http-api/request-paging-events.sh) @tab Response @[code{response}](@samples/http-api/request-paging-events.sh) ::: Using the links in the stream of events, you can traverse through all the events in the stream by going to the `last` URL and following `previous` links, or by following `next` links from the `first` link. For example, if you request the `last` link from above: ::: tabs @tab Request @[code{curl}](@samples/http-api/request-last-link.sh) @tab Response @[code{response}](@samples/http-api/request-last-link.sh) ::: You then follow `previous` links until you are back to the head of the stream, where you can continue reading events in real time by polling the `previous` link. ::: tip All links except the head link are fully cacheable as you can see in the HTTP header `Cache-Control: max-age=31536000, public`. This is important when discussing intermediaries and performance as you commonly replay a stream from storage. You should **never** bookmark links aside from the head of the stream resource, and always follow links. We may in the future change how internal links work, and bookmarking links other than the head may break. ::: ### Reading all events `$all` is a special paged stream for all events. You can use the same paged form of reading described above to read all events for a node by pointing the stream at */streams/$all*. As it's a stream like any other, you can perform all operations, except posting to it. ::: tip To access the `$all` stream, you must use admin details. Find more information on the [security](security.md) page. ::: ::: tabs @tab Request @[code{curl}](@samples/http-api/read-all-events.sh) @tab Response @[code{response}](@samples/http-api/read-all-events.sh) ::: ### Conditional GETs The head link supports conditional `GET`s with the use of [ETAGS](http://en.wikipedia.org/wiki/HTTP_ETag), a well-known HTTP construct. You can include the ETAG of your last request and issue a conditional `GET` to the server. If nothing has changed, it won't return the full feed. For example the earlier response has an ETAG: @[code{responseHeader}](@samples/http-api/request-paging-events.sh) You can use this in your next request when polling the stream for changes by putting it in the `If-None-Match` header. This tells the server to check if the response is the one you already know and returning a '304 not modified' response. If the tags have changed, the server returns a '200 OK' response. You can use this method to optimise your application by not sending large streams if there are no changes. ::: tabs @tab Request @[code{curl}](@samples/http-api/request-etag.sh) @tab Response @[code{response}](@samples/http-api/request-etag.sh) ::: ::: tip You create Etags using the version of the stream and the media type of the stream you are reading. You can't read an Etag from a stream in one media type and use it with another media type. ::: ### Embedding data into streams in JSON format So far in this guide, the feeds returned have contained links that refer to the actual event data. This is normally a preferable mechanism for several reasons: * They can be in a different media type than the feed, and you can negotiate them separately from the feed itself (for example, the feed in JSON, the event in XML). You can cache the event data separately from the feed, and you can point it to different feeds. If you use a `linkTo()` in your [projection](projections.md) this is what happens in your atom feeds. * If you are using JSON, you can embed the events into the atom feed events. This can help cut down on the number of requests in some situations, but the messages are larger. There are ways of embedding events and further metadata into your stream by using the `embed` parameter. #### Rich embed mode The `rich` embed mode returns more properties about the event (`eventtype`, `streamid`, `position`, and so on) as you can see in the following request. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream-rich.sh) @tab Response @[code{response}](@samples/http-api/read-stream-rich.sh) ::: #### Body embed mode The `body` embed mode returns the JSON/XML body of the events into the feed as well, depending on the type of the feed. You can see this in the request below: ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream-body.sh) @tab Response @[code{response}](@samples/http-api/read-stream-body.sh) ::: ##### Variants of body embed mode Two other modes are variants of `body`: * `PrettyBody` tries to reformat the JSON to make it "pretty to read". * `TryHarder` works harder to try to parse and reformat the JSON from an event to return it in the feed. These do not include further information and are focused on how the feed looks. ## Deleting a stream ### Soft deleting To delete a stream over the Atom interface, issue a `DELETE` request to the resource. ::: tabs @tab Request @[code](@samples/http-api/delete-stream/delete-stream.sh) @tab Response @[code](@samples/http-api/delete-stream/delete-stream-response.http) ::: By default when you delete a stream, EventStoreDB soft deletes it. This means you can recreate it later by setting the `$tb` metadata section in the stream. If you try to `GET` a soft deleted stream you receive a 404 response: ::: tabs @tab Request @[code](@samples/http-api/delete-stream/get-deleted-stream.sh) @tab Response @[code](@samples/http-api/delete-stream/get-deleted-stream-response.http) ::: You can recreate the stream by appending new events to it (like creating a new stream): ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event.sh) @tab Response @[code](@samples/http-api/append-event.http) ::: The version numbers do not start at zero but at where you soft deleted the stream from ### Hard deleting You can hard delete a stream. To issue a permanent delete use the `ES-HardDelete` header. ::: warning A hard delete is permanent and the stream is not removed during a scavenge. If you hard delete a stream, you cannot recreate the stream. ::: Issue the `DELETE` as before but with the permanent delete header: ::: tabs @tab Request @[code](@samples/http-api/delete-stream/hard-delete-stream.sh) @tab Response @[code](@samples/http-api/delete-stream/hard-delete-stream.http) ::: The stream is now permanently deleted, and now the response is a `410`. ::: tabs @tab Request @[code{curl}](@samples/http-api/delete-stream/get-deleted-stream.sh) @tab Response @[code](@samples/http-api/delete-stream/get-deleted-stream-response.http) ::: If you try to recreate the stream as in the above example you also receive a `410` response. ::: tabs @tab Request @[code{curl}](@samples/http-api/delete-stream/append-event-deleted.sh) @tab Response @[code{response}](@samples/http-api/delete-stream/append-event-deleted.sh) ::: ## Description document With the addition of Competing Consumers, which is another way of reading streams, the need arose to expose these different methods to consumers. The introduction of the description document has some benefits: * Clients can rely on the keys (streams, streamSubscription) in the description document to remain unchanged across versions of EventStoreDB and you can rely on it as a lookup for the particular method of reading a stream. * Allows the restructuring of URIs underneath without breaking clients. e.g., `/streams/newstream` -> `/streams/newstream/atom`. ### Fetching the description document There are three ways in which EventStoreDB returns the description document. * Attempting to read a stream with an unsupported media type. * Attempting to read a stream with no accept header. * Requesting the description document explicitly. The client is able to request the description document by passing `application/vnd.eventstore.streamdesc+json` in the `accept` header, for example: ::: tabs @tab Request @[code{curl}](@samples/http-api/get-dd.sh) @tab Response @[code{response}](@samples/http-api/get-dd.sh) ::: In the example above, the client requested the description document for the stream called `newstream` which has a set of links describing the supported methods and content types. The document also includes additional methods available such as the `streamSubscription`. If there are no subscriptions to the `newstream`, the `streamSubscription` key is absent. ## Optimistic concurrency and idempotence ### Idempotence All operations on the HTTP interface are idempotent (unless the [expected version](#expected-version-header) is ignored). It is the responsibility of the client to retry operations under failure conditions, ensuring that the event IDs of the events posted are the same as the first attempt. Provided the client maintains this EventStoreDB will treat all operations as idempotent. For example: ::: tabs @tab Request ```bash curl -i -d @event.txt "http://127.0.0.1:2113/streams/newstream" ``` @tab Response ```http HTTP/1.1 201 Created Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, PUT, DELETE Location: http://127.0.0.1:2113/streams/newstream444/1 Content-Type: application/json Server: Mono-HTTPAPI/1.0 Date: Thu, 06 Sep 2012 19:49:37 GMT Content-Length: 107 Keep-Alive: timeout=15,max=100 ``` ::: Assuming you were posting to a new stream you would get the event appended once (and the stream created). The second event returns as the first but not write again. ::: tip This allows the client rule of “if you get an unknown condition, retry” to work. ::: For example: ::: tabs @tab Request ```bash curl -i "http://127.0.0.1:2113/streams/newstream444" ``` @tab Response ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, PUT, DELETE Content-Type: application/json Server: Mono-HTTPAPI/1.0 Date: Thu, 06 Sep 2012 19:50:30 GMT Content-Length: 2131 Keep-Alive: timeout=15,max=100 { "title": "Event stream 'newstream444'", "id": "http://127.0.0.1:2113/streams/newstream444", "updated": "2012-09-06T16:39:44.695643Z", "author": { "name": "EventStore" }, "links": [ { "uri": "http://127.0.0.1:2113/streams/newstream444", "relation": "self" }, { "uri": "http://127.0.0.1:2113/streams/newstream444", "relation": "first" } ], "entries": [ { "title": "newstream444 #1", "id": "http://127.0.0.1:2113/streams/newstream444/1", "updated": "2012-09-06T16:39:44.695643Z", "author": { "name": "EventStore" }, "summary": "Entry #1", "links": [ { "uri": "http://127.0.0.1:2113/streams/newstream444/1", "relation": "edit" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/1?format=text", "type": "text/plain" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/1?format=json", "relation": "alternate", "type": "application/json" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/1?format=xml", "relation": "alternate", "type": "text/xml" } ] }, { "title": "newstream444 #0", "id": "http://127.0.0.1:2113/streams/newstream444/0", "updated": "2012-09-06T16:39:44.695631Z", "author": { "name": "EventStore" }, "summary": "Entry #0", "links": [ { "uri": "http://127.0.0.1:2113/streams/newstream444/0", "relation": "edit" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/0?format=text", "type": "text/plain" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/0?format=json", "relation": "alternate", "type": "application/json" }, { "uri": "http://127.0.0.1:2113/streams/newstream444/event/0?format=xml", "relation": "alternate", "type": "text/xml" } ] } ] } ``` ::: ## Stream metadata Every stream in EventStoreDB has metadata stream associated with it, prefixed by `$$`, so the metadata stream from a stream called `foo` is `$$foo`. Internally, the metadata includes information such as the ACL of the stream, the maximum count and age for the events in the stream. Client code can also add information into stream metadata for use with projections or the client API. Stream metadata is stored internally as JSON, and you can access it over the HTTP API. ### Reading stream metadata To read the metadata, issue a `GET` request to the attached metadata resource, which is typically of the form: ```http http://{eventstore-ip-address}/streams/{stream-name}/metadata ``` You should not access metadata by constructing this URL yourself, as the right to change the resource address is reserved. Instead, you should follow the link from the stream itself, which enables your client to tolerate future changes to the addressing structure. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-metadata.sh) @tab Response @[code{response}](@samples/http-api/read-metadata.sh) ::: Once you have the URI of the metadata stream, issue a `GET` request to retrieve the metadata: ```bash curl -i -H "Accept:application/vnd.eventstore.atom+json" http://127.0.0.1:2113/streams/%24users/metadata --user admin:changeit ``` If you have security enabled, reading metadata may require that you pass credentials, as in the examples above. If credentials are required and you do not pass them, then you receive a `401 Unauthorized` response. ::: tabs @tab Request @[code{curl}](@samples/http-api/missing-credentials.sh) @tab Response @[code{response}](@samples/http-api/missing-credentials.sh) ::: ### Writing metadata To update the metadata for a stream, issue a `POST` request to the metadata resource. Inside a file named *metadata.json*: @[code](@samples/http-api/metadata.json) You can also add user-specified metadata here. Some examples user-specified metadata are: * Which adapter populates a stream. * Which projection created a stream. * A correlation ID to a business process. You then post this information is then posted to the stream: ::: tabs @tab Request @[code{curl}](@samples/http-api/update-metadata.sh) @tab Response @[code{response}](@samples/http-api/update-metadata.sh) ::: If the specified user does not have permissions to write to the stream metadata, you receive a '401 Unauthorized' response. --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/optional-http-headers.md' --- # Optional HTTP headers EventStoreDB supports custom HTTP headers for requests. The headers were previously in the form `X-ES-ExpectedVersion` but were changed to `ES-ExpectedVersion` in compliance with [RFC-6648](https://datatracker.ietf.org/doc/html/rfc6648). The headers supported are: | Header | Description | |-------------------------------------------------------|----------------------------------------------------------------------------------------------------| | [ES-ExpectedVersion](#expected-version) | The expected version of the stream (allows optimistic concurrency) | | [ES-ResolveLinkTo](#resolve-linkto) | Whether to resolve `linkTos` in stream | | [ES-RequiresMaster](#requires-master) | Whether this operation needs to run on the master node | | [ES-TrustedAuth](../security.md#trusted-intermediary) | Allows a trusted intermediary to handle authentication | | [ES-LongPoll](#longpoll) | Instructs the server to do a long poll operation on a stream read | | [ES-HardDelete](#harddelete) | Instructs the server to hard delete the stream when deleting as opposed to the default soft delete | | [ES-EventType](#eventtype) | Instructs the server the event type associated to a posted body | | [ES-EventId](#eventid) | Instructs the server the event id associated to a posted body | ## EventID When you append to a stream and don't use the `application/vnd.eventstore.events+json/+xml` media type, you need to specify an event ID with the event you post. This is not required with the custom media type as it is specified within the format (there is an `EventId` on each entry in the format). EventStoreDB uses `EventId` for impotency. You can include an event ID on an event by specifying this header. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-to-new-stream.sh) @tab Response @[code{response}](@samples/http-api/append-event-to-new-stream.sh) ::: If you don't add an `ES-EventId` header on an append where the body is considered the actual event (e.g., not using `application/vnd.eventstore.events+json/+xml`) EventStoreDB generates a unique identifier for you and redirects you to an idempotent URI where you can post your event. If you can create a UUID then you shouldn't use this feature, but it's useful when you cannot create a UUID. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-no-id.sh) @tab Response @[code{response}](@samples/http-api/append-event-no-id.sh) ::: EventStoreDB returned a `307 Temporary Redirect` with a location header that points to a generated URI that is idempotent for purposes of retrying the post. ## EventType When you append to a stream and don't the `application/vnd.eventstore.events+json/+xml` media type you must specify an event type with the event that you are posting. This isn't required with the custom media type as it's specified within the format itself. You use the `ES-EventType` header as follows. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-to-new-stream.sh) @tab Response @[code{response}](@samples/http-api/append-event-to-new-stream.sh) ::: If you view the event in the UI or with cURL it has the `EventType` of `SomeEvent`: ::: tabs @tab Request @[code{curl}](@samples/http-api/read-event.sh) @tab Response @[code{response}](@samples/http-api/read-event.sh) ::: ## Expected Version When you append to a stream you often want to use `Expected Version` to allow for optimistic concurrency with a stream. You commonly use this for a domain object projection. i.e., "A append operations can succeed if I have seen everyone else's append operations." You set `ExpectedVersion` with the syntax `ES-ExpectedVersion: #`, where `#` is an integer version number. There are other special values available: * `0`, the stream should exist but be empty when appending. * `-1`, the stream should not exist when appending. * `-2`, the write should not conflict with anything and should always succeed. * `-4`, the stream or a metadata stream should exist when appending. If the `ExpectedVersion` does not match the version of the stream, EventStoreDB returns an HTTP 400 `Wrong expected EventNumber` response. This response contains the current version of the stream in an `ES-CurrentVersion` header. In the following cURL command `ExpectedVersion` is not set, and it appends or create/append to the stream. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-to-new-stream.sh) @tab Response @[code{response}](@samples/http-api/append-event-to-new-stream.sh) ::: The stream `newstream` has one event. If you append with an expected version of `3`, you receive an error. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-wrong-version.sh) @tab Response @[code{response}](@samples/http-api/append-event-wrong-version.sh) ::: You can see from the `ES-CurrentVersion` header above that the stream is at version 0. Appending with an expected version of 0 works. The expected version is always the version of the last event known in the stream. ::: tabs @tab Request @[code{curl}](@samples/http-api/append-event-version.sh) @tab Response @[code{response}](@samples/http-api/append-event-version.sh) ::: ## HardDelete The `ES-HardDelete` header controls deleting a stream. By default EventStoreDB soft deletes a stream allowing you to later reuse that stream. If you set the `ES-HardDelete` header EventStoreDB permanently deletes the stream. ::: tabs @tab Request @[code{curl}](@samples/http-api/delete-stream/hard-delete-stream.sh) @tab Response @[code{response}](@samples/http-api/delete-stream/hard-delete-stream.sh) ::: This changes the general behaviour from returning a `404` and the stream to be recreated (soft-delete) to the stream now return a `410 Deleted` response. ## LongPoll You use the `ES-LongPoll` header to tell EventStoreDB that when on the head link of a stream and no data is available to wait a period of time to see if data becomes available. You can use this to give lower latency for Atom clients instead of client initiated polling. Instead of the client polling every 5 seconds to get data from the feed the client sends a request with `ES-LongPoll: 15`. This instructs EventStoreDB to wait for up to 15 seconds before returning with no result. The latency is therefore lowered from the poll interval to about 10ms from the time an event is appended until the time the HTTP connection is notified. You can see the use of the `ES-LongPoll` header in the following cURL command. First go to the head of the stream. ::: tabs @tab Request @[code{curl}](@samples/http-api/read-stream.sh) @tab Response @[code{response}](@samples/http-api/read-stream.sh) ::: Then fetch the previous `rel` link `http://127.0.0.1:2113/streams/newstream/2/forward/20` and try it. It returns an empty feed. ::: tabs @tab Request @[code{curl}](@samples/http-api/get-forward-link.sh) @tab Response @[code{response}](@samples/http-api/get-forward-link.sh) ::: The entries section is empty (there is no further data to provide). Now try the same URI with a long poll header. @[code](@samples/http-api/longpoll.sh) If you do not insert any events into the stream while this is running it takes 10 seconds for the HTTP request to finish. If you append an event to the stream while its running you see the result for that request when you append the event. ## Requires Master When running in a clustered environment there are times when you only want an operation to happen on the current leader node. A client can fetch information in an eventually consistent fashion by communicating with the servers. The TCP client included with the multi-node version does this. Over HTTP the `RequiresMaster` header tells the node that it is not allowed to serve a read or forward a write request. If the node is the leader everything works as normal, if it isn't it responds with a `307` temporary redirect to the leader. Run the below on the master: ::: tabs @tab Request ```bash curl -i "http://127.0.0.1:32004/streams/newstream" \ -H "ES-RequireMaster: True" ``` @tab Response ```json HTTP/1.1 200 OK Cache-Control: max-age=0, no-cache, must-revalidate Content-Length: 1296 Content-Type: application/vnd.eventstore.atom+json; charset: utf-8 ETag: "0;-2060438500" Vary: Accept Server: Microsoft-HTTPAPI/2.0 Access-Control-Allow-Methods: POST, DELETE, GET, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-PINGOTHER Access-Control-Allow-Origin: * Date: Thu, 27 Jun 2013 14:48:37 GMT { "title": "Event stream 'stream'", "id": "http://127.0.0.1:32004/streams/stream", "updated": "2013-06-27T14:48:15.2596358Z", "streamId": "stream", "author": { "name": "EventStore" }, "links": [ { "uri": "http://127.0.0.1:32004/streams/stream", "relation": "self" }, { "uri": "http://127.0.0.1:32004/streams/stream/head/backward/20", "relation": "first" }, { "uri": "http://127.0.0.1:32004/streams/stream/0/forward/20", "relation": "last" }, { "uri": "http://127.0.0.1:32004/streams/stream/1/forward/20", "relation": "previous" }, { "uri": "http://127.0.0.1:32004/streams/stream/metadata", "relation": "metadata" } ], "entries": [ { "title": "0@stream", "id": "http://127.0.0.1:32004/streams/stream/0", "updated": "2013-06-27T14:48:15.2596358Z", "author": { "name": "EventStore" }, "summary": "TakeSomeSpaceEvent", "links": [ { "uri": "http://127.0.0.1:32004/streams/stream/0", "relation": "edit" }, { "uri": "http://127.0.0.1:32004/streams/stream/0", "relation": "alternate" } ] } ] } ``` ::: Run the following on any other node: ::: tabs @tab Request ```bash curl -i "http://127.0.0.1:31004/streams/newstream" \ -H "ES-RequireMaster: True" ``` @tab Response ```http HTTP/1.1 307 Temporary Redirect Content-Length: 0 Content-Type: text/plain; charset: utf-8 Location: http://127.0.0.1:32004/streams/stream Server: Microsoft-HTTPAPI/2.0 Access-Control-Allow-Methods: POST, DELETE, GET, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-PINGOTHER Access-Control-Allow-Origin: * Date: Thu, 27 Jun 2013 14:48:28 GMT ``` ::: ## Resolve LinkTo When using projections you can have links placed into another stream. By default EventStoreDB always resolve `linkTo`s for you returning the event that points to the link. You can use the `ES-ResolveLinkTos: false` HTTP header to tell EventStoreDB to return you the actual link and to not resolve it. You can see the differences in behaviour in the following cURL commands. ::: tabs @tab Request @[code{curl}](@samples/http-api/resolve-links.sh) @tab Response @[code{response}](@samples/http-api/resolve-links.sh) ::: ::: tip The content links are pointing to the original projection stream. The linked events are resolved back to where they point. With the header set the links (or embedded content) instead point back to the actual `linkTo` events. ::: ::: tabs @tab Request @[code{curl}](@samples/http-api/resolve-links-false.sh) @tab Response @[code{response}](@samples/http-api/resolve-links-false.sh) ::: --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/persistent.md' --- # Persistent subscriptions This document explains how to use HTTP API for setting up and consuming persistent subscriptions and competing consumer subscription groups. For an overview on competing consumers and how they relate to other subscription types please see our [getting started guide](../persistent-subscriptions.md). ::: tip The Administration UI includes a *Competing Consumers* section where you are able to create, update, delete and view subscriptions and their statuses. ::: ## Creating a persistent subscription Before interacting with a subscription group, you need to create one. You receive an error if you try to create a subscription group more than once. This requires [admin permissions](security.md). ::: warning\ Persistent subscriptions to `$all` are not supported over the HTTP API. If you want to create persistent subscriptions to `$all`, use the [appropriate client method](@clients/grpc/persistent-subscriptions.md#subscribing-to-all).\ ::: | URI | Supported Content Types | Method | | --------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}` | `application/json` | PUT | ### Query parameters | Parameter | Description | | ------------------- | --------------------------------------------- | | `stream` | The stream the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | ### Body | Parameter | Description | | ----------------------------- | -------------------------------------------------------------------------------------------------- | | `resolveLinktos` | Tells the subscription to resolve link events. | | `startFrom` | Start the subscription from the position-of the event in the stream. | | `extraStatistics` | Tells the backend to measure timings on the clients so statistics will contain histograms of them. | | `checkPointAfterMilliseconds` | The amount of time the system should try to checkpoint after. | | `liveBufferSize` | The size of the live buffer (in memory) before resorting to paging. | | `readBatchSize` | The size of the read batch when in paging mode. | | `bufferSize` | The number of messages that should be buffered when in paging mode. | | `maxCheckPointCount` | The maximum number of messages not checkpointed before forcing a checkpoint. | | `maxRetryCount` | Sets the number of times a message should be retried before considered a bad message. | | `maxSubscriberCount` | Sets the maximum number of allowed TCP subscribers. | | `messageTimeoutMilliseconds` | Sets the timeout for a client before the message will be retried. | | `minCheckPointCount` | The minimum number of messages to write a checkpoint for. | | `namedConsumerStrategy` | RoundRobin/DispatchToSingle/Pinned | ## Updating a persistent subscription You can edit the settings of an existing subscription while it is running. This drops the current subscribers and resets the subscription internally. This requires admin permissions. | URI | Supported Content Types | Method | | --------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}` | `application/json` | POST | ::: warning\ Persistent subscriptions to `$all` are not supported over the HTTP API. To update persistent subscriptions to `$all`, use the [appropriate client method](@clients/grpc/persistent-subscriptions.md#updating-a-subscription-group).\ ::: ### Query parameters | Parameter | Description | | ------------------- | ------------------------------------------------ | | `stream` | The stream to the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | ### Body *Same parameters as "Creating a Persistent Subscription"* ## Deleting a persistent subscription | URI | Supported Content Types | Method | | --------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}` | `application/json` | DELETE | ::: warning Deleting persistent subscriptions to `$all` is not supported over the HTTP API. If you want to delete persistent subscriptions to `$all`, the gRPC client should be used instead. ::: ### Query parameters | Parameter | Description | | ------------------- | ------------------------------------------------ | | `stream` | The stream to the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | ## Reading a stream via a persistent subscription By default, reading a stream via a persistent subscription returns a single event per request and does not embed the event properties as part of the response. | URI | Supported Content Types | Method | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------ | | `/subscriptions/{stream}/{subscription_name} /subscriptions/{stream}/{subscription_name}?embed={embed} /subscriptions/{stream}/{subscription}/{count}?embed={embed}` | `application/vnd.eventstore.competingatom+xml application/vnd.eventstore.competingatom+json` | GET | ### Query parameters | Parameter | Description | | ------------------- | ------------------------------------------------------------ | | `stream` | The stream the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | | `count` | How many events to return for the request. | | `embed` | `None`, `Content`, `Rich`, `Body`, `PrettyBody`, `TryHarder` | Read [Reading Streams](introduction.md#reading-streams-and-events) for information on the different embed levels. ### Response @[code](@samples/http-api/persistent-subscriptions/read-stream-response.json) ## Acknowledgements Clients must acknowledge (or not acknowledge) messages in the competing consumer model. If the client fails to respond in the given timeout period, the message is retried. You should use the `rel` links in the feed for acknowledgements not bookmark URIs as they are subject to change in future versions. For example: ```json { "uri": "http://localhost:2113/subscriptions/newstream/competing_consumers_group1/ack/c322e299-cb73-4b47-97c5-5054f920746f", "relation": "ack" } ``` ### Ack multiple messages | URI | Supported Content Types | Method | | ------------------------------------------------------------------ | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/ack?ids={messageids}` | `application/json` | POST | #### Query parameters | Parameter | Description | | :------------------ | :--------------------------------------------- | | `stream` | The stream the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | | `messageids` | The ids of the messages that needs to be ACKed | ### Ack a single message | URI | Supported Content Types | Method | | ------------------------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/ack/{messageid}` | `application/json` | POST | #### Query parameters | Parameter | Description | | ------------------- | ------------------------------------------------ | | `stream` | The stream to the persistent subscription is on. | | `subscription_name` | The name of the subscription group. | | `messageid` | The id of the message that needs to be acked | ### Nack multiple messages | URI | Supported Content Types | Method | | ----------------------------------------------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/nack?ids={messageids}?action={action}` | `application/json` | POST | #### Query parameters | Parameter | Description | | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --- | | `stream` | The stream to the persistent subscription is on. | | | `subscription_name` | The name of the subscription group. | | | `action` | **Park**: Don't retry the message, park it until a request is sent to reply the parked messages**Retry**: Retry the message**Skip**: Discard the message**Stop**: Stop the subscription | | | `messageid` | The id of the message that needs to be acked | | ### Nack a single message | URI | Supported Content Types | Method | | ------------------------------------------------------------------------------ | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/nack/{messageid}?action={action}` | `application/json` | POST | ## Replaying parked messages | URI | Supported Content Types | Method | | ---------------------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/replayParked` | `application/json` | POST | ## Getting information for all subscriptions | URI | Method | | ---------------- | ------ | | `/subscriptions` | GET | ### Response @[code](@samples/http-api/persistent-subscriptions/get-all-subscriptions-response.json) ## Get subscriptions for a stream | URI | Supported Content Types | Method | | ------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}` | `application/json` | GET | ### Response @[code](@samples/http-api/persistent-subscriptions/get-subscriptions-for-stream-response.json) ## Getting a specific subscription | URI | Supported Content Types | Method | | -------------------------------------------------- | ----------------------- | ------ | | `/subscriptions/{stream}/{subscription_name}/info` | `application/json` | GET | ### Response @[code](@samples/http-api/persistent-subscriptions/get-subscription-response.json) --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/projections.md' --- # Projections You can also query the state of all projections using the HTTP API. @[code](@samples/http-api/projections/list-all-projections.sh) The response is a list of all known projections and useful information about them. @[code](@samples/http-api/projections/list-all-projections.json) ## Add sample data Download the following files that contain sample data used throughout this step of the getting started guide. * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1164.json](@httpapi/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1164.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1165.json](@httpapi/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1165.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1166.json](@httpapi/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1166.json) * [shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1167.json](@httpapi/data/shoppingCart-b989fe21-9469-4017-8d71-9820b8dd1167.json) Add the sample data to four different streams: @[code](@samples/http-api/add-sample-data.sh) ## Creating your first projection ::: tip Next steps Read [this guide](#projections-api) to find out more about the user defined projection's API. ::: The projection counts the number of 'XBox One S's that customers added to their shopping carts. A projection starts with a selector, in this case `fromAll()`. Another possibility is `fromCategory({category})` which this step discusses later, but for now, `fromAll` should do. The second part of a projection is a set of filters. There is a special filter called `$init` that sets up an initial state. You want to start a counter from 0 and each time EventStoreDB observes an `ItemAdded` event for an 'Xbox One S,' increment the counter. Here is the projection code: @[code](@samples/http-api/xbox-one-s-counter.js) You create a projection by calling the projection API and providing it with the definition of the projection. Here you decide how to run the projection, declaring that you want the projection to start from the beginning and keep running. You can create a projection using the Admin UI by opening the *Projections* tab, clicking the *New Projection* button and filling in the details of your projection. ![Creating a projection with the EventStoreDB Admin UI](./images/getting-started-create-projection.png) You can also create projections programmatically. Pass the projection JSON file as a parameter of your request, along with any other settings: @[code](@samples/http-api/projections/create-projection.sh) ::: tip Next steps [Read here](api.md) for more information on creating projections with the HTTP API and the parameters available, or [our projections section](projections.md) for details on projection syntax. ::: ## Querying for the state of the projection Now the projection is running, you can query the state of the projection. As this projection has a single state, query it with the following request: @[code](@samples/http-api/projections/query-state.sh) The server will send a response similar to this: @[code](@samples/http-api/projections/query-state.json) ## Appending to streams from projections The above gives you the correct result but requires you to poll for the state of a projection. What if you wanted EventStoreDB to notify you about state updates via subscriptions? ### Output state Update the projection to output the state to a stream by calling the `outputState()` method on the projection which by default produces a `$projections-{projection-name}-result` stream. Below is the updated projection: @[code](@samples/http-api/xbox-one-s-counter-outputState.js) To update the projection, edit the projection definition in the Admin UI, or issue the following request: @[code](@samples/http-api/xbox-one-s-counter-outputState.sh) Then reset the projection you created above: @[code](@samples/http-api/projections/reset-projection.sh) You should get a response similar to the one below: @[code](@samples/http-api/projections/reset-projection.json) You can now read the events in the result stream by issuing a read request. @[code](@samples/http-api/projections/read-projection-events.sh) And you'll get a response like this: @[code](@samples/http-api/projections/reset-projection.json) ## Configure projection properties You can configure properties of the projection by updating values of the `options` object. For example, the following projection changes the name of the results stream: @[code{2}](@samples/http-api/projections/update-projection-options.js) Then send the update to the projection: @[code](@samples/http-api/projections/update-projection-options.sh) ::: tip You can find all the options available in the [user-defined projections guide](../projections.md#user-defined-projections). ::: Now you can read the result as above, but use the new stream name: @[code](@samples/http-api/projections/read-projection-events-renamed.sh) ## The number of items per shopping cart The example in this step so far relied on a global state for the projection, but what if you wanted a count of the number of items in the shopping cart per shopping cart. EventStoreDB has a built-in `$by_category` projection that lets you select events from a particular list of streams. Enable this projection with the following command. @[code](@samples/http-api/projections/enable-by-category.sh) The projection links events from existing streams to new streams by splitting the stream name by a separator. You can configure the projection to specify the position of where to split the stream `id` and provide a separator. By default, the category splits the stream `id` by a dash. The category is the first string. | Stream Name | Category | |--------------------|----------------------------------------| | shoppingCart-54 | shoppingCart | | shoppingCart-v1-54 | shoppingCart | | shoppingCart | *No category as there is no separator* | You want to define a projection that produces a count per stream for a category, but the state needs to be per stream. To do so, use `$by_category` and its `fromCategory` API method. Below is the projection: @[code](@samples/http-api/projections/shopping-cart-counter.js) Create the projection with the following request: @[code](@samples/http-api/projections/shopping-cart-counter.sh) #### Querying for the state of the projection by partition Querying for the state of the projection is different due to the partitioning of the projection. You have to specify the partition and the name of the stream. @[code](@samples/http-api/projections/read-state-partition.sh) The server then returns the state for the partition: @[code](@samples/http-api/projections/read-state-partition.json) ## Projections API ### List Projections | URI | Description | HTTP verb | |:---------------------------------------------------|:-----------------------------|:----------| | `/projections/any` | Returns all known projections. | GET | | `/projections/all-non-transient` | Returns all known non ad-hoc projections. | GET | ### Manage Continuous Projections Continuous projections will continue to run until they are disabled or until they encounter an unrecoverable error. These are the most typical projections. | URI | Description | HTTP verb | |:---------------------------------------------------|:-----------------------------|:----------| | `/projections/continuous` | Returns all known continuous projections. | GET | | `/projections/continuous?name={name}&type={type}&enabled={enabled}&emit={emit}&trackemittedstreams={trackemittedstreams}` | Create a continuous projection. | POST | #### Parameters * `name`: Name of the projection * `type`: JS or Native. (JavaScript or native. At this time, EventStoreDB only supports JavaScript) * `enabled`: Enable the projection (true/false) * `emit`: Allow the projection to append to streams (true/false) * `trackemittedstreams`: Write the name of the streams the projection is managing to a separate stream. $projections-{projection-name}-emittedstreams (true/false) ### Manage Transient Projections Transient projections are sometimes called ad-hoc projections or queries. This type of projection will run until completion and automatically delete itself afterwards. | URI | Description | HTTP verb | |:---------------------------------------------------|:-----------------------------|:----------| | `/projections/transient` | Returns all known ad-hoc projections. | GET | | `/projections/transient?name={name}&type={type}&enabled={enabled}` | Create an ad-hoc projection (or query). | POST | #### Parameters * `name`: Name of the projection * `type`: JS or Native. (JavaScript or native. At this time, EventStoreDB only supports JavaScript) * `enabled`: Enable the projection (true/false) ### Manage OneTime Projections OneTime projections run until completion and then stop. These are useful for periodically generating state or reports when you don't need the projection to be always running. | URI | Description | HTTP verb | |:---------------------------------------------------|:-----------------------------|:----------| | `/projections/onetime` | Returns all known one-time projections. | GET | | `/projections/onetime?name={name}&type={type}&enabled={enabled}&checkpoints={checkpoints}&emit={emit}&trackemittedstreams={trackemittedstreams}` | Create a one-time projection. | POST | #### Parameters * `name`: Name of the projection * `type`: JS or Native. (JavaScript or native. At this time, EventStoreDB only supports JavaScript) * `enabled`: Enable the projection (true/false) * `checkpoints`: Enable checkpoints (true/false) * `emit`: Enable the ability for the projection to append to streams (true/false) * `trackemittedstreams`: Write the name of the streams the projection is managing to a separate stream. $projections-{projection-name}-emittedstreams (true/false) ### Manage a Projection | URI | Description | HTTP verb | |:---------------------------------------------------|:-----------------------------|:----------| | `/projection/{name}` | Returns information for a projection. | GET | | `/projection/{name}/query?config={config}` | Returns the definition query and if config is set to true, will return the configuration. | GET | `/projection/{name}/query?type={type}&emit={emit}` | Update a projection's query. | PUT | | `/projection/{name}?deleteStateStream={deleteStateStream}&deleteCheckpointStream={deleteCheckpointStream}&deleteEmittedStreams={deleteEmittedStreams}` | Delete a projection, optionally delete the streams that were created as part of the projection. | DELETE | | `/projection/{name}/statistics` | Returns detailed information for a projection. | GET | | `/projection/{name}/state?partition={partition}` | Query for the state of a projection. | GET | | `/projection/{name}/result?partition={partition}` | Query for the result of a projection. | GET | | `/projection/{name}/command/enable?enableRunAs={enableRunAs}` | Enable a projection. | POST | | `/projection/{name}/command/disable?enableRunAs={enableRunAs}` | Disable a projection. | POST | | `/projection/{name}/command/reset?enableRunAs={enableRunAs}` | Reset a projection. (This will re-emit events, streams that are written to from the projection will also be soft deleted). | POST | | `/projection/{name}/command/abort?enableRunAs={enableRunAs}` | Abort a projection. | POST | #### Parameters * `name`: Name of the projection * `config`: Return the definition of the projection (true/false) * `type`: JS or Native. (JavaScript or native. At this time, EventStoreDB only supports JavaScript) * `emit`: Allow the projection to write to streams (true/false) * `trackemittedstreams`: Write the name of the streams the projection is managing to a separate stream. $projections-{projection-name}-emittedstreams (true/false) * `name`: Name of the projection * `deleteStateStream`: Delete the state stream (true/false) * `deleteCheckpointStream`: Delete the checkpoint stream (true/false) * `deleteEmittedStreams`: Delete the emitted streams stream (true/false) * `partition`: The name of the partition * `enableRunAs`: Enables the projection to run as the user who issued the request. --- --- url: 'https://docs.kurrent.io/server/v22.10/http-api/security.md' --- # Security EventStoreDB supports basic authentication for HTTP API calls, and access control lists (ACL). ## Authentication ### Creating users EventStoreDB supports basic HTTP authentication to internal users. You create these users with the HTTP API or the admin console. You need to use the credentials of the default user in the request, which has the username of `admin`, and the password of `changeit`. When using the HTTP API, you can send the following JSON payload to the server: @[code](@samples/http-api/new-user.json) ::: tabs @tab Request @[code](@samples/http-api/new-user.sh) @tab Response @[code](@samples/http-api/new-user.http) ::: Once you have added users, you can use their details with requests. If you were to use the wrong user or no user when a request requires one, you receive a `401 Unauthorized` response. ::: tabs @tab Request @[code](@samples/http-api/incorrect-user.sh) @tab Response @[code](@samples/http-api/incorrect-user.http) ::: As you pass the username and password in the request we recommend you to enable SSL to encrypt the user information. [Read this guide for instructions](../security.md). ## Access control lists Alongside authentication, EventStoreDB supports per stream configuration of Access Control Lists (ACL). To configure the ACL of a stream go to its head and look for the `metadata` relationship link to fetch the metadata for the stream. To set access control lists over HTTP you can post to the metadata stream as [with setting any other metadata](introduction.md#stream-metadata). You can also set Access Control Lists for a stream in the admin UI. ### ACL example The ACL below gives `writer` read and write permission on the stream, while `reader` has read permission on the stream. Only users in the `$admins` group can delete the stream or read and write the metadata. The request body placed in the file named *metadata.json*: @[code](@samples/http-api/metadata.json) Then, when you execute HTTP request as follows: @[code{curl}](@samples/http-api/update-acl.sh) You get a confirmation from the server: @[code{response}](@samples/http-api/update-acl.sh) ## Default ACL ::: tip All these examples assume you have created a user named `ouro` with password `ouroboros`. ::: If you try to access the `$settings` stream as an unauthorized user, the server returns a 401 response. ::: tabs @tab Request ```bash curl -i http://127.0.0.1:2113/streams/%24settings \ -u ouro:ouroboros ``` @tab Response ```http HTTP/1.1 401 Unauthorized Access-Control-Allow-Methods: POST, DELETE, GET, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-PINGOTHER, Authorization, ES-LongPoll, ES-ExpectedVersion, ES-EventId, ES-EventType, ES-RequiresMaster, ES-HardDelete, ES-ResolveLinkTo, ES-ExpectedVersion Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Location, ES-Position WWW-Authenticate: Basic realm="ES" Content-Type: text/plain; charset=utf-8 Server: Mono-HTTPAPI/1.0 Date: Mon, 02 Mar 2015 15:21:27 GMT Content-Length: 0 Keep-Alive: timeout=15,max=100 ``` ::: If you wanted to give `ouro` access by default to system streams, POST the following JSON: ```json { "$userStreamAcl": { "$r": "$all", "$w": "ouro", "$d": "ouro", "$mr": "ouro", "$mw": "ouro" }, "$systemStreamAcl": { "$r": ["$admins", "ouro"], "$w": "$admins", "$d": "$admins", "$mr": "$admins", "$mw": "$admins" } } ``` At which point `ouro` can read system streams by default: ::: tabs @tab Request ```bash curl -i http://127.0.0.1:2113/streams/%24settings \ -u ouro:ouroboros ``` @tab Response ```http HTTP/1.1 200 OK Access-Control-Allow-Methods: POST, DELETE, GET, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Requested-With, X-PINGOTHER, Authorization, ES-LongPoll, ES-ExpectedVersion, ES-EventId, ES-EventType, ES-RequiresMaster, ES-HardDelete, ES-ResolveLinkTo, ES-ExpectedVersion Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Location, ES-Position Cache-Control: max-age=0, no-cache, must-revalidate Vary: Accept ETag: "1;-1296467268" Content-Type: application/atom+xml; charset=utf-8 Server: Mono-HTTPAPI/1.0 Date: Mon, 02 Mar 2015 15:25:17 GMT Content-Length: 1286 Keep-Alive: timeout=15,max=100 ``` ::: You can also limit ACLs on particular streams which are then merged with the default ACLs. ```json { "$acl": { "$r": ["reader", "also-reader"] } } ``` If you add the above to a stream's ACL, then it overrides the read permission on that stream to allow `reader` and `also-reader` to read streams, but not `ouro`, resulting in the effective ACL below. ```json { "$acl": { "$r": ["reader", "also-reader"], "$w": "ouro", "$d": "ouro", "$mr": "ouro", "$mw": "ouro" } } ``` ::: warning Caching is allowed on a stream if you have enabled it to be visible to `$all`. This is as a performance optimization to avoid having to set `cache=private` on all data. If people are bookmarking your URIs and they were cached by an intermediary then they may still be accessible after you change the permissions from `$all`. While clients should not be bookmarking URIs in this way, it's an important consideration. ::: --- --- url: 'https://docs.kurrent.io/server/v22.10/indexes.md' --- # Indexes ## Indexing EventStoreDB stores indexes separately from the main data files, accessing records by stream name. ### Overview EventStoreDB creates index entries as it processes commit events. It holds these in memory (called *memtables*) until it reaches the `MaxMemTableSize` and then persisted on disk in the *index* folder along with `.bloomfilter` files and an index map file. The index files are uniquely named, and the index map file called *indexmap*. The index map describes the order and the level of the index file as well as containing the data checkpoint for the last written file, the version of the index map file and a checksum for the index map file. The logs refer to the index files as a *PTable*. Indexes are sorted lists based on the hashes of stream names. To speed up seeking the correct location in the file of an entry for a stream, EventStoreDB keeps midpoints to relate the stream hash to the physical offset in the file. As EventStoreDB saves more files, they are automatically merged together whenever there are more than 2 files at the same level into a single file at the next level. Each index entry is 24 bytes and the index file size is approximately 24Mb per 1M events. The Bloom filter files are approximately 1% of the size of the rest of the index. Level 0 is the level of the *memtable* that is kept in memory. Generally there is only 1 level 0 table unless an ongoing merge operation produces multiple level 0 tables. Assuming the default `MaxMemTableSize` of 1M, the index files by level are: | Level | Number of entries | Size | |-------|-------------------|----------------| | 1 | 1M | 24MB | | 2 | 2M | 48MB | | 3 | 4M | 96MB | | 4 | 8M | 192MB | | 5 | 16M | 384MB | | 6 | 32M | 768MB | | 7 | 64M | 1536MB | | 8 | 128M | 3072MB | | n | 2^(n-1) \* 1M | 2^(n-1) \* 24Mb | Each index entry is 24 bytes and the index file size is approximately 24Mb per M events. ## Indexing in depth For general operation of EventStoreDB the following information is not critical but useful for developers wanting to make changes in the indexing subsystem and for understanding crash recovery and tuning scenarios. ### Index map files *Indexmap* files are text files made up of line delimited values. The line delimiter varies based on operating system, so while you can consider *indexmap* files valid when transferred between operating systems, if you make changes to fix an issue (for example, disk corruption) it is best to make them on the same operating system as the cluster. The *indexmap* structure is as follows: * `hash` - an md5 hash of the rest of the file * `version` - the version of the *indexmap* file * `checkpoint` - the maximum prepare/commit position of the persisted *ptables* * `maxAutoMergeLevel` - either the value of `MaxAutoMergeLevel` or `int32.MaxValue` if it was not set. This is primarily used to detect increases in `MaxAutoMergeLevel`, which is not supported. * `ptable`,`level`,`index`- List of all the *ptables* used by this index map with the level of the *ptable* and it's order. EventStoreDB writes *indexmap* files to a temporary file and then deletes the original and renames the temporary file. EventStoreDB attempts this 5 times before failing. Because of the order, this operation can only fail if there is an issue with the underlying file system or the disk is full. This is a 2 phase process, and in the unlikely event of a crash during this process, EventStoreDB recovers by rebuilding the indexes using the same process used if it detects corrupted files during startup. ### Writing and merging of index files Merging *ptables*, updating the *indexmap* and persisting *memtable* operations happen on a background thread. These operations are performed on a single thread with any additional operations queued and completed later. EventStoreDB runs these operations on a thread pool thread rather than a dedicated thread. Generally there is only one operation queued at a time, but if merging to *ptables* at one level causes 2 tables to be available at the next level, then the next merge operation is immediately queued. While merge operations are in progress, if EventStoreDB is appending large numbers of events, it may queue 1 or more *memtables* for persistence. The number of pending operations is logged. For safety *ptables* EventStoreDB is currently merging are only deleted after the new *ptable* has persisted and the *indexmap* updated. In the event of a crash, EventStoreDB recovers by deleting any files not in the *indexmap* and reindexing from the prepare/commit position stored in the *indexmap* file. ### Manual merging If you have set the maximum level ([`MaxAutoMergeIndexLevel`](#auto-merge-index-level)) for automatically merging indexes, then you need to trigger merging indexes above this level manually by using the `/admin/mergeindexes` endpoint, or the es-cli tool that is available with commercial support. Triggering a manual merge causes EventStoreDB to merge all tables that have a level equal to the maximum merge level or above into a single table. If there is only 1 table at the maximum level or above, no merge is performed. ### Stream existence filter The *Stream Existence Filter* is a pair of files in the index directory. * `/index/stream-existence/streamExistenceFilter.chk` * `/index/stream-existence/streamExistenceFilter.dat` It is made up of a persisted Bloom filter (the `.dat` file), and a checkpoint (the `.chk` file) that records where in the log the filter has processed up to. As per the documented backup procedure, the checkpoint needs to be backed up before the dat file. The Stream Existence Filter is used when reading and writing to quickly determine whether a stream *might exist* or *definitely does not exist*. If a stream definitely does not exist then EventStoreDB can skip looking through the index to find information about it. This is particularly useful when creating new streams (i.e. appending to stream that did not previously exist). Additional information can be found in this [`blog post`](https://www.eventstore.com/blog/bloom-filters). ## Configuration options The configuration options that affect indexing are: | Option | What's it for | |:-------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| | [`Index`](#index-location) | Where the indexes are stored | | [`MaxMemTableSize`](#memtable-size) | How many entries to have in memory before writing out to disk | | [`IndexCacheDepth`](#index-cache-depth) | Sets the minimum number of midpoints to calculate for an index file | | [`SkipIndexVerify`](#skip-index-verification) | Tells the server to not verify indexes on startup | | [`MaxAutoMergeIndexLevel`](#auto-merge-index-level) | The maximum level of index file to merge automatically before manual merge | | [`StreamExistenceFilterSize`](#stream-existence-filter-size) | Size in bytes of the stream existence filter | | [`IndexCacheSize`](#index-cache-size) | Maximum number of entries in each index LRU cache | | [`UseIndexBloomFilters`](#use-index-bloom-filters) | Feature flag which can be used to disable the index Bloom filters | Read more below to understand these options better. ### Index location | Format | Syntax | |:---------------------|:-------------------| | Command line | `--index` | | YAML | `Index` | | Environment variable | `EVENTSTORE_INDEX` | **Default**: data files location `Index` affects the location of the index files. We recommend you place index files on a separate drive to avoid competition for IO between the data, index and log files. ### Memtable size | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--max-mem-table-size` | | YAML | `MaxMemTableSize` | | Environment variable | `EVENTSTORE_MAX_MEM_TABLE_SIZE` | **Default**: `1000000` `MaxMemTableSize` affects disk IO when EventStoreDB writes files to disk, index seek time and database startup time. The default size is a good tradeoff between low disk IO and startup time. Increasing the `MaxMemTableSize` results in longer database startup time because a node has to read through the data files from the last position in the `indexmap` file and rebuild the in memory index table before it starts. Increasing `MaxMemTableSize` also decreases the number of times EventStoreDB writes index files to disk and how often it merges them together, which increases IO operations. It also reduces the number of seek operations when stream entries span multiple files as EventStoreDB needs to search each file for the stream entries. This affects streams written to over longer periods of time more than streams written to over a shorter time, where time is measured by the number of events created, not time passed. This is because streams written to over longer time periods are more likely to have entries in multiple index files. ### Index cache depth | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--index-cache-depth` | | YAML | `IndexCacheDepth` | | Environment variable | `EVENTSTORE_INDEX_CACHE_DEPTH` | **Default**: `16` `IndexCacheDepth` affects how many midpoints EventStoreDB calculates for an index file which affects file size slightly, but can affect lookup times significantly. Looking up a stream entry in a file requires a binary search on the midpoints to find the nearest midpoint, and then a seek through the entries to find the entry or entries that match. Increasing this value decreases the second part of the operation and increase the first for extremely large indexes. **The default value of 16** results in files up to about 1.5 GB in size being fully searchable through midpoints. After that a maximum distance between midpoints of 4096 bytes for the seek, which is buffered from disk, up to a maximum level of 2 TB where the seek distance starts to grow. Reducing this value can relieve a small amount of memory pressure in highly constrained environments. Increasing it causes index files larger than 1.5 GB, and less than 2 TB to have more dense midpoint populations which means the binary search is not used for long before switching back to scanning the entries between. The maximum number of entries scanned in this way is `distance/24b`, so with the default setting and a 2TB index file this is approximately 170 entries. Most clusters should not need to change this setting. ### Skip index verification | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--skip-index-verify` | | YAML | `SkipIndexVerify` | | Environment variable | `EVENTSTORE_SKIP_INDEX_VERIFY` | **Default**: `false` `SkipIndexVerify` skips reading and verification of index file hashes during startup. Instead of recalculating midpoints when EventStoreDB reads the file, it reads the midpoints directly from the footer of the index file. You can set `SkipIndexVerify` to `true` to reduce startup time in exchange for the acceptance of a small risk that the index file becomes corrupted. This corruption could lead to a failure if you read the corrupted entries, and a message saying the index needs to be rebuilt. You can safely disable this setting for ZFS on Linux as the filesystem takes care of file checksums. In the event of corruption indexes will be rebuilt by reading through all the chunk files and recreating the indexes from scratch. ### Auto-merge index level | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--max-auto-merge-index-level` | | YAML | `MaxAutoMergeIndexLevel` | | Environment variable | `EVENTSTORE_MAX_AUTO_MERGE_INDEX_LEVEL` | **Default**: `2147483647` `MaxAutoMergeIndexLevel` allows you to specify the maximum index file level to automatically merge. By default EventStoreDB merges all levels. Depending on the specification of the host running EventStoreDB, at some point index merges will use a large amount of disk IO. For example: > Merging 2 level 7 files results in at least 3072 MB reads (2 \* 1536 MB), and 3072 MB writes while merging 2 level 8 files together results in at least 6144 MB reads (2 \* 3072 MB) and 6144 MB writes. Setting `MaxAutoMergeLevel` to 7 allows all levels up to and including level 7 to be automatically merged, but to merge the level 8 files together, you need to trigger a manual merge. This manual merge allows better control over when these larger merges happen and which nodes they happen on. Due to the replication process, all nodes tend to merge at about the same time. ### Stream existence filter size | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--stream-existence-filter-size` | | YAML | `StreamExistenceFilterSize` | | Environment variable | `EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE` | **Default**: `256000000` `StreamExistenceFilterSize` is the amount of memory & disk space, in bytes, to use for the stream existence filter. This should be set to roughly the maximum number of streams you expect to have in your database, i.e if you expect to have a max of 500 million streams, use a value of 500000000. The value you select should also fit entirely in memory to avoid any performance degradation. Use 0 to disable the filter. Upgrading to a version of EventStoreDB that supports the Stream Existence Filter requires the filter to be built - unless it is disabled. This will take approximately as long as it takes to read through the whole index. Resizing the filter will also cause a full rebuild of the filter. ### Index cache size | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--index-cache-size` | | YAML | `IndexCacheSize` | | Environment variable | `EVENTSTORE_INDEX_CACHE_SIZE` | **Default**: `0` `IndexCacheSize` is the maximum number of entries in each index LRU cache. The cache size is set to 0 (off) by default because it has an associated memory overhead and can be detrimental to workloads that produce a lot of cache misses. The cache is, however, well suited to read-heavy workloads of long-lived streams. The index LRU cache is only created for index files that have Bloom filters. ### Use index Bloom filters | Format | Syntax | |:---------------------|:-------------------------------------| | Command line | `--use-index-bloom-filters` | | YAML | `UseIndexBloomFilters` | | Environment variable | `EVENTSTORE_USE_INDEX_BLOOM_FILTERS` | **Default**: `true` `UseIndexBloomFilters` is a feature flag which can be used to disable the index Bloom filters. This should not be necessary and this flag will be removed in a future release, but is provided for safety since the index Bloom filters are a new feature. Please contact EventStore if you discover some need to disable this feature. Unless this flag is set to false, EventStoreDB creates a `.bloomfilter` file for each new PTable. The Bloom filter describes which streams are present in the PTable. This speeds up stream reads since EventStoreDB can avoid searching in index files do not contain the stream. Note that immediately after upgrading to a version of EventStoreDB that produces index Bloom filters, no Bloom filters will yet exist. Either wait for new PTables to be produced with Bloom filters in the natural course of writing/merging/scavenging PTables, or rebuild the index for immediate generation. ## Tuning indexes For most EventStoreDB clusters, the default settings are enough to give consistent and good performance. For clusters with larger numbers of events, or those that run in constrained environments the configuration options allow for some tuning to meet operational constraints. The most common optimization needed is to set a `MaxAutoMergeLevel` to avoid large merges occurring across all nodes at approximately the same time. Large index merges use a lot of IOPS and in IOPS constrained environments it is often desirable to have better control over when these happen. Because increasing this value requires an index rebuild you should start with a higher value and decrease until the desired balance between triggering manual merges (operational cost) and automatic merges (IOPS) cost. The exact value to set this varies between environments due to IOPS generated by other operations such as read and write load on the cluster. For example: > A cluster with 3000 256b IOPS can read/write about 0.73Gb/sec (This level of IOPS represents a small cloud instance). Assuming sustained read/write throughput of 0.73Gb/s. When an index merge of level 7 or above starts, it consumes as many IOPS up to all on the node until it completes. Because EventStoreDB has a shared nothing architecture for clustering this operation is likely to cause all nodes to appear to stall simultaneously as they all try and perform an index merge at the same time. By setting `MaxAutoMergeLevel` to 6 or below you can avoid this, and you can run the merge on each node individually keeping read/write latency in the cluster consistent. --- --- url: 'https://docs.kurrent.io/server/v22.10/installation.md' --- # Installation ## Quick start EventStoreDB can run as a single node or as a highly-available cluster. For the cluster deployment, you'd need three server nodes. The installation procedure consists of the following steps: * Create a configuration file for each cluster node. * Install EventStoreDB on each node using one of the available methods. * Obtain SSL certificates, either signed by a publicly trusted or private certificate authority. * Copy the configuration files and SSL certificates to each node. * Start the EventStoreDB service on each node. * Check the cluster status using the Admin UI on any node. ### Default access | User | Password | |-------|----------| | admin | changeit | | ops | changeit | ### Configuration Wizard The [EventStore Configurator](https://configurator.eventstore.com) is an online tool that can help you to go through all the required steps. You can provide the details about your desired deployment topology, and get the following: * Generated configuration files * Instructions for obtaining or generating SSL certificates * Installation guidelines * Client connection details ::: tip Event Store Cloud You can avoid deploying, configuring, and maintaining the EventStoreDB instance yourself by using [Event Store Cloud](https://www.eventstore.com/event-store-cloud). ::: ## Linux ### Install from PackageCloud EventStoreDB has pre-built [packages available for Debian-based distributions](https://packagecloud.io/EventStore/EventStore-OSS) , [manual instructions for distributions that use RPM](https://packagecloud.io/EventStore/EventStore-OSS/install#bash-rpm) , or you can [build from source](https://github.com/EventStore/EventStore#linux). The final package name to install is `eventstore-oss`. If you installed from a pre-built package, the server is registered as a service. Therefore, you can start EventStoreDB with: ```bash:no-line-numbers sudo systemctl start eventstore ``` When you install the EventStoreDB package, the service doesn't start by default. This allows you to change the configuration, located at `/etc/eventstore/eventstore.conf` and to prevent creating database and index files in the default location. ::: warning We recommend that when using Linux you set the 'open file limit' to a high number. The precise value depends on your use case, but at least between `30,000` and `60,000`. ::: ### Building from source You can also build EventStoreDB from source. Before doing that, you need to install the .NET 6 SDK. EventStoreDB packages have the .NET Runtime embedded, so you don't need to install anything except the EventStoreDB package. ### Uninstall If you installed one of the [pre-built packages for Debian based systems](https://packagecloud.io/EventStore/EventStore-OSS), you can remove it with: ```bash:no-line-numbers sudo apt-get purge eventstore-oss ``` This removes EventStoreDB completely, including any user settings. If you built EventStoreDB from source, remove it by deleting the directory containing the source and build and manually removing any environment variables. ## Windows ::: warning EventStoreDB doesn't install as a Windows service. You need to ensure that the server executable starts automatically. ::: ### Install from Chocolatey EventStoreDB has [Chocolatey packages](https://chocolatey.org/packages/eventstore-oss) available that you can install with the following command with administrator permissions. ```powershell:no-line-numbers choco install eventstore-oss ``` ### Download the binaries You can also [download](https://eventstore.com/downloads/) a binary, unzip the archive and run from the folder location with administrator permissions. The following command starts EventStoreDB in dev mode with the database stored at the path `./db` and the logs in `./logs`. Read more about configuring the EventStoreDB server in the [Configuration section](configuration.md). ```powershell:no-line-numbers EventStore.ClusterNode.exe --dev --db ./db --log ./logs ``` EventStoreDB runs in an administration context because it starts an HTTP server through `http.sys`. For permanent or production instances, you need to provide an ACL such as: ```powershell:no-line-numbers netsh http add urlacl url=http://+:2113/ user=DOMAIN\username ``` For more information, refer to Microsoft's `add urlacl` [documentation](https://docs.microsoft.com/en-us/windows/win32/http/add-urlacl). To build EventStoreDB from source, refer to the EventStoreDB [GitHub repository](https://github.com/EventStore/EventStore#building-eventstoredb). ### Uninstall If you installed EventStoreDB with Chocolatey, you can uninstall with: ```powershell:no-line-numbers choco uninstall eventstore-oss ``` This removes the `eventstore-oss` Chocolatey package. If you installed EventStoreDB by [downloading a binary](https://eventstore.com/downloads/), you can remove it by: * Deleting the `EventStore-OSS-Win-*` directory. * Removing the directory from your PATH. ## Docker You can run EventStoreDB in Docker container as a single node, using insecure mode. It is useful in most cases to try out the product and for local development purposes. It's also possible to run a three-node cluster with or without SSL using Docker Compose. Such a setup is closer to what you'd run in production. ### Run with Docker EventStoreDB has a Docker image available for any platform that supports Docker. The following command will start the EventStoreDB node using default HTTP port, without security. You can then connect to it using one of the clients and the `esdb://localhost:2113?tls=false` connection string. The Admin UI will be accessible, but the Stream Browser won't work (as it needs AtomPub to be enabled). ```bash:no-line-numbers docker run --name esdb-node -it -p 2113:2113 -p 1113:1113 \ eventstore/eventstore:latest --insecure --run-projections=All ``` If you want to start the node with legacy protocols enabled (TCP and AtomPub), you need to add a couple of other options: ```bash:no-line-numbers docker run --name esdb-node -it -p 2113:2113 -p 1113:1113 \ eventstore/eventstore:latest --insecure --run-projections=All \ --enable-external-tcp --enable-atom-pub-over-http ``` The command above would run EventStoreDB as a single node without SSL and with the legacy TCP protocol enabled, so you can try out your existing apps with the latest database version. Then, you'd be able to connect to EventStoreDB with gRPC and TCP clients. Also, the Stream Browser will work in the Admin UI. In order to sustainably keep the data, we also recommend mapping the database and index volumes. ### Use Docker Compose You can also run a single-node instance or a three-node secure cluster locally using Docker Compose. #### Insecure single node You can use Docker Compose to run EventStoreDB in the same setup as the `docker run` command mentioned before. Create file `docker-compose.yaml` with following content: @[code{curl}](@samples/server/docker-compose.yaml) Run the instance: ```bash:no-line-numbers docker compose up ``` The command above would run EventStoreDB as a single node without SSL and with the legacy TCP protocol enabled. You also get AtomPub protocol enabled, so you can get the stream browser to work in the Admin UI. #### Secure cluster With Docker Compose, you can also run a three-node cluster with security enabled. That kind of setup is something you'd expect to use in production. Create file `docker-compose.yaml` with following content: @[code{curl}](@samples/server/docker-compose-cluster.yaml) Quite a few settings are shared between the nodes and we use the `env` file to avoid repeating those settings. So, add the `vars.env` file to the same location: @[code{curl}](@samples/server/vars.env) Containers will use the shared volume using the local `./certs` directory for certificates. However, if you let Docker to create the directory on startup, the container won't be able to get write access to it. Therefore, you should create the `certs` directory manually. You only need to do it once. ```bash:no-line-numbers mkdir certs ``` Now you are ready to start the cluster. ```bash:no-line-numbers docker-compose up ``` Watching the log messages, you will see that after some time, the elections process completes. Then you're able to connect to each node using the Admin UI. Nodes should be accessible on the loopback address (`127.0.0.1` or `localhost`) over HTTP and TCP, using ports specified below: | Node | TCP port | HTTP port | | :---- | :------- | :-------- | | node1 | 1111 | 2111 | | node2 | 1112 | 2112 | | node3 | 1113 | 2113 | You have to tell your client to use secure connection for both TCP and gRPC. | Protocol | Connection string | | :------- | :---------------------------------------------------------------------------------------------------- | | TCP | `GossipSeeds=localhost:1111,localhost:1112,localhost:1113;ValidateServer=False;UseSslConnection=True` | | gRPC | `esdb://localhost:2111,localhost:2112,localhost:2113?tls=true&tlsVerifyCert=false` | As you might've noticed, both connection strings have a setting to disable the certificate validation (`ValidateServer=False` for `TCP` and `tlsVerifyCert=false` for `gRPC`). It would prevent the invalid certificate error since the cluster uses a private, auto-generated CA. However, **we do not recommend using this setting in production**. Instead, you can either add the CA certificate to the trusted root CA store or instruct your application to use such a certificate. See the [security section](security.md#certificate-installation-on-a-client-environment) for detailed instructions. ## Compatibility notes Depending on how your EventStoreDB instance is configured, some features might not work. Below are some features that are unavailable due to the specified options. | Feature | Options impact | | :---------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Connection for TCP clients | External TCP is disabled by default. You need to enable it explicitly by using the `EnableExternalTcp` option. | | Connection without SSL or TLS | EventStoreDB 20.6+ is secure by default. Your clients need to establish a secure connection, unless you use the `Insecure` option. | | Authentication and ACLs | When using the `Insecure` option for the server, all the security is disabled. The `Users` menu item is also disabled in the Admin UI. | | Projections | Running projections is disabled by default and the `Projections` menu item is disabled in the Admin UI. You need to enable projections explicitly by using the `RunProjections` option. | | AtomPub protocol | In 20.6+, the AtomPub protocol is disabled by default. If you use this protocol, you have to explicitly enable it by using the `EnableAtomPubOverHttp` option. | | Stream browser | The stream browser feature in Admin UI depends on the AtomPub protocol and is greyed out by default. You need to enable AtomPub (previous line) to make the stream browser work. | --- --- url: 'https://docs.kurrent.io/server/v22.10/introduction.md' --- # Introduction Welcome to the EventStoreDB 22.10 documentation. EventStoreDB is a database designed for [Event Sourcing](https://eventstore.com/blog/what-is-event-sourcing/). This documentation introduces key concepts of EventStoreDB and explains its installation, configuration and operational concerns. EventStoreDB is available in both an Open-Source and an Enterprise version: * EventStoreDB v22.10 OSS is the [open-source](https://github.com/EventStore/EventStore/tree/release/oss-v22.10) and free-to-use edition of EventStoreDB. * EventStoreDB v22.10 Enterprise is available for customers with an EventStoreDB [paid support subscription](https://eventstore.com/support/). EventStoreDB Enterprise adds enterprise-focused features such as LDAP integration, correlation event sequence visualisation, and management CLI. ::: note Although version 22.10 is licensed under the Event Store License based on BSD 3-Clause, starting from version 24.10, EventStoreDB will be licensed under the [Event Store License v2 (ESLv2)](https://github.com/EventStore/EventStore/blob/4cab8ca81a63f0a8f708d5564ea459fe5a7131de/LICENSE.md), which is not an OSI-approved Open Source License. ::: ## Getting started Get started by learning more about the principles of EventStoreDB, Event Sourcing, database installation guidelines and choosing a [client SDK](#protocols-clients-and-sdks). ## Support ### EventStoreDB community Users of the OSS version of EventStoreDB can use the [community forum](https://discuss.eventstore.com) for questions, discussions and getting help from community members. ### Enterprise customers Customers with the paid [support plan](https://eventstore.com/support/) can open tickets using the [support portal](https://eventstore.freshdesk.com). ### Issues Since EventStoreDB is an open-source product, we track most of the issues openly in the EventStoreDB [repository on GitHub](https://github.com/EventStore/EventStore). Before opening an issue, please ensure that a similar issue hasn't been opened already. Also, try searching closed issues that might contain a solution or workaround for your problem. When opening an issue, follow our [guidelines](https://github.com/EventStore/EventStore/blob/master/CONTRIBUTING.md) for bug reports and feature requests. By doing so, you would greatly help us to solve your concerns most efficiently. ## Protocols, clients, and SDKs This getting started guide shows you how to get started with EventStoreDB by setting up an instance or cluster and configuring it. EventStoreDB supports two protocols: gRPC and TCP, described below. ### gRPC protocol The gRPC protocol is based on [open standards](https://grpc.io/) and is widely supported by many programming languages. EventStoreDB uses gRPC to communicate between the cluster nodes as well as for client-server communication. We recommend using gRPC since it is the primary protocol for EventStoreDB moving forward. When developing software that uses EventStoreDB, we recommend using one of the official SDKs. #### EventStoreDB supported clients * Python: [pyeventsourcing/esdbclient](https://pypi.org/project/esdbclient/) * Node.js (javascript/typescript): [EventStore/EventStore-Client-NodeJS](https://github.com/EventStore/EventStore-Client-NodeJS) * Java: [(EventStore/EventStoreDB-Client-Java](https://github.com/EventStore/EventStoreDB-Client-Java) * .NET: [EventStore/EventStore-Client-Dotnet](https://github.com/EventStore/EventStore-Client-Dotnet) * Go: [EventStore/EventStore-Client-Go](https://github.com/EventStore/EventStore-Client-Go) * Rust: [EventStore/EventStoreDB-Client-Rust](https://github.com/EventStore/EventStoreDB-Client-Rust) Read more in the [gRPC clients documentation](@clients/grpc/README.md). #### Community developed clients * [Ruby (yousty/event\_store\_client)](https://github.com/yousty/event_store_client) * [Elixir (NFIBrokerage/spear)](https://github.com/NFIBrokerage/spear) ### Legacy TCP protocol (support ends with 23.10 LTS) EventStoreDB offers a low-level protocol in the form of an asynchronous TCP protocol that exchanges protobuf objects. At present this protocol has adapters for .NET and the JVM. ::: warning Deprecation Note TCP protocol will be available only through version 23.10. Please plan to migrate your applications that use the TCP client SDK to use the gRPC SDK instead. ::: Find out more about configuring the TCP protocol on the [TCP configuration](networking.md#tcp-configuration) page. #### EventStoreDB supported clients * [.NET Framework and .NET Core](http://www.nuget.org/packages/EventStore.Client) #### Community supported clients Community supported clients are developed and maintained by community members, not Event Store staff. Feel free to open issues and PRs, when possible, in the client's GitHub repository. **The following clients which use TCP protocol, will not be compatible with Event Store server versions after 23.10.** * [Node.js (x-cubed/event-store-client)](https://github.com/x-cubed/event-store-client) * [Node.js (nicdex/node-eventstore-client)](https://github.com/nicdex/node-eventstore-client) * [Elixir (exponentially/extreme)](https://github.com/exponentially/extreme) * [Java 8 (msemys/esjc)](https://github.com/msemys/esjc) * [Maven plugin (fuinorg/event-store-maven-plugin)](https://github.com/fuinorg/event-store-maven-plugin) (archived) * [Go (jdextraze/go-gesclient)](https://github.com/jdextraze/go-gesclient) * [PHP (prooph/event-store-client)](https://github.com/prooph/event-store-client/) * [JVM Client (EventStore/EventStore.JVM)](https://github.com/EventStore/EventStore.JVM) * [Haskell (EventStore/EventStoreDB-Client-Haskell)](https://github.com/EventStore/EventStoreDB-Client-Haskell) ### HTTP EventStoreDB also offers an HTTP-based interface, based specifically on the [AtomPub protocol](https://datatracker.ietf.org/doc/html/rfc5023). As it operates over HTTP, this is less efficient, but nearly every environment supports it. Find out more about configuring the HTTP protocol on the [HTTP configuration](networking.md#http-configuration) page. ::: warning Deprecation Note The current AtomPub-based HTTP application API is disabled by default since v20 of EventStoreDB. You can enable it by adding an [option](networking.md#atompub) to the server configuration. Although we plan to remove AtomPub support from future server versions, the server management HTTP API will remain available. ::: As the AtomPub protocol doesn't get any changes, you can use the v5 [HTTP API documentation](./http-api/introduction.md) for it. #### Community developed clients * [PHP (prooph/event-store-http-client)](https://github.com/prooph/event-store-http-client/) * [Ruby (yousty/event\_store\_client)](https://github.com/yousty/event_store_client) --- --- url: 'https://docs.kurrent.io/server/v22.10/networking.md' --- # Networking ## Network configuration EventStoreDB provides two interfaces: * HTTP(S) for gRPC communication and REST APIs * TCP for cluster replication (internal) and legacy clients (external) Nodes in the cluster replicate with each other using the TCP protocol, but use gRPC for [discovering other cluster nodes](cluster.md#discovering-cluster-members). Server nodes separate internal and external TCP communication explicitly, but use a single HTTP binding. Replication between the cluster nodes is considered internal and all the TCP clients that connect to the database are external. EventStoreDB allows you to separate network configurations for internal and external communication. For example, you can use different network interfaces on the node. All the internal communication can go over the isolated private network but access for external clients can be configured on another interface, which is connected to a more open network. You can set restrictions on those external interfaces to make your deployment more secure. For gRPC and HTTP, there's no internal vs external separation of traffic. ## HTTP configuration HTTP is the primary protocol for EventStoreDB. It is used in gRPC communication and HTTP APIs (management, gossip and diagnostics). Unlike for [TCP protocol](#tcp-configuration), there is no separation between internal and external communication. The HTTP endpoint always binds to the IP address configured in the `ExtIp` setting. | Format | Syntax | |:---------------------|:--------------------| | Command line | `--ext-ip` | | YAML | `ExtIp` | | Environment variable | `EVENTSTORE_EXT_IP` | When the `ExtIp` setting is not provided, EventStoreDB will use the first available non-loopback address. You can also bind HTTP to all available interfaces using `0.0.0.0` as the setting value. If you do that, you'd need to configure the `ExtHostAdvertiseAs` setting (read mode [here](#network-address-translation)), since `0.0.0.0` is not a valid IP address to connect from the outside world. The default HTTP port is `2113`. Depending on the [security settings](security.md) of the node, it either responds over plain HTTP or via HTTPS. There is no HSTS redirect, so if you try reaching a secure node via HTTP, you can an empty response. You can change the HTTP port using the `HttpPort` setting: | Format | Syntax | |:---------------------|:-----------------------| | Command line | `--http-port` | | YAML | `HttpPort` | | Environment variable | `EVENTSTORE_HTTP_PORT` | **Default**: `2113` If your network setup requires any kind of IP address, DNS name and port translation for internal or external communication, you can use available [address translation](#network-address-translation) settings. ### Keep-alive pings The reliability of the connection between the client application and database is crucial for the stability of the solution. If the network is not stable or has some periodic issues, the client may drop the connection. Stability is essential for the [stream subscriptions](@clients/grpc/subscriptions.md) where a client is listening to database notifications. Having an existing connection open when an app resumes activity allows for the initial gRPC calls to be made quickly, without any delay caused by the reestablished connection. EventStoreDB supports the built-in gRPC mechanism for keeping the connection alive. If the other side does not acknowledge the ping within a certain period, the connection will be closed. Note that pings are only necessary when there's no activity on the connection. Keepalive pings are enabled by default, with the default interval set to 10 seconds. The default value is based on the [gRPC proposal](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#extending-for-basic-health-checking) that suggests 10 seconds as the minimum. It's a compromise value to ensure that the connection is open and not making too many redundant network calls. You can customise the following Keepalive settings: #### KeepAliveInterval After a duration of `keepAliveInterval` (in milliseconds), if the server doesn't see any activity, it pings the client to see if the transport is still alive. | Format | Syntax | |:---------------------|:------------------------| | Command line | `--keep-alive-interval` | | YAML | `KeepAliveInterval` | | Environment variable | `KEEP_ALIVE_INTERVAL` | **Default**: `10000` (ms, 10 sec) #### KeepAliveTimeout After having pinged for keepalive check, the server waits for a duration of `keepAliveTimeout` (in milliseconds). If the connection doesn't have any activity even after that, it gets closed. | Format | Syntax | |:---------------------|:-----------------------| | Command line | `--keep-alive-timeout` | | YAML | `KeepAliveTimeout` | | Environment variable | `KEEP_ALIVE_TIMEOUT` | **Default**: `10000` (ms, 10 sec) As a general rule, we do not recommend putting EventStoreDB behind a load balancer. However, if you are using it and want to benefit from the Keepalive feature, then you should make sure if the compatible settings are properly set. Some load balancers may also override the Keepalive settings. Most of them require setting the idle timeout larger/longer than the `keepAliveTimeout`. We suggest checking the load balancer documentation before using Keepalive pings. ### AtomPub The AtomPub application protocol over HTTP is disabled by default since v20. We plan to deprecate the AtomPub support in the future versions, but we aim to provide a replacement before we finally deprecate AtomPub. In Event Store Cloud, the AtomPub protocol is enabled. For self-hosted instances, use the configuration setting to enable AtomPub. | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--enable-atom-pub-over-http` | | YAML | `EnableAtomPubOverHttp` | | Environment variable | `ENABLE_ATOM_PUB_OVER_HTTP` | **Default**: `false` (AtomPub is disabled) ### Kestrel Settings It's generally not expected that you'll need to update the Kestrel configuration that EventStoreDB has set by default, but it's good to know that you can update the following settings if needed. Kestrel uses the `kestrelsettings.json` configuration file. This file should be located in the [default configuration directory](configuration.md#configuration-file). #### MaxConcurrentConnections Sets the maximum number of open connections. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentconnections?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.MaxConcurrentConnections` in the settings file. #### MaxConcurrentUpgradedConnections Sets the maximum number of open, upgraded connections. An upgraded connection is one that has been switched from HTTP to another protocol, such as WebSockets. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentupgradedconnections?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.MaxConcurrentUpgradedConnections` in the settings file. #### Http2 InitialConnectionWindowSize Sets how much request body data the server is willing to receive and buffer at a time aggregated across all requests (streams) per connection. Note requests are also limited by `KestrelInitialStreamWindowSize` The value must be greater than or equal to 65,535 and less than 2^31. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialconnectionwindowsize?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.Http2.InitialConnectionWindowSize` in the settings file. #### Http2 InitialStreamWindowSize Sets how much request body data the server is willing to receive and buffer at a time per stream. Note connections are also limited by `KestrelInitialConnectionWindowSize` Value must be greater than or equal to 65,535 and less than 2^31. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialstreamwindowsize?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.Http2.InitialStreamWindowSize` in the settings file. ## TCP configuration The TCP protocol is used internally for cluster nodes to replicate with each other. It happens over the [internal](#internal) TCP communication. In addition, you can enable [external](#external) TCP if you use the TCP client library in your applications. ### Internal Internal TCP binds to the IP address specified in the `IntIp` setting. It must be configured if you run a multi-node cluster. By default, EventStoreDB binds its internal networking on the loopback interface only (`127.0.0.1`). You can change this behaviour and tell EventStoreDB to listen on a specific internal IP address. To do that set the `IntIp` to `0.0.0.0` or the IP address of the network interface. | Format | Syntax | |:---------------------|:--------------------| | Command line | `--int-ip` | | YAML | `IntIp` | | Environment variable | `EVENTSTORE_Int_IP` | **Default**: `127.0.0.1` (loopback). If you keep this setting to its default value, cluster nodes won't be able to talk to each other. By default, EventStoreDB uses port `1112` for internal TCP. You can change this by specifying the `IntTcpPort` setting. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--int-tcp-port` | | YAML | `IntTcpPort` | | Environment variable | `EVENTSTORE_INT_TCP_PORT` | **Default**: `1112` ### External By default, TCP protocol is not exposed externally. If you use a TCP client library in your applications, you need to enable external TCP explicitly using the setting below. | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--enable-external-tcp` | | YAML | `EnableExternalTcp` | | Environment variable | `EVENTSTORE_ENABLE_EXTERNAL_TCP` | **Default**: `false`, TCP is disabled externally. When enabled, the external TCP will be exposed on the `ExtIp` address (described [here](#http-configuration)) using port `1113`. You can change the external TCP port using the `ExtTcpPort` setting. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--ext-tcp-port` | | YAML | `ExtTcpPort` | | Environment variable | `EVENTSTORE_EXT_TCP_PORT` | **Default**: `1113` ### Security When the node is secured (by default), all the TCP traffic will use TLS. You can disable TLS for TCP internally and externally using the settings described below. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-internal-tcp-tls` | | YAML | `DisableInternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_INTERNAL_TCP_TLS` | **Default**: `false` | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-external-tcp-tls` | | YAML | `DisableExternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_EXTERNAL_TCP_TLS` | **Default**: `false` If your network setup requires any kind of IP address, DNS name and port translation for internal or external communication, you can use available [address translation](#network-address-translation) settings. ## Network address translation Due to NAT (network address translation), or other reasons a node may not be bound to the address it is reachable from other nodes. For example, the machine has an IP address of `192.168.1.13`, but the node is visible to other nodes as `10.114.12.112`. Options described below allow you to tell the node that even though it is bound to a given address it should not gossip that address. When returning links over HTTP, EventStoreDB will also use the specified addresses instead of physical addresses, so the clients that use HTTP can follow those links. Another case when you might want to specify the advertised address although there's no address translation involved. When you configure EventStoreDB to bind to `0.0.0.0`, it will use the first non-loopback address for gossip. It might or might not be the address you want it to use. Whilst the best way to avoid such a situation is to configure the binding properly using the `ExtIp` and `IntIp` settings, you can also use address translation setting with the correct IP address or DNS name. Also, even if you specified the `ExtIp` and `IntIp` settings in the configuration, you might still want to override the advertised address if you want to use hostnames and not IP addresses. That might be needed when running a secure cluster with certificates that only contain DNS names of the nodes. The only place where these settings make any effect is the [gossip](cluster.md#gossip-protocol) endpoint response. ## HTTP translations By default, a cluster node will advertise itself using `ExtIp` and `HttpPort`. You can override the advertised HTTP port using the `HttpPortAdvertiseAs` setting. | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--http-port-advertise-as` | | YAML | `HttpPortAdvertiseAs` | | Environment variable | `EVENTSTORE_HTTP_PORT_ADVERTISE_AS` | If you want the node to advertise itself using the hostname rather than its IP address, use the `ExtHostAdvertiseAs` setting. | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--ext-host-advertise-as` | | YAML | `ExtHostAdvertiseAs` | | Environment variable | `EVENTSTORE_EXT_HOST_ADVERTISE_AS` | ### TCP translations Both internal and external TCP ports can be advertised using custom values: | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--int-tcp-port-advertise-as` | | YAML | `IntTcpPortAdvertiseAs` | | Environment variable | `EVENTSTORE_INT_TCP_PORT_ADVERTISE_AS` | | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--ext-tcp-port-advertise-as` | | YAML | `ExtTcpPortAdvertiseAs` | | Environment variable | `EVENTSTORE_EXT_TCP_PORT_ADVERTISE_AS` | If you want to change how the node TCP address is advertised internally, use the `IntHostAdvertiseAs` setting. You can use an IP address or a hostname. | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--int-host-advertise-as` | | YAML | `IntHostAdvertiseAs` | | Environment variable | `EVENTSTORE_INT_HOST_ADVERTISE_AS` | Externally, TCP is advertised using the address specified in the `ExtIp` or `ExtHostAdvertiseAs` (as for HTTP). ### Advertise to clients In some cases, the cluster needs to advertise itself to clients using a completely different set of addresses and ports. Usually, you need to do it because addresses and ports configured for the HTTP protocol are not available as-is to the outside world. One of the examples is running a cluster in Docker Compose. In such environment, HTTP uses internal hostnames in the Docker network, which isn't accessible on the host. So, in order to connect to the cluster from the host machine, you need to use `localhost` and translated HTTP ports to reach the cluster nodes. To configure how the cluster nodes advertise to clients, use the `Advertise<*>ToClient` settings listed below. Specify the advertised hostname or IP address: | Format | Syntax | |:---------------------|:-----------------------------------------| | Command line | `--advertise-host-to-client-as` | | YAML | `AdvertiseHostToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS` | Specify the advertised HTTP(S) port: | Format | Syntax | |:---------------------|:----------------------------------------------| | Command line | `--advertise-http-port-to-client-as` | | YAML | `AdvertiseHttpPortToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_HTTP_PORT_TO_CLIENT_AS` | Specify the advertised TCP port (only if external TCP is enabled): | Format | Syntax | |:---------------------|:---------------------------------------------| | Command line | `--advertise-tcp-port-to-client-as` | | YAML | `AdvertiseTcpPortToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_TCP_PORT_TO_CLIENT_AS` | ## Heartbeat timeouts EventStoreDB uses heartbeats over all TCP connections to discover dead clients and nodes. Heartbeat timeouts should not be too short, as short timeouts will produce false positives. At the same time, setting too long timeouts will prevent discovering dead nodes and clients in time. Each heartbeat has two points of configuration. The first is the *interval*, this represents how often the system should consider a heartbeat. EventStoreDB doesn't send a heartbeat for every interval, but only if it has not heard from a node within the configured interval. On a busy cluster, you may never see any heartbeats. The second point of configuration is the *timeout*. This determines how long EventStoreDB server waits for a client or node to respond to a heartbeat request. Different environments need different values for these settings. The defaults are likely fine on a LAN. If you experience frequent elections in your environment, you can try to increase both interval and timeout, for example: * An interval of 5000ms. * A timeout of 1000ms. ::: tip If in doubt, choose higher numbers. This adds a small period of time to discover a dead client or node and is better than the alternative, which is false positives. ::: Internal TCP heartbeat (between cluster nodes): | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--int-tcp-heartbeat-interval` | | YAML | `IntTcpHeartbeatInterval` | | Environment variable | `EVENTSTORE_INT_TCP_HEARTBEAT_INTERVAL` | **Default**: `700` (ms) | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--int-tcp-heartbeat-timeout` | | YAML | `IntTcpHeartbeatTimeout` | | Environment variable | `EVENTSTORE_INT_TCP_HEARTBEAT_TIMEOUT` | **Default**: `700` (ms) External TCP heartbeat (between client and server): | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--ext-tcp-heartbeat-interval` | | YAML | `ExtTcpHeartbeatInterval` | | Environment variable | `EVENTSTORE_EXT_TCP_HEARTBEAT_INTERVAL` | **Default**: `2000` (ms) | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--ext-tcp-heartbeat-timeout` | | YAML | `ExtTcpHeartbeatTimeout` | | Environment variable | `EVENTSTORE_EXT_TCP_HEARTBEAT_TIMEOUT` | **Default**: `1000` (ms) ### gRPC heartbeats For the gRPC heartbeats, EventStoreDB and its gRPC clients use the protocol feature called *Keepalive ping*. Read more about it on the [HTTP configuration page](#keep-alive-pings). ## Exposing endpoints If you need to disable some HTTP endpoints on the external HTTP interface, you can change some settings below. It is possible to disable the Admin UI, stats and gossip port to be exposed externally. You can disable the Admin UI on external HTTP by setting `AdminOnExt` setting to `false`. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--admin-on-ext` | | YAML | `AdminOnExt` | | Environment variable | `EVENTSTORE_ADMIN_ON_EXT` | **Default**: `true`, Admin UI is enabled on the external HTTP. Exposing the `stats` endpoint externally is required for the Admin UI and can also be useful if you collect stats for an external monitoring tool. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--stats-on-ext` | | YAML | `StatsOnExt` | | Environment variable | `EVENTSTORE_STATS_ON_EXT` | **Default**: `true`, stats endpoint is enabled on the external HTTP. You can also disable the gossip protocol in the external HTTP interface. If you do that, ensure that the internal interface is properly configured. Also, if you use [gossip with DNS](cluster.md#cluster-with-dns), ensure that the [gossip port](cluster.md#gossip-port) is set to the internal HTTP port. | Format | Syntax | |:---------------------|:---------------------------| | Command line | `--gossip-on-ext` | | YAML | `GossipOnExt` | | Environment variable | `EVENTSTORE_GOSSIP_ON_EXT` | **Default**: `true`, gossip is enabled on the external HTTP. --- --- url: 'https://docs.kurrent.io/server/v22.10/operations.md' --- # Maintenance EventStoreDB requires regular maintenance with three operational concerns: * [Scavenging](#scavenging) for freeing up space after deleting events. * [Backup and restore](#backup-and-restore) for disaster recovery. * [Certificate update](#certificate-update-upon-expiry) to renew certificates. You might also be interested learning about EventStoreDB [diagnostics](diagnostics.md) and [indexes](indexes.md), which might require attention. ## Scavenging In EventStoreDB, events are no longer present in stream reads or subscriptions after they have been deleted or they have expired according to the metadata of the stream. The events are, however, still present in the database and will be visible in reads and subscriptions to the `$all` stream. To remove these events from the database, which may be necessary for GDPR, you need to run a 'scavenge' on each of your nodes. A scavenge operation removes events and reclaims disk space by creating a copy of the relevant chunk, minus those events, and then deleting the old chunk. The scavenged events are also removed from the index. ::: warning Scavenging is destructive. Once a scavenge has run, you cannot recover any deleted events except from a backup. ::: ### Starting a scavenge Start a scavenge by issuing an empty `POST` request to the HTTP API with the credentials of an `admin` or `ops` user: @[code{curl}](@samples/server/scavenge.sh) Scavenges can also be started from the *Admin* page of the Admin UI. ![Start a scavenge in the Admin UI](images/admin-scavenge.png) Each node in a cluster contains an independent copy of the database. As such, when you run a scavenge, you need to issue a scavenge request to each node. Scavenges can be run concurrently, or be run in series to spread the load. ### Getting the current scavenge ID Get the ID of the currently running scavenge, if there is one, by issuing a `GET` request to the following HTTP API endpoint with the `admin` or `ops` credentials. ```bash:no-line-numbers curl -i -X GET http://127.0.0.1:2113/admin/scavenge/current -u "admin:changeit" ``` ### Stopping a scavenge Stop a running scavenge operation by issuing a `DELETE` request to the HTTP API with the credentials of an `admin` or `ops` user and the ID of the scavenge you want to stop: ```bash:no-line-numbers curl -i -X DELETE http://localhost:2113/admin/scavenge/{scavengeId} -u "admin:changeit" ``` Or stop the currently running scavenge by specifying an ID of `current`: ```bash:no-line-numbers curl -i -X DELETE http://localhost:2113/admin/scavenge/current -u "admin:changeit" ``` A 200 response is returned after the scavenge has stopped. Scavenges can also be stopped from the *Admin* page of the Admin UI. ::: tip A scavenge can be stopped at any time. The next time a scavenge is started, it will resume from the place the previous scavenge stopped. ::: ### Viewing progress The logs contain detailed information about the progress of the scavenge. The [execution phase](#execution-phase) of the scavenge emits events into streams. Each scavenge operation generates a new stream containing the events related to that operation. Refer to the `$scavenges` [stream documentation](streams.md#scavenges) to learn how to observe the scavenging operation progress and status. ## Scavenging best practices ### Backups Do not take [file-copy](#types-of-backups) backups while scavenge is running. Stop the scavenge and resume it after the backup. [Disk snapshot](#types-of-backups) backups can be taken while scavenge is running. ### How often to scavenge This depends on: * How often you delete streams. * How you set `$maxAge`, `$maxCount` or `$tb` metadata on your streams. * How important freeing the disk space is to you. * Your GDPR requirements. You can tell from the scavenge output in the logs and streams how much data it is removing. This can help guide how frequently to scavenge. You can set up a scheduled task, for example using cron on Linux or [Windows Task Scheduler](https://learn.microsoft.com/en-us/windows/win32/taskschd/task-scheduler-start-page), to trigger a scavenge as often as you need. ### Spreading the load Scavenging does place extra load on the server, especially in terms of disk IO. If this is a concern, consider spreading the load with the following: * Run scavenge on one node at a time. * Run scavenge on the Follower nodes to avoid adding load on the Leader. Then resign the Leader node and perform scavenging on that node. * Stop the scavenge during peak times and resume it afterwards. * Use the [throttle](#throttle-percent) and [threshold](#threshold) options. ## Scavenging algorithm Central to the scavenging process is the concept of *scavenge points*. Physically, these are log records in the transaction log, each containing the following information: * The position in the log to which scavenge will run * A number unique to the scavenge point (counting from 0) * The time (`EffectiveNow`) used to determine if an events `maxAge` has been exceeded * The threshold that a chunk's weight must reach to be executed Any run of the scavenge process is associated with a single scavenge point, and it scavenges the log up to that point. Log records after that scavenge point do not exist as far as that scavenge is concerned. In this way, a scavenge can be run on the first node, creating a scavenge point. Then it can be run (potentially later) on other nodes to scavenge up to the same point, producing the same effect on the log. The scavenging algorithm itself consists of several phases: ### Beginning When a scavenge is started, it first checks to see if a previous scavenge was stopped. If so, it resumes from where the previous scavenge stopped. Otherwise, it begins a fresh scavenge. When beginning a fresh scavenge, it checks to see if a scavenge point already exists that has not been reached by previous scavenges. If so, it begins scavenging up to that point. Otherwise, it writes a new scavenge point to the log (which is replicated to the other nodes) and then begins a scavenge up to there. Writing a new scavenge point also causes the active chunk to be completed so that it can be scavenged. ### Accumulation phase During the accumulation phase, the scavenging process reads through the chunks that have been added since the previous scavenge, up to the current scavenge point. It finds necessary information (such as tombstones and metadata records) and stores it in the scavenge database. In this way, any particular chunk is only accumulated once, regardless of how many times scavenge is run. ::: tip The first time the scavenge is run it needs to accumulate all the chunks. Typically, this makes the first scavenge take longer than subsequent scavenges. ::: ### Calculation phase During the calculation phase, the scavenging process calculates which events can be discarded and in which chunks the events are located for each stream the scavenging process accumulated tombstones or metadata. Weight is assigned to those chunks. ### Execution phase The execution phase uses the data from the calculation phase to remove events from the chunks and indexes. Small chunks are then merged together. Only chunks whose weight meets the threshold will be executed. ### Cleaning phase This final phase removes data from the scavenge database that is no longer needed. ## Starting a scavenge When starting a scavenge with an HTTP POST request, the following options are available. ### Threads Specify the number of threads to use for running the scavenging process. The default value is 1. Example: ```bash:no-line-numbers curl -i -X POST http://127.0.0.1:2113/admin/scavenge?threads=2 -u "admin:changeit" ``` ### Threshold By default, all chunks that have events to be removed are scavenged. Setting this option allows you to scavenge only the chunks that have a sufficiently large number of events expected to be removed. This allows scavenge to run more quickly by focusing on the chunks that would benefit from it most. The weights of the chunks that are being scavenged or skipped can be found in the log files by searching for the term "with weight". The weight of a chunk is approximately twice the number of records that can be removed from it. Possible values for the threshold: * `-1`: Scavenge all chunks, even if there are no events to remove. This should not be necessary in practice. * `0`: Default value. Scavenges every chunk that has events to remove. * `> 0`: The minimum weight a chunk must have in order to be scavenged. Example: ```bash:no-line-numbers curl -i -X POST http://127.0.0.1:2113/admin/scavenge?threshold=2000 -u "admin:changeit" ``` ::: tip Setting a positive threshold means that not all deleted and expired events will be removed. This may be important to consider with respect to GDPR. ::: ### Throttle percent The scavenging process can be time-consuming and resource-intensive. You can control the speed and resource usage of the scavenge process using the `throttlePercent` option. When set to 100 (default value), the scavenge process runs at full speed. Setting it to 50 makes the process take twice as long by pausing regularly. A scavenge can be stopped and restarted with a different `throttlePercent`. For `throttlePercent` values: * Throttle percent must be between 1 and 100. * Throttle percent must be 100 for a multi-threaded scavenge. Example: ```bash:no-line-numbers curl -i -X POST http://127.0.0.1:2113/admin/scavenge?throttlePercent=50 -u "admin:changeit" ``` ### Sync Only The `syncOnly` option is a boolean value and is false by default. When true, it prevents the creation of a new scavenge point and will only run the scavenge if there is an existing scavenge point that has not yet been reached in a previous scavenged. After running a scavenge on one node, this flag can be used to ensure that a subsequent node scavenges to that same point. Example: ```bash:no-line-numbers curl -i -X POST http://127.0.0.1:2113/admin/scavenge?syncOnly=true -u "admin:changeit" ``` ### Start From Chunk This option is deprecated. It is ignored and will be removed. ## Scavenging database options Below you can find some options that change the way how scavenging works on the server node. ### Disable chunk merging Scavenged chunks may be small enough to be merged into a single physical chunk file of approximately 256 MB. This behaviour can be disabled with this option. | Format | Syntax | | :------------------- | :------------------------------------ | | Command line | `--disable-scavenge-merging` | | YAML | `DisableScavengeMerging` | | Environment variable | `EVENTSTORE_DISABLE_SCAVENGE_MERGING` | **Default**: `false`, small scavenged chunks are merged together. ### Scavenge history Each scavenge operation is assigned a unique ID and creates a corresponding stream. These streams provide valuable insights into the scavenge history, including the duration of each operation and the amount of disk space reclaimed. However, retaining this history indefinitely may not be necessary. To manage this, you can limit the duration for which scavenge history is stored in the database using this option. | Format | Syntax | | :------------------- | :------------------------------------ | | Command line | `--scavenge-history-max-age` | | YAML | `ScavengeHistoryMaxAge` | | Environment variable | `EVENTSTORE_SCAVENGE_HISTORY_MAX_AGE` | **Default**: `30` (days) ### Always keep scavenged (*Deprecated*) This option ensures that the newer chunk from a scavenge operation is always kept. | Format | Syntax | | :------------------- | :--------------------------------- | | Command line | `--always-keep-scavenged` | | YAML | `AlwaysKeepScavenged` | | Environment variable | `EVENTSTORE_ALWAYS_KEEP_SCAVENGED` | ### Scavenge backend page size Specify the page size of the scavenge database. The default value is 16 KiB. | Format | Syntax | | :------------------- | :-------------------------------------- | | Command line | `--scavenge-backend-page-size` | | YAML | `ScavengeBackendPageSize` | | Environment variable | `EVENTSTORE_SCAVENGE_BACKEND_PAGE_SIZE` | **Default**: `16` (KiB) ### Scavenge backend cache size Specify the amount of memory, in bytes, to use for backend caching during scavenging. The default value is 64 MiB. | Format | Syntax | | :------------------- | :--------------------------------------- | | Command line | `--scavenge-backend-cache-size` | | YAML | `ScavengeBackendCacheSize` | | Environment variable | `EVENTSTORE_SCAVENGE_BACKEND_CACHE_SIZE` | ### Scavenge hash users cache capacity Specify the number of stream hashes to remember when checking for collisions. If the accumulation phase is reporting a lot of cache misses, it may benefit from increasing this number. The default value is 100000. | Format | Syntax | | :------------------- | :---------------------------------------------- | | Command line | `--scavenge-hash-users-cache-capacity` | | YAML | `ScavengeHashUsersCacheCapacity` | | Environment variable | `EVENTSTORE_SCAVENGE_HASH_USERS_CACHE_CAPACITY` | ## Backup and restore Backing up an EventStoreDB database is straightforward but relies on carrying out the steps below in the correct order. ### Types of backups There are two main ways to perform backups: * **Disk snapshots**: If your infrastructure is virtualized, [disk snapshots](#disks-snapshot) are an option and the easiest way to perform backup and restore operations. * **Regular file copy**: * [Simple full backup](#simple-full-backup--restore): when the DB size is small and the frequency of appends is low. * [Differential backup](#differential-backup--restore): when the DB size is large or the system has a high append frequency. ### Backup and restore best practices * Backing up one node is recommended. However, ensure that the node you choose to back up is **up to date** and **connected to the cluster**. * For additional safety, you can also back up at least a quorum of nodes. * Do not attempt to back up a node using file copy at the same time a scavenge operation is running. * [Read-only replica](cluster.md#read-only-replica) nodes may be used as a backup source. * When either running a backup or restoring, do not mix backup files of different nodes. * A restore must happen on a *stopped* node. * The restore process can happen on any node of a cluster. * You can restore any number of nodes in a cluster from the same backup source. It means, for example, in the event of a non-recoverable three node cluster, that the same backup source can be used to restore a completely new three node cluster. * When you restore a node that was the backup source, perform a full backup after recovery. ### Database files information By default, there are two directories containing data that needs to be included in the backup: * `db/ ` where the data is located * `index/ ` where the indexes are kept The exact name and location are dependent on your configuration. * `db/ ` contains: * the chunks files named `chk-X.Y` where `X` is the chunk number and `Y` the version. * the checkpoints files `*.chk` (`chaser.chk`, `epoch.chk`, `proposal.chk`, `truncate.chk`, `writer.chk`) * `index/ ` contains: * the index map: `indexmap` * the indexes: UUID named files , e.g `5a1a8395-94ee-40c1-bf93-aa757b5887f8` ### Disks snapshot If the `db/ ` and `index/ ` directories are on the same volume, a snapshot of that volume is enough. However, if they are on different volumes, take first a snapshot of the volume containing the `index/ ` directory and then a snapshot of the volume containing the `db/ ` directory. ### Simple full backup & restore #### Backup 1. Copy any index checkpoint files (`/**/*.chk`) to your backup location. 2. Copy the other index files to your backup location (the rest of ``, excluding the checkpoints). 3. Copy the database checkpoint files (`*.chk`) to your backup location. 4. Copy the chunk files (`chunk-X.Y`) to your backup location. For example, with a database in `data` and index in `data/index`: ```bash:no-line-numbers rsync -aIR data/./index/**/*.chk backup rsync -aI --exclude '*.chk' data/index backup rsync -aI data/*.chk backup rsync -a data/*.0* backup ``` #### Restore 1. Ensure the EventStoreDB process is stopped. Restoring a database on running instance is not possible and, in most cases, will lead to data corruption. 2. Copy all files to the desired location. 3. Create a copy of `chaser.chk` and call it `truncate.chk`. This effectively overwrites the restored `truncate.chk`. ### Differential backup & restore The following procedure is designed to minimize the backup storage space, and can be used to do a full and differential backup. #### Backup First backup the index: 1. If there are no files in the index directory (apart from directories), go to step 7. 2. Copy the `index/indexmap` file to the backup. If the source file does not exist, repeat until it does. 3. Make a list `indexFiles` of all the `index/` and `index/.bloomfilter` files in the source. 4. Copy the files listed in `indexFiles` to the backup, skipping file names already in the backup. 5. Compare the contents of the `indexmap` file in the source and the backup. If they are different (i.e. the `indexmap` file has changed since step 2 or no longer exists), go back to step 2. 6. Remove `index/` and `index/.bloomfilter` files from the backup that are not listed in `indexFiles`. 7. Copy the `index/stream-existence/streamExistenceFilter.chk` file (if present) to the backup. 8. Copy the `index/stream-existence/streamExistenceFilter.dat` file (if present) to the backup. 9. Copy the `index/scavenging/scavenging.db` file (if present) to the backup. It should be the only file in the `scavenging` directory. Then backup the log: 1. Rename the last chunk in the backup to have a `.old` suffix. e.g. rename `chunk-000123.000000` to `chunk-000123.000000.old`. 2. Copy `chaser.chk` to the backup. 3. Copy `epoch.chk` to the backup. 4. Copy `writer.chk` to the backup. 5. Copy `proposal.chk` to the backup. 6. Make a list `chunkFiles` of all chunk files (`chunk-X.Y`) in the source. 7. Copy the files listed in `chunkFiles` to the backup, skipping file names already in the backup. All files should copy successfully. None should have been deleted since scavenge is not running. 8. Remove any chunks from the backup that are not in the `chunksFiles` list. This will include the `.old` file from step 1. #### Restore 1. Ensure the Event Store DB process is stopped. Restoring a database on running instance is not possible and, in most cases, will lead to data corruption. 2. Copy all files to the desired location. 3. Create a copy of `chaser.chk` and call it `truncate.chk`. ### Other options for data recovery #### Additional node (aka Hot Backup) Increase the cluster size from 3 to 5 to keep further copies of data. This increase in the cluster size will slow the cluster's writing performance as two follower nodes will need to confirm each write. Alternatively, you can use a [read-only replica](cluster.md#read-only-replica) node, which is not a part of the cluster. In this case, the write performance will be minimally impacted. #### Alternative storage Set up a durable subscription that writes all events to another storage mechanism such as a key/value or column store. These methods would require a manual set up for restoring a cluster node or group. #### Backup cluster Use a second EventStoreDB cluster as a backup. Such a strategy is known as a primary/secondary back up scheme. The primary cluster asynchronously pushes data to the second cluster using a durable subscription. The second cluster is available in case of a disaster on the primary cluster. If you are using this strategy, we recommend you only support manual fail over from primary to secondary as automated strategies risk causing a [split brain](http://en.wikipedia.org/wiki/Split-brain_%28computing%29) problem. ## Certificate update upon expiry In EventStoreDB, the certificates require updating when they have expired or are going to expire soon. Follow the steps below to perform a rolling certificate update. ### Step 1: Generate new certificates The new certificates can be created in the same manner as you generated the existing certificates. You can use the EventStore [es-gencert-cli](https://github.com/EventStore/es-gencert-cli) tool to generate the CA and node certificates. You can also follow the [Configurator](https://configurator.eventstore.com/) to create commands for generating the certificates based on your cluster's configuration. ::: tip As of version 23.10.0, it is possible to do a rolling update with new certificates having a Common Name (CN) that's different from the original certificates. If `CertificateReservedNodeCommonName` was set in your configuration, any changes to its value will be taken into consideration during a config reload. ::: ### Step 2: Replace the certificates The next step is to replace the outdated certificates with the newly generated certificates. If you are using symlinks, then you can update the symlink to point it to the new certificates. #### Linux OS We normally have a symlink that points to the existing certificates: ca -> `/etc/eventstore/certs/ca` node.crt -> `/etc/eventstore/certs/node.crt` node.key -> `/etc/eventstore/certs/node.key` Update the symlink so that the link points to the new certificates: ca -> `/etc/eventstore/newCerts/ca` node.crt -> `/etc/eventstore/newCerts/node.crt` node.key -> `/etc/eventstore/newCerts/node.key` In the above example we have the links in a folder at path `/home/ubuntu/links`. ### Step 3: Reload the configuration You can reload the certificate configuration without restarting the node by issuing the following curl command. ```bash:no-line-numbers curl -k -X POST --basic https://{nodeAddress}:{HttpPort}/admin/reloadconfig -u {username}:{Password} ``` For Example: ```bash:no-line-numbers curl -k -X POST --basic https://127.0.0.1:2113/admin/reloadconfig -u admin:changeit ``` #### Linux OS Linux users can also send the SIGHUP signal to the EventStoreDB to reload the certificates. For Example: ``` kill -s SIGHUP 38956 ``` Here "38956" is the Process ID (PID) of EventStoreDB on our local machine. To find the PID, use the following command in the Linux terminal. ``` pidof eventstored ``` Once the configuration has been reloaded successfully, the server logs will look something like: ``` [108277,30,14:46:07.453,INF] Reloading the node's configuration since a request has been received on /admin/reloadconfig. [108277,29,14:46:07.457,INF] Loading the node's certificate(s) from file: "/home/ubuntu/links/node.crt" [108277,29,14:46:07.488,INF] Loading the node's certificate. Subject: "CN=eventstoredb-node", Previous thumbprint: "05526714107700C519E24794E8964A3B30EF9BD0", New thumbprint: "BE6D5CD681D7B9281D60A5969B5A7E31AF775E9F" [108277,29,14:46:07.488,INF] Loading intermediate certificate. Subject: "CN=EventStoreDB Intermediate CA 78ae8d5a159b247568039cf64f4b04ad, O=Event Store Ltd, C=UK", Thumbprint: "ED4AA0C5ED4AD120DDEE2FA8B3B0CCC5A30B81E3" [108277,29,14:46:07.489,INF] Loading trusted root certificates. [108277,29,14:46:07.490,INF] Loading trusted root certificate file: "/home/ubuntu/links/ca/ca.crt" [108277,29,14:46:07.491,INF] Loading trusted root certificate. Subject: "CN=EventStoreDB CA c39fece76b4efcb65846145c942c037c, O=Event Store Ltd, C=UK", Thumbprint: "5F020804E8D7C419F9FC69ED3B4BC72DD79A5996" [108277,29,14:46:07.493,INF] Certificate chain verification successful. [108277,29,14:46:07.493,INF] All certificates successfully loaded. [108277,29,14:46:07.494,INF] The node's configuration was successfully reloaded ``` ::: tip If there are any errors during loading certificates, the new configuration changes will not take effect. ::: The server logs the following error when the node certificate is not available at the path specified in the EventStore configuration file. ``` [108277,30,14:49:47.481,INF] Reloading the node's configuration since a request has been received on /admin/reloadconfig. [108277,29,14:49:47.488,INF] Loading the node's certificate(s) from file: "/home/ubuntu/links/node.crt" [108277,29,14:49:47.489,ERR] An error has occurred while reloading the configuration System.IO.FileNotFoundException: Could not find file '/home/ubuntu/links/node.key'. ``` ### Step 4: Update the other nodes Now update the certificates on the other nodes. ### Step 5: Monitor the cluster The connection between nodes in EventStoreDB reset every 10 minutes so the new configuration won’t take immediate effect. Use this time to update the certificates on all of the nodes. It is advisable to monitor the cluster after this timeframe to make sure everything is working as expected. The EventStoreDB will log certificate errors if the certificates are not reloaded on all of the nodes before the connection resets. ``` [108277,29,14:59:47.489,ERR] eventstoredb-node : A certificate chain could not be built to a trusted root authority. [108277,29,14:59:47.489,ERR] Client certificate validation error: "The certificate (CN=eventstoredb-node) provided by the client failed validation with the following error(s): RemoteCertificateChainErrors (PartialChain)" ``` The above error indicates that there is some issue with the certificate because the reload process is not yet completed on all the nodes. The error can be safely ignored during the duration of the reloading certificate process, but the affected nodes will not be able to communicate with each other until the certificates have been updated on all the nodes. ::: warning It can take up to 10 minutes for certificate changes to take effect. Certificates on all nodes should be updated within this window to avoid connection issues. ::: --- --- url: 'https://docs.kurrent.io/server/v22.10/persistent-subscriptions.md' --- # Persistent subscriptions ## Persistent subscription A common operation is to subscribe to a stream and receive notifications for changes. As new events arrive, you continue following them. You can only subscribe to one stream or the `$all` stream. You can use server-side projections for linking events to new aggregated streams. System projections create pre-defined streams that aggregate events by type or by category and are available out-of-the box. Learn more about system and user-defined projections [here](projections.md). Persistent subscriptions run on the Leader node and are not dropped when the connection is closed. Moreover, this subscription type supports the "[competing consumers](https://www.enterpriseintegrationpatterns.com/patterns/messaging/CompetingConsumers.html)" messaging pattern and are useful when you need to distribute messages to many workers. EventStoreDB saves the subscription state server-side and allows for at-least-once delivery guarantees across multiple consumers on the same stream. It is possible to have many groups of consumers compete on the same stream, with each group getting an at-least-once guarantee. ::: tip The Administration UI includes a *Persistent Subscriptions* section where a user can create, update, delete and view subscriptions and their statuses. However, persistent subscriptions to the $all stream have to be created through a gRPC client. ::: ## Concepts Persistent subscriptions serve the same purpose as catch-up or volatile subscriptions, but in a different way. All subscriptions aim to deliver events in real-time to connected subscribers. But, unlike other subscription types, persistent subscriptions are maintained by the server. In a way, catch-up and persistent subscriptions are similar. Both have a last known position from where the subscription starts getting events. However, catch-up subscriptions must take care about keeping the last known position on the subscriber side and persistent subscriptions keep the position on the server. Since it is the server who decides from where the subscription should start receiving events and knows where events are delivered, subscribers that use a persistent subscription can be load-balanced and process events in parallel. In contrast, catch-up subscriptions, which are client-driven, always receive and process events sequentially and can only be load-balanced on the client side. Therefore, persistent subscriptions allow using the competing consumers pattern that is common in the world of message brokers. In order for the server to load-balance subscribers, it uses the concept of consumer groups. All clients that belong to a single consumer group will get a portion of events and that's how load balancing works inside a group. It is possible to create multiple consumer groups for the same stream and they will be completely independent of each other, receiving and processing events at their own pace and having their own last known position handled by the server. ![Consumer groups](images/consumer-groups.jpg) ::: warning Just as in the world of message brokers, processing events in a group of consumers running in parallel processes will most likely get events out of order within a certain window. For example, if a consumer group has ten consumers, ten messages will be distributed among the available consumers, based on the [strategy](#consumer-strategies) of the group. Even though some strategies make an attempt to consistently deliver ordered events to a single consumer, it's done on the best effort basis and there is no guarantee of events coming in order with any strategy. ::: ## Acknowledging messages Clients must acknowledge (or not acknowledge) messages as they are handled. If messages aren't acknowledged before they time out on the server, then the server will retry the messages. If a message has been retried more than the `maxRetryCount` setting for the persistent subscription, then the message will be parked and processing will continue. ## Parked messages Messages that have been retried too will often be parked in the persistent subscription's parked message stream. This stream is named `$persistentsubscription-{groupname}::{streamname}-parked`. You can easily see the number of parked events in the persistent subscription statistics or browse the parked messages in the admin UI. If you want to retry the parked messages, you can `Replay` the parked messages for that subscription. This will push the parked messages to subscribers before any new events on the subscription. You can also specify the number of parked messages to replay over the HTTP endpoint. This can be done by providing the `stopAt` parameter when requesting to replay messages through the HTTP url. For example: ```bash:no-line-numbers curl -i -X POST -d {} https://localhost:2113/subscriptions/{stream}/{groupnanme}/replayParked?stopAt={numberofevents} -u "admin:changeit" ``` If you don't want to replay any of the parked messages for a subscription and want to clear them out, you can do this by deleting the parked stream like a normal stream. ## Checkpointing Once a persistent subscription has handled enough events, it will write a checkpoint. If the subscription is restarted, for example due to a Leader change, then the persistent subscription will continue processing from the last checkpoint. This means that some events my be received multiple times by consumers. If a persistent subscription has a filter, then the persistent subscription will checkpoint when enough events are either handled or skipped by the filter. ::: note Persistent Subscriptions won't write a new checkpoint if there's one already in the process of being written. This means that even if you configure the subscription with a max checkpoint count of 1, it's not guaranteed to write a checkpoint after every event. ::: ## Consumer strategies When creating a persistent subscription, you can choose between a number of consumer strategies. These strategies determine how the server pushes events to subscribed clients. ### RoundRobin (default) Distributes events to all clients evenly. If the client `bufferSize` is reached, the client is ignored until events are acknowledged/not acknowledged. This strategy provides equal load balancing between all consumers in the group. ### DispatchToSingle Distributes events to a single client until the `bufferSize` is reached. After that, the next client is selected in a round-robin style, and the process is repeated. This option can be seen as a fall-back scenario for high availability, when a single consumer processes all the events until it reaches its maximum capacity. When that happens, another consumer takes the load to free up the main consumer resources. ### Pinned For use with an indexing projection such as the [system](projections.md#by-category) `$by_category` projection. EventStoreDB inspects the event for its source stream id, hashing the id to one of 1024 buckets assigned to individual clients. When a client disconnects its buckets are assigned to other clients. When a client connects, it is assigned some existing buckets. This naively attempts to maintain a balanced workload. The main aim of this strategy is to decrease the likelihood of concurrency and ordering issues while maintaining load balancing. This is not a guarantee, and you should handle the usual ordering and concurrency issues. ::: warning This strategy behaves differently depending on whether `ResolveLinkTos` is enabled. If you want to use this strategy with an indexing projection such as `$by_category` then you should have `ResolveLinkTos` enabled. ::: ### PinnedByCorrelation This is similar to the `Pinned` strategy, but instead of using the source stream id to bucket the messages, it distributes the events based on the event's correlationId. ## Considerations Persistent subscriptions are a powerful tool, but they are not always appropriate for every situation. Here are some things to consider before deciding whether you should use them: ### Persistent subscriptions run on leader Persistent subscriptions only run on the Leader node. This means that more pressure will be put on the Leader node, and there is no option to balance the load to a follower like with a Catch-up subscription. It also means that the subscriptions will reload from the last checkpoint whenever the Leader changes. ### Ordering is not guaranteed Ordering is not guaranteed with persistent subscriptions due to the possibility of messages being retried, or consumers handling events before others. While some strategies do attempt to mitigate this, it is still on a best-effort basis and messages may still arrive to consumers out of order. If you need an ordering guarantee then you should use a Catch-up subscription instead and handle the checkpointing in your client code. --- --- url: 'https://docs.kurrent.io/server/v22.10/projections.md' --- # Projections ## Introduction Projections is an EventStoreDB subsystem that lets you append new events or link existing events to streams in a reactive manner. Projections are good at solving one specific query type, a category known as 'temporal correlation queries'. This query type is common in business systems and few can execute these queries well. ::: tip Projections require the event body to be in JSON. ::: ### Business case examples For example. You are looking for how many Twitter users said "happy" within 5 minutes of the word "foo coffee shop" and within 2 minutes of saying "london". This is the type of query that projections can solve. Let's try a more complex business problem. As a medical research doctor you want to find people diagnosed with pancreatic cancer within the last year. During their treatment a patient should not have had any proxies for a heart condition such as taking aspirin every morning. Within three weeks of their diagnosis they should have been put on treatment X. Within one month after starting the treatment they should have failed with a lab result that looks like L1. Within another six weeks they should have been put on treatment Y, and within four weeks failed that treatment with a lab result that looks like L2. You can use projections in nearly all examples of near real-time complex event processing. There are a large number of problems that fit into this category from monitoring of temperature sensors, to reacting to changes in the stock market. It's important to remember the types of problems that projections help to solve. Many problems are not a good fit for projections and are better served by hosting another read model populated by a catchup subscription. ### Continuous querying Projections support the concept of continuous queries. When running a projection you can choose whether the query should run and give you all results present, or whether the query should continue running into the future finding new results as they happen and updating its result set. In the medical example above the doctor could leave the query running to be notified of any new patients that meet the criteria. The output of all queries is a stream, you can listen to this stream like any other stream. ### Types of projections There are two types of projections in EventStoreDB: * [Built in (system) projections](#system-projections) * [User-defined JavaScript projections](#user-defined-projections) which you create via the API or the admin UI ### Performance impact Keep in mind that all projections emit events as a reaction to events that they process. We call this effect *write amplification* because emitting new events or link events creates additional load on the server IO. Some system projections emit link events to their streams for each event appended to the database. These projections are By Category, By Event Type and By Correlation Id. If all those three projections are enabled and started, adding one event to the database will, in fact, produce three additional events and, therefore, quadruples the number of write operations. System projections `$streams` and `$stream-by-category` produce new events too, either per each new stream or per new stream category. If your system has a lot of small streams, the `$streams` system projection would also amplify writes significantly. Custom projections create the most significant write amplification since they produce new events or link events, which in turn get processed by system projections. Projections only run on a leader node of the cluster due to consistency concerns. It creates more CPU and IO load on the leader node compared to follower nodes. ### Limitations Streams where projections emit events cannot be used to append events from applications. When this happens, the projection will detect events not produced by the projection itself and it will break. The reason projections exclusively own their streams is that otherwise they would lose all predictability. The projection would no longer have any idea what should be in that stream. For example, when a projection starts up from a checkpoint, it first goes through all the events after that checkpoint and checks them against the emitted stream. By doing this, the projection can understand if it up to the last event and can continue from where it left off. On top of that, the projection can verify that everything is in order, no events missing, etc. If anyone can append to the emitted streams, then the projection would have no idea where it got to last in terms of processing. Therefore, it can no longer trust that the projection itself emitted that event or if something else did. ## System projections EventStoreDB ships with five built in projections: * [By Category](#by-category) (`$by_category`) * [By Event Type](#by-event-type) (`$by_event_type`) * [By Correlation ID](#by-correlation-id) (`$by_correlation_id`) * [Stream by Category](#stream-by-category) (`$stream_by_category`) * [Streams](#streams-projection) (`$streams`) ### Enabling system projections When you start EventStoreDB from a fresh database, these projections are present but disabled and querying their statuses returns `Stopped`. You can enable a projection by issuing a request which switches the status of the projection from `Stopped` to `Running`. ```bash:no-line-numbers curl -i -X POST "http://{event-store-ip}:{ext-http-port}/projection/{projection-name}/command/enable" -H "accept:application/json" -H "Content-Length:0" -u admin:changeit ``` ### By category The `$by_category` (*http://127.0.0.1:2113/projection/$by\_category*) projection links existing events from streams to a new stream with a `$ce-` prefix (a category) by splitting a stream `id` by a configurable separator. ```text:no-line-numbers first - ``` You can configure the separator, as well as where to split the stream `id`. You can edit the projection and provide your own values if the defaults don't fit your particular scenario. The first parameter specifies how the separator is used, and the possible values for that parameter is `first` or `last`. The second parameter is the separator, and can be any character. For example, if the body of the projection is `first` and `-`, for a stream id of `account-9E763770-0A8D-456D-AF23-410ADBC88249`, the stream name the projection creates is `$ce-account`. If the body of the projection is `last` and `-`, for a stream id of `shopping-cart-1`, the stream name the projection creates is `$ce-shopping-cart`. ::: warning You can change the projection setting at any time, so it can be quite dangerous. Consider all possible event consumers of the category stream that expect it to be in the format that is already there. Changing the setting might break all of them. ::: The use case of this project is subscribing to all events within a category. ### By event type The `$by_event_type` (*http://127.0.0.1:2113/projection/$by\_event\_type*) projection links existing events from streams to a new stream with a stream id in the format `$et-{event-type}`. For example, if you append an event with the `EventType` field set to `PaymentProcessed`, no matter in what stream you appended this event, you get a link event in the `$et-PaymentProcessed` stream. You cannot configure this projection. ### By correlation ID The `$by_correlation_id` (*http://127.0.0.1:2113/projection/$by\_correlation\_id*) projection links existing events from projections to a new stream with a stream id in the format `$bc-`. The projection takes one parameter, a JSON string as a projection source: ```json:no-line-numbers { "correlationIdProperty": "$myCorrelationId" } ``` ### Stream by category The `$stream_by_category` (*http://127.0.0.1:2113/projection/$by\_category*) projection links existing events from streams to a new stream with a `$category` prefix by splitting a stream `id` by a configurable separator. ```text:no-line-numbers first - ``` By default, the `$stream_by_category` projection links existing events from a stream id with a name such as `account-1` to a stream called `$category-account`. You can configure the separator as well as where to split the stream `id`. You can edit the projection and provide your own values if the defaults don't fit your particular scenario. The first parameter specifies how the separator is used, and the possible values for that parameter is `first` or `last`. The second parameter is the separator, and can be any character. For example, if the body of the projection is `first` and `-`, for a stream id of `account-1`, the stream name the projection creates is `$category-account`, and the `account-1` stream is linked to it. Future streams prefixed with `account-` are likewise linked to the newly created `$category-account` stream. If the body of the projection is last and `-`, for a stream id of `shopping-cart-1`, the stream name the projection creates is `$category-shopping-cart`, and the `shopping-cart-1` stream is linked to it. Future streams whose left-side split by the *last* `-` is `shopping-cart`, are likewise linked to the newly created `$category-shopping-cart` stream. The use case of this projection is subscribing to all stream instances of a category. ### Streams projection The `$streams` (*http://127.0.0.1:2113/projection/$streams*) projection links existing events from streams to a stream named `$streams` You cannot configure this projection. ## User defined projections ::: warning WIP This section is work in progress. ::: You create user defined projections in JavaScript. For example, the `my_demo_projection_result` projection below counts the number of `myEventType` events from the `account-1` stream. It then uses the `transformBy` function to change the final state: ```javascript options({ resultStreamName: "my_demo_projection_result", $includeLinks: false, reorderEvents: false, processingLag: 0 }) fromStream('account-1') .when({ $init: function () { return { count: 0 } }, myEventType: function (state, event) { state.count += 1; } }) .transformBy(function (state) { return {Total: state.count} }) .outputState() ``` ### User defined projections API #### Options | Name | Description | Notes | |:-------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------| | `resultStreamName` | Overrides the default resulting stream name for the `outputState()` transformation, which is `$projections-{projection-name}-result`. | | | `$includeLinks` | Configures the projection to include/exclude link to events. | Default: `false` | | `processingLag` | When `reorderEvents` is enabled, this value is used to compare the total milliseconds between the first and last events in the buffer and if the value is equal or greater, the events in the buffer are processed. The buffer is an ordered list of events. | Default: `500ms`. Only valid for `fromStreams()` selector | | `reorderEvents` | Process events by storing a buffer of events ordered by their prepare position | Default: `false`. Only valid for `fromStreams()` selector | #### Selectors | Selector | Description | Notes | |:--------------------------------------------------------------------------------------------------------|:-------------------------------------------------|:------| | `fromAll()` | Selects events from the `$all` stream. | \*\* | | Provides\*\* `partitionBy``when``foreachStream``outputState` | | | | `fromCategory({category})` | Selects events from the `$ce-{category}` stream. | \*\* | | Provides\*\* `partitionBy``when``foreachStream``outputState` | | | | `fromStream({streamId})` | Selects events from the `streamId` stream. | \*\* | | Provides\*\* `partitionBy``when``outputState` | | | | `fromStreams(streams[])` | Selects events from the streams supplied. | \*\* | | Provides\*\*`partitionBy``when``outputState` | | | #### Filters and transformations | Filter/Partition | Description | Notes | |:-------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------|:------| | `when(handlers)` | Allows only the given events of a particular to pass through the projection. | \*\* | | Provides\*\* `$defines_state_transform` `transformBy``filterBy``outputTo``outputState` | | | | `foreachStream()` | Partitions the state for each of the streams provided. | \*\* | | Provides\*\* `when` | | | | `outputState()` | If the projection maintains state, setting this option produces a stream called `$projections-{projection-name}-result` with the state as the event body. | \*\* | | Provides\*\* `transformBy``filterBy``outputTo` | | | | `partitionBy(function(event))` | Partitions a projection by the partition returned from the handler. | \*\* | | Provides\*\* `when` | | | | `transformBy(function(state))` | Provides the ability to transform the state of a projection by the provided handler. | \*\* | | Provides\*\* `transformBy``filterBy``outputState``outputTo` | | | | `filterBy(function(state))` | Causes projection results to be `null` for any `state` that returns a `false` value from the given predicate. | \*\* | | Provides\*\* `transformBy``filterBy``outputState``outputTo` | | | #### Handlers Each handler is provided with the current state of the projection as well as the event that triggered the handler. The event provided through the handler contains the following properties. * `isJson`: true/false * `data`: {} * `body`: {} * `bodyRaw`: string * `sequenceNumber`: integer * `metadataRaw`: {} * `linkMetadataRaw`: string * `partition`: string * `eventType`: string * `streamId`: string | Handler | Description | Notes | |:---------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------| | `{event-type}` | When using `fromAll()` and 2 or more event type handlers are specified and the `$by_event_type` projection is enabled and running, the projection starts as a `fromStreams($et-event-type-foo, $et-event-type-bar)` until the projection has caught up and moves to reading from the transaction log (i.e. from `$all`). | | | `$init` | Provide the initialization for a projection. | Commonly used to setup the initial state for a projection. | | `$initShared` | Provide the initialization for a projection where the projection is possibly partitioned. | | | `$any` | Event type pattern match that match any event type. | Commonly used when the user is interested in any event type from the selector. | | `$deleted` | Called upon the deletion of a stream. | Can only be used with `foreachStream` | #### Functions | Handler | Description | |:-------------------------------------------------|:------------------------------------------------| | `emit(streamId, eventType, eventBody, metadata)` | Appends an event to the designated stream | | `linkTo(streamId, event, metadata)` | Writes a link to event to the designated stream | ## Configuring projections By changing these settings, you can lessen the amount of pressure projections put on an EventStoreDB node or improve projection performance. You can change these settings on a case-by-case basis, and monitor potential improvements. ::: warning You can only change the configuration of a stopped projection. ::: You change the configuration of a projection by setting the relevant key and value in a request, or when you create a projection with the web admin interface. ![Web admin interface projections configuration screen](images/wai-projection-config.jpg) ### Emit options These options control how projections append events. In busy systems, projections can put a lot of extra pressure on the master node. This is especially true for EventStoreDB servers that also have persistent subscriptions running, which only the master node can process. If you see a lot of commit timeouts and slow writes from your projections and other clients, then start with these settings. #### Emit enabled The `emit` boolean setting determines whether a projection can emit events and any projection that calls `emit()` or `linkTo()` requires it. If this option is not set and a projection attempts to emit events, you see an error message like the following: ```bash:no-line-numbers 'emit' is not allowed by the projection/configuration/mode ``` EventStoreDB disables this setting by default, and is usually set when you create the projection and if you need the projection to emit events. #### Track emitted streams The `trackemittedstreams` boolean setting enables tracking of a projection's emitted streams. It only has an affect if the projection has `EmitEnabled` enabled. Tracking emitted streams enables you to delete a projection and all the streams that it has created. You should only the setting if you intend to delete a projection and create new ones that project to the same stream. ::: warning By default, EventStoreDB disables the `trackemittedstreams` setting for projections. When enabled, an event appended records the stream name (in `$projections-{projection_name}-emittedstreams`) of each event emitted by the projection. This means that write amplification is a possibility, as each event that the projection emits appends a separate event. As such, this option is not recommended for projections that emit a lot of events, and you should enable only where necessary. ::: ::: tip Between EventStoreDB versions 3.8.0 and 4.0.2, this option was enabled by default when a projection was created through the UI. If you have any projections created during this time frame, it's worth checking whether this option is enabled. ::: #### Max allowed writes in flight The `MaxAllowedWritesInFlight` setting sets the maximum number of writes to allow for a projection. Because a projection can write to multiple different streams, it's possible for the projection to send multiple writes at the same time. This option sets the number of concurrent writes that a projection can perform. By default, projections try to perform writes as quickly as they come. This can add a lot of pressure to a node, especially for projections that emit to different streams. If you see your projections causing frequent commit timeouts or slow reads, you can try lowering this value to see if there is any improvement. ::: tip Lower values may cause the projection to slow down as the number of writes are throttled, but the trade-off for this is cleaner logs and fewer commit timeouts. ::: By default, this is unbounded, allowing a projection to write as fast as it can. #### Max write batch length The `MaxWriteBatchLength` setting sets the maximum number of events the projection can write in a batch at a time. **Default:** `500` (events). ### Checkpoint options Checkpoints store how far along a projection is in the streams it is processing from. There is a performance overhead with writing a checkpoint, as it does more than append an event, and writing them too often can slow projections down. We recommend you try other methods of improving projections before changing these values, as checkpoints are an important part of running projections. #### Checkpoint after Ms The `CheckpointAfterMs` setting prevents a new checkpoint from being written within a certain time frame from the previous one. The setting is to keep a projection from writing too many checkpoints too quickly, something that can happen in a busy system. The default setting is 0 seconds, which means there is no limit to how quickly checkpoints can be written. #### Checkpoint handled threshold The `CheckpointHandledThreshold` setting controls the number of events that a projection can handle before attempting to write a checkpoint. An event is considered handled if it actually passed through the projection's filter. If the projection is set to checkpoint every 4,000 events, but it only reads from the `foo` stream, the projection only checkpoints every 4,000 `foo` events. **Default:** `4000` (events). #### Checkpoint unhandled bytes threshold The `CheckpointUnhandledBytesThreshold` setting specifies the number of bytes a projection can process before attempting to write a checkpoint. Unhandled bytes are the events that are not processed by the projection itself. For example, if the projection reads from the `foo` stream, but writes from the `bar` stream comes through, a checkpoint is written after this number of bytes have been processed. This prevents the projection from having to read through a potentially large number of unrelated events again because none of them passed its filter. **Default:** `10` (MiB). ### Processing options #### Pending events threshold The `PendingEventsThreshold` setting determines the number of events that can be pending before the projection is paused. Pausing the projection stops the projection from reading, allowing it to finish with the current events that are waiting to be processed. Once the pending queue has drained to half the threshold, the projection starts reading again. **Default:** `5000` (events). ## Debugging [User projections you create in JavaScript](#user-defined-projections) have a bonus that debugging is easier via any browser that ships with debugging capabilities. The screenshots in this document show the use of Chrome, but we have tested debugging with all major browsers including Firefox, Edge and Safari. ### Logging from within a projection For debugging purposes, projections includes a log method which, when called, sends messages to the configured EventStoreDB logger (the default is `NLog`, to a file, and `stdout`). You might find printing out the structure of the event body for inspection useful. For example: ```javascript fromStream('$stats-127.0.0.1:2113') .when({ $any: function (s, e) { log(JSON.stringify(e)); } }) ``` #### Creating a sample projection for debugging purposes Filename: *stats-counter.json* Contents: ```javascript fromStream('$stats-127.0.0.1:2113') .when({ $init: function () { return { count: 0 } }, $any: function (s, e) { s.count += 1; } }) ``` You create the projection by making a call to the API and providing it with the definition of the projection. ```bash:no-line-numbers curl -i -d@stats-counter.json \ http://localhost:2113/projections/continuous?name=stats-counter%26type=js%26enabled=true%26emit=true%26trackemittedstreams=true \ -u admin:changeit ``` ### Debugging your first projection Once the projection is running, open your browser and enable the developer tools. Once you have the developer tools open, visit your projection URL and you should see a button labelled *Debug*. ![Projections Debugging Part 1](images/projections_debugging_part_1.png) After clicking the projection "Debug" button, you see the debugging interface with the definition of the projection and information about the events the projection is processing on the right-hand side. At the top there are a couple of buttons to take note of, specifically the *Run Step* and *Update* buttons. You use *Run Step* to step through the event waiting in the queue, placing you in projection debugging mode. The *Update* button provides you with a way to update the projection definition without having to go back to the projection itself and leave the context of the debugger. ![Projections Debugging Part 2](images/projections_debugging_part_2.png) If the *Run Step* button is not greyed out and you click it, the browser has hit a breakpoint. ![Projections Debugging Part 3](images/projections_debugging_part_3.png) You are now able to step through the projection, the important method to step into is the `handler(state, eventEnvelope)` method. ## Projections settings Settings in this section concern projections that are running on the server. ::: warning Server-side projections impact the performance of the EventStoreDB server. For example, some standard [system projections](#system-projections) like Category or Event Type projections produce new (link) events that are stored in the database in addition to the original event. This effectively doubles or triples the number of events appended and therefore creates pressure on the IO of the server node. We often call this effect "write amplification". ::: ### Projection runtime An Interpreted runtime was introduced in 21.6.0 to replace the existing V8 runtime. The `ProjectionRuntime` option can be used to select which runtime the Projection Subsystem should use. We only recommend changing this setting if you observe a difference in behaviour between running an existing projection on the Legacy runtime versus the Interpreted runtime. | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--projection-runtime` | | YAML | `ProjectionRuntime` | | Environment variable | `EVENTSTORE_PROJECTION_RUNTIME` | **Default**: `Interpreted`, use the new Interpreted runtime by default. Accepted values are `Interpreted` and `Legacy`. ### Run projections The `RunProjections` option tells the server if you want to run all projections, only system projections or no projections at all. Keep in mind that the `StartStandardProjections` setting has no effect on custom projections. The option accepts three values: `None`, `System` and `All`. When the option value is set to `None`, the projections subsystem of EventStoreDB will be completely disabled and the Projections menu in the Admin UI will be disabled. By using the `System` value for this option, you can instruct the server to enable system projections when the server starts. However, system projections will only start if the `StartStandardProjections` option is set to `true`. When the `RunProjections` option value is `System` (or `All`) but the `StartStandardProjections` option value is `false`, system projections will be enabled but not start. You can start them later manually via the Admin UI or via an API call. Finally, you can set `RunProjections` to `All` and it will enable both system and custom projections. | Format | Syntax | |:---------------------|:-----------------------------| | Command line | `--run-projections` | | YAML | `RunProjections` | | Environment variable | `EVENTSTORE_RUN_PROJECTIONS` | **Default**: `None`, all projections are disabled by default. Accepted values are `None`, `System` and `All`. ### Projection threads Projection threads are used to make calls in to the V8 JavaScript engine, and coordinate dispatching operations back into the main worker threads of the database. While they carry out none of the operations listed directly, they are indirectly involved in all of them. The primary reason for increasing the number of projection threads is projections which perform a large amount of CPU-bound processing. Projections are always eventually consistent - if there is a mismatch between egress from the database log and processing speed of projections, the window across which the latest events have not been processed promptly may increase. Too many projection threads can end up with increased context switching and memory use, since a V8 engine is created per thread. There are three primary influences over projections lagging: * Large number of writes, outpacing the ability of the engine to process them in a timely fashion. * Projections which perform a lot of CPU-bound work (heavy calculations). * Projections which result in a high system write amplification factor, especially with latent disks. Use the `ProjectionThreads` option to adjust the number of threads dedicated to projections. | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--projection-threads` | | YAML | `ProjectionThreads` | | Environment variable | `EVENTSTORE_PROJECTION_THREADS` | **Default**: `3` ### Fault out of order projections It is possible that in some cases a projection would get an unexpected event version. It won't get an event that precedes the last processed event, such a situation is very unlikely. But, it might get the next event that doesn't satisfy the `N+1` condition for the event number. The projection expects to get an event number `5` after processing the event number `4`, but eventually it might get an event number `7` because events `5` and `6` got deleted and scavenged. The projections engine can keep track of the latest processed event for each projection. It allows projections to guarantee ordered handling of events. By default, the projections engine ignore ordering failures like described above. You can force out of order projections to fail by setting the `FailOutOfOrderProjections` to `true`. | Format | Syntax | |:---------------------|:--------------------------------------------| | Command line | `--fault-out-of-order-projections` | | YAML | `FaultOutOfOrderProjections` | | Environment variable | `EVENTSTORE_FAULT_OUT_OF_ORDER_PROJECTIONS` | **Default**: `false` --- --- url: 'https://docs.kurrent.io/server/v22.10/release-notes.md' --- # Release notes This page contains the release notes for EventStoreDB 22.10 and 22.6 ## [22.10.5](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.5) 20 February 2024 ### Addressed [CVE-2024-26133](https://github.com/EventStore/EventStore/security/advisories/GHSA-6r53-v8hj-x684) Fixed a potential password leak in the EventStoreDB Projections Subsystem. Only database instances that use custom projections are affected by this vulnerability. User passwords may become accessible to those who have access to the chunk files on disk, and users who have read access to system streams. Only users in the `$admins` group can access system streams by default. Recommended action * Upgrade EventStoreDB: Kurrent Cloud customers follow the instructions in the [cloud upgrade guide](https://docs.kurrent.io/cloud/dedicated/ops/#upgrading-kurrentdb-version). Otherwise, follow the instructions in the [standard upgrade guide](./upgrade-guide.md). * Reset the passwords for current and previous members of `$admins` and `$ops` groups. * If a password was reused in any other system, reset it in those systems to a unique password to follow best practices. ## [22.10.4](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.4) 11 December 2023 ### Important fix: Prevent unreplicated data from being visible before truncation This addresses an issue where unreplicated data could be exposed before truncation in certain edge cases. It was possible for reads and subscriptions to receive these events which would then be truncated. ### Additional fixes * Updating Persistent Subscriptions no longer clears the filter * Filtered subscriptions now checkpoint on the correct intervals ## [22.10.3](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.3) 14 September 2023 ### In this release #### Fix live reloading of certificates There was a regression in version 22.10.0 that prevented certificates from being reloaded through the `/admin/reloadconfig` endpoint or through a SIGHUP signal. This means that it was not possible to perform a rolling certificate upgrade across a cluster on previous versions of 22.10. Now that this is fixed you can perform a rolling certificate upgrade by following the [documented steps](./operations.md#certificate-update-upon-expiry) #### Support bloom filters for very large index files This patch fixes two issues that could cause the server to exit when trying to create or read very large bloom filters. The first issue occurred when EventStoreDB tried to create a bloom filter for a PTable larger than 400gb (around 16 billion events). This caused EventStoreDB to create a bloom filter larger than the maximum allowed size, and caused the server to exit. The size of bloom filters is now clamped to the maximum allowed size. The second issue is Linux-specific. Bloom filters larger than around 2gb could not be opened due to the way they were being read off disk by EventStoreDB. This caused the bloom filters to not be used by the server. This has now been fixed. ## [22.10.2](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.2) 15 May 2023 ### In this release #### "Could not get TimeStamp range for chunk" error during Scavenge It was possible for scavenging on EventStoreDB 22.10.0 and 22.10.1 to fail with the error message "Could not get TimeStamp range for chunk" if a scavenge had previously been run on an older version of EventStoreDB. This happened when the previous scavenge had scavenged away all of the events in a chunk (leaving it empty) but hadn't scavenged the index. This case is now correctly handled in 22.10.2 #### Properly report Create/Delete errors for persistent subscriptions Previously, the gRPC client was not getting notified when the creation or deleting of a persistent subscription failed, resulting in these failed requests just timing out. This version will now correctly report the fact that the request failed back to the client. ## [22.10.1](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.1) 13 February 2023 ### In this release #### Database checkpoints no longer become inconsistent when running out of disk space Fixed an issue with checkpoints becoming inconsistent when the database ran out of disk space, causing the EventStoreDB to fail to start with the following error: ``` Prefix/suffix length inconsistency: prefix length(196) != suffix length (0).\nActual pre-position: 45670073. Something is seriously wrong in chunk #199-199 (chunk-000199.000000). ``` Databases already experiencing the above error can be recovered by copying the `chaser.chk` over the `truncate.chk` like you would when restoring a backup. This will truncate the database back to the most recent valid data. #### Slow persistent subscription consumers no longer slow down other subscribers Ensure persistent subscriptions messages don't continue on the main queue. This prevents continuations which could take a long time to process from happening on the main queue. #### Additional fixes * Checkpoints in filtered $all subscriptions now continue to update after the subscription is live * Cancel reads already in the reader queues when the gRPC call is cancelled * Added more information to `SLOW QUEUE MSG` log entries for reads and writes ## [22.10.0](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.10.0) 14 November 2022 ### In this release #### Improved scavenge process 22.10.0 includes the new scavenge process which runs approximately 4-40x faster depending on the circumstances, and uses much less memory than scavenges in the previous versions. The new scavenge has a few differences to the previous version: ##### Nodes can scavenge to the same point The new scavenge process saves a scavenge point in the log containing the location that the scavenge will run up to and the timestamp used for checking `maxAge` during the scavenge. This scavenge point is shared across all of the nodes in the cluster. Any time a scavenge is started on a node, it will first check if there are any existing scavenge points from another node. If there is, it will scavenge up to the latest scavenge point. Otherwise it will create a new one. In this way, each node in the cluster will have the same data after a scavenge has been run on all of the nodes. ##### Data is removed from the current active chunk Previously, the most recently written chunk in the database would not be scavenged because the chunk was not marked as complete. The new scavenge process completes the current chunk when it writes a scavenge point. This means that all of your data is eligible to be scavenged and you no longer have to wait until enough data has been written to complete a chunk to scavenge recently-written data. ##### New scavenge directory Scavenge accumulates data that it references throughout the current scavenge and subsequent scavenges on the same node. This data is stored in the 'scavenge' directory inside the index directory. We recommend the scavenge directory is included in backups to save time having to rebuild it next time a scavenge is run after a restore. As before, ensure that scavenge is not running while a backup is taken (this is not necessary when backing up with disk snapshots). #### Dev certificates We have introduced a new dev mode to make running a local dev instance of EventStoreDB easier. You can now run a secure single EventStoreDB node on localhost with the command: ``` ./EventStore.ClusterNode.exe -–dev ``` This does a few things. It generates a self-signed dev certificate for the node if one isn’t already present, enables AtomPub over HTTP, enables system projections, and starts the secure EventStoreDB instance. You can then browse to the admin UI or connect a gRPC client with very little hassle. The benefit to this over insecure mode is that there are no extra configuration changes needed for your clients, and user management is enabled in secure mode. If you wish to remove the dev certificates created by EventStoreDB, you can run the command: ``` ./EventStore.ClusterNode.exe --remove-dev-certs ``` #### Dynamic stream info cache The Stream Info Cache is the lookup that contains key information about any stream that has recently been read or written to. Having entries in this cache significantly improves write and read performance to cached streams on larger databases. Previously, this cache size was set to a static value based on the amount of free memory at the time of EventStoreDB starting up. This could result in an unnecessarily large Stream Info Cache, or in too much memory pressure on the node. As of EventStoreDB 22.10.0, the Stream Info Cache is dynamic by default. This means that it gets resized periodically or under certain conditions based on how much free memory is available. The dynamic cache deals with the following scenarios more efficiently than the statically-sized one: * Growth of the database with time * Heavy operations such as index merges * Growth in the number of clients connected * Spike in memory usage of other processes on the system The StreamInfoCache can still be made static by specifying `--stream-info-cache-capacity` New statistics for caches are also available under `es.stats.cache.*` in the stats log file. #### Better support for certificates when using DNS discovery EventStoreDB 21.10.5 added support for wildcard certificates when using DNS discovery in a cluster. This means that you don't have to include the IP address in the node's certificate when using \`DiscoverViaDns: true. #### Faster startup times for large databases When EventStoreDB upgraded to NET5.0, a performance regression in `Directory.EnumerateFiles` made a performance issue in the EventStoreDB code more obvious, which greatly affected startup and truncation times on large databases. We have reduced the computational complexity of the methods run at startup to lower the impact of the regression and improve startup times. This fix was introduced in EventStoreDB 21.10.2. ### Breaking changes #### Proto2 upgraded to Proto3 (TCP) The server now uses Proto3 for messages sent to the legacy TCP clients. This may impact community clients, especially when it comes to default or required fields as these are no longer supported in Proto3. #### EmitEnabled and TrackEmittedStreams for projections (gRPC) Since 22.6.0, the server supports clients setting EmitEnabled and TrackEmittedStreams when creating a projection. Essentially, the names of `EmitEnabled` and `TrackEmittedStreams` have been swapped in the proto because what the server used to refer to as `TrackEmittedStreams` is, in fact, the correct behaviour for `EmitEnabled`. Therefore clients that were relying on the old behaviour and just sending `TrackEmittedStreams=true` to enable emitting when creating a projection will continue to work after upgrading the server. However, the server will now throw when sent a create request with `TrackEmittedStreams=true`, but `EmitEnabled=false`. This is because a code change will be needed when a client library is upgraded, so throwing an error will make that code change more obvious. ## [22.6.0](https://github.com/kurrent-io/KurrentDB/releases/tag/oss-v22.6.0) 10 August 2022 ### In this release #### $all position is available when reading streams over gRPC The $all position has been added to the result of stream reads, subscriptions, and persistent subscriptions. This means that you can now read an event in a stream and find its corresponding position in the $all stream. In order to get this functionality, you may need to upgrade your gRPC client to the latest version. The one caveat is that this is not supported on events written through transactions in the legacy TCP client. For these events, the positions in the events will be the same as before. #### Better support for the Windows cert store EventStoreDB 22.6.0 introduces the ability to load the Trusted Root Certificate from the Windows certificate store rather than from a folder. To use this, provide `TrustedRootCertificateStoreName` and `TrustedRootCertificateStoreLocation` in the settings when configuring your EventStoreDB, and then provide either the `TrustedRootCertificateThumbprint` or `TrustedRootCertificateSubjectName` to specify which certificate to load. For example, using thumbprint: ``` TrustedRootCertificateStoreLocation: LocalMachine TrustedRootCertificateStoreName: Root TrustedRootCertificateThumbprint: {thumbprint} CertificateStoreLocation: LocalMachine CertificateStoreName: My CertificateThumbprint: {thumbprint} ``` Or using SubjectName (this will match any certificate that contains the subject name) : ``` TrustedRootCertificateStoreLocation: LocalMachine TrustedRootCertificateStoreName: Root TrustedRootCertificateSubjectName: EventStoreDB CA CertificateStoreLocation: LocalMachine CertificateStoreName: My CertificateSubjectName: eventstoredb-node ``` ### Breaking changes for community gRPC clients EmitEnabled and TrackEmittedStreams for projections The server now supports clients setting EmitEnabled and TrackEmittedStreams when creating a projection. Updates for our supported clients will be made after this release, but if you maintain your own client, then you will need to take note of the changes made here: https://github.com/EventStore/EventStore/pull/3384 https://github.com/EventStore/EventStore/pull/3412 Essentially, the names of `EmitEnabled` and `TrackEmittedStreams` have been swapped in the proto because what the server used to refer to as `TrackEmittedStreams` is, in fact, the correct behaviour for `EmitEnabled`. Therefore clients that were relying on the old behaviour and just sending `TrackEmittedStreams=true` to enable emitting when creating a projection will continue to work after upgrading the server. However, the server will now throw when sent a create request with `TrackEmittedStreams=true`, but `EmitEnabled=false`. This is because a code change will be needed when a client library is upgraded, so throwing an error will make that code change more obvious. --- --- url: 'https://docs.kurrent.io/server/v22.10/release-schedule.md' --- # Release schedule EventStoreDB has been renamed to KurrentDB, and has a new release schedule which you can find in [the latest documentation](https://docs.kurrent.io/server/latest/release-schedule/). The LTS versions of EventStoreDB will still be supported for two years from their release date. This means that the following versions of EventStoreDB are still within Kurrent's support window: * [EventStoreDB 24.10](https://docs.kurrent.io/server/v24.10/quick-start/) supported until October 2026. * [EventStoreDB 23.10](https://docs.kurrent.io/server/v23.10/quick-start/) supported until October 2025. --- --- url: 'https://docs.kurrent.io/server/v22.10/security.md' --- # Security ## Security For production use it is important to configure EventStoreDB security features to prevent unauthorised access to your data. Security features of EventStoreDB include: * [User management](#authentication) for allowing users with different roles to access the database * [Access Control Lists](#access-control-lists) to restrict access to specific event streams * Encryption in-flight using HTTPS and TLS ### Protocol security EventStoreDB supports gRPC and the proprietary TCP protocol for high-throughput real-time communication. It also has some HTTP endpoints for the management operations like scavenging, creating projections and so on. EventStoreDB also uses HTTP for the gossip seed endpoint, both internally for the cluster gossip, and internally for clients that connect to the cluster using discovery mode. All those protocols support encryption with TLS and SSL. Each protocol has its own security configuration, but you can only use one set of certificates for both TLS and HTTPS. The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](installation.md), which also has instructions on how to generate and install the certificates and configure EventStoreDB nodes to use them. ## Security options Below you can find more details about each of the available security options. ### Running without security Unlike previous versions, EventStoreDB v20+ is secure by default. It means that you have to supply valid certificates and configuration for the database node to work. We realise that many users want to try out the latest version with their existing applications, and also run a previous version of EventStoreDB without any security in their internal networks. For this to work, you can use the `Insecure` option: | Format | Syntax | |:---------------------|:----------------------| | Command line | `--insecure` | | YAML | `Insecure` | | Environment variable | `EVENTSTORE_INSECURE` | **Default**: `false` ::: warning When running with protocol security disabled, everything is sent unencrypted over the wire. In the previous version it included the server credentials. Sending username and password over the wire without encryption is not secure by definition, but it might give a false sense of security. In order to make things explicit, EventStoreDB v20 **does not use any authentication and authorisation** (including ACLs) when running insecure. ::: ### Certificates configuration In this section, you can find settings related to protocol security (HTTPS and TLS). #### Certificate common name SSL certificates can be created with a common name (CN), which is an arbitrary string. Usually is contains the DNS name for which the certificate is issued. When cluster nodes connect to each other, they need to ensure that they indeed talk to another node and not something that pretends to be a node. Therefore, EventStoreDB expects the connecting party to have a certificate with a pre-defined CN `eventstoredb-node`. When using the Event Store [certificate generator](#certificate-generation-tool), the CN is properly set by default. However, you might want to change the CN and in this case, you'd also need to tell EventStoreDB what value it should expect instead of the default one, using the setting below: | Format | Syntax | |:---------------------|:---------------------------------------------------| | Command line | `--certificate-reserved-node-common-name` | | YAML | `CertificateReservedNodeCommonName` | | Environment variable | `EVENTSTORE_CERTIFICATE_RESERVED_NODE_COMMON_NAME` | **Default**: `eventstoredb-node` ::: warning Server certificates **must** have the internal and external IP addresses or DNS names as subject alternative names. ::: #### Trusted root certificates When getting an incoming connection, the server needs to ensure if the certificate used for the connection can be trusted. For this to work, the server needs to know where trusted root certificates are located. EventStoreDB will not use the default trusted root certificates store location of the platform. So, even if you use a certificate signed by a publicly trusted CA, you'd need to explicitly tell the node to use the OS default root certificate store. For certificates signed by a private CA, you just provide the path to the CA certificate file (but not the filename). If you are running on Windows, you can also load the trusted root certificate from the Windows Certificate Store. The available options for configuring this are described [below](#certificate-store-windows). | Format | Syntax | |:---------------------|:--------------------------------------------| | Command line | `--trusted-root-certificates-paths` | | YAML | `TrustedRootCertificatesPath` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH` | **Default**: n/a #### Certificate file The `CertificateFile` setting needs to point to the certificate file, which will be used by the cluster node. | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--certificate-file` | | YAML | `CertificateFile` | | Environment variable | `EVENTSTORE_CERTIFICATE_FILE` | If the certificate file is protected by password, you'd need to set the `CertificatePassword` value accordingly, so the server can load the certificate. | Format | Syntax | |:---------------------|:----------------------------------| | Command line | `--certificate-password` | | YAML | `CertificatePassword` | | Environment variable | `EVENTSTORE_CERTIFICATE_PASSWORD` | If the certificate file doesn't contain the certificate private key, you need to tell the node where to find the key file using the `CertificatePrivateKeyFile` setting. | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--certificate-private-key-file` | | YAML | `CertificatePrivateKeyFile` | | Environment variable | `EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE` | ::: warning RSA private key EventStoreDB expects the private key to be in RSA format. Check the first line of the key file and ensure that it looks like this: ``` -----BEGIN RSA PRIVATE KEY----- ``` If you have non-RSA private key, you can use `openssl` to convert it: ```bash openssl rsa -in privkey.pem -out privkeyrsa.pem ``` ::: #### Certificate store (Windows) The certificate store location is the location of the Windows certificate store, for example `CurrentUser`. | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--certificate-store-location` | | YAML | `CertificateStoreLocation` | | Environment variable | `EVENTSTORE_CERTIFICATE_STORE_LOCATION` | The certificate store name is the name of the Windows certificate store, for example `My`. | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--certificate-store-name` | | YAML | `CertificateStoreName` | | Environment variable | `EVENTSTORE_CERTIFICATE_STORE_NAME` | You can load a certificate using either its thumbprint or its subject name. If using the thumbprint, the server expects to only find one certificate file matching that thumbprint in the cert store. | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--certificate-thumbprint` | | YAML | `CertificateThumbprint` | | Environment variable | `EVENTSTORE_CERTIFICATE_THUMBPRINT` | The subject name matches any certificate that contains the specified name. This means that multiple matching certificates could be found. To match any certificate made by the es-gencert-cli, you can set the subject name to `eventstoredb-node`. If multiple matching certificates are found, then the certificate with the latest expiry date will be selected. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--certificate-subject-name` | | YAML | `CertificateSubjectName` | | Environment variable | `EVENTSTORE_CERTIFICATE_SUBJECT_NAME` | When you are loading your node certificates from the Windows cert store, you are likely to want to load the trusted root certificate from the cert store as well. The options to configure this are similar to the ones for node certificates. The trusted root certificate store location is the location of the Windows certificate store in which the trusted root certificate is installed, for example `CurrentUser`. | Format | Syntax | |:---------------------|:-----------------------------------------------------| | Command line | `--trusted-root-certificate-store-location` | | YAML | `TrustedRootCertificateStoreLocation` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_STORE_LOCATION` | The trusted root certificate store name is the name of the Windows certificate store in which the trusted root certificate is installed, for example `Root`. | Format | Syntax | |:---------------------|:-------------------------------------------------| | Command line | `--trusted-root-certificate-store-name` | | YAML | `TrustedRootCertificateStoreName` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_STORE_NAME` | Trusted root certificates can also be loaded using either its thumbprint or its subject name. If using the thumbprint, the server expects to only find one trusted root certificate file matching that thumbprint in the cert store. | Format | Syntax | |:---------------------|:-------------------------------------------------| | Command line | `--trusted-root-certificate-thumbprint` | | YAML | `TrustedRootCertificateThumbprint` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_THUMBPRINT` | The subject name matches any certificate that contains the specified name. This means that multiple matching certificates could be found. To match any root certificate made through the es-gencert-cli, you can set the Subject Name to `EventStoreDB CA`. If multiple matching root certificates are found, then the root certificate with the latest expiry date will be selected. | Format | Syntax | |:---------------------|:---------------------------------------------------| | Command line | `--trusted-root-certificate-subject-name` | | YAML | `TrustedRootCertificateSubjectName` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_SUBJECT_NAME` | ### Certificate generation tool Event Store provides the interactive Certificate Generation CLI, which creates certificates signed by a private, auto-generated CA for EventStoreDB. You can use the [configuration wizard](installation.md), that will provide you exact CLI commands that you need to run to generate certificates matching your configuration. #### Getting started CLI is available as Open Source project in the [Github Repository](https://github.com/EventStore/es-gencert-cli). The latest release can be found under the [GitHub releases page.](https://github.com/EventStore/es-gencert-cli/releases) We're releasing binaries for Windows, Linux and macOS. We also publish the tool as a Docker image. Basic usage for Certificate Generation CLI: ```bash ./es-gencert-cli [options] [args] ``` Getting help for a specific command: ```bash ./es-gencert-cli -help ``` ::: warning If you are running EventStoreDB on Linux, remember that all certificate files should have restrictive rights, otherwise the OS won't allow using them. Usually, you'd need to change rights for each certificate file to prevent the "permissions are too open" error. You can do it by running the following command: ```bash chmod 600 [file] ``` ::: #### Generating the CA certificate As the first step CA certificate needs to be generated. It'll need to be trusted for each of the nodes and client environment. By default, the tool will create the `ca` directory in the `certs` directory you created. Two keys will be generated: * `ca.crt` - public file that need to be used also for the nodes and client configuration, * `ca.key` - private key file that should be used only in the node configuration. **Do not copy it to client environment**. CA certificate will be generated with pre-defined CN `eventstoredb-node`. To generate CA certificate run: ```bash ./es-gencert-cli create-ca ``` You can customise generated cert by providing following params: | Param | Description | |:--------|:------------------------------------------------------------------| | `-days` | The validity period of the certificate in days (default: 5 years) | | `-out` | The output directory (default: ./ca) | Example: ```bash ./es-gencert-cli create-ca -out ./es-ca ``` #### Generating the Node certificate You need to generate certificates signed by the CA for each node. They should be installed only on the specific node machine. By default, the tool will create the `ca` directory in the `certs` directory you created. Two keys will be generated: * `node.crt` - the public file that needs to be also used for the nodes and client configuration, * `node.key` - the private key file that should be used only in the node's configuration. **Do not copy it to client environment**. To generate node certificate run command: ```bash ./es-gencert-cli -help create_node ``` You can customise generated cert by providing following params: | Param | Description | |:------------------|:------------------------------------------------------------------------------| | `-ca-certificate` | The path to the CA certificate file (default: `./ca/ca.crt`) | | `-ca-key` | The path to the CA key file (default: `./ca/ca.key`) | | `-days` | The output directory (default: `./nodeX` where X is an auto-generated number) | | `-out` | The output directory (default: `./ca`) | | `-ip-addresses` | Comma-separated list of IP addresses of the node | | `-dns-names` | Comma-separated list of DNS names of the node | ::: warning While generating the certificate, you need to remember to pass internal end external: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` and/or * DNS names to `-dns-names`: e.g. `localhost,node1.eventstore` that will match the URLs that you will be accessing EventStoreDB nodes. ::: Sample: ``` ./es-gencert-cli-cli create-node \ -ca-certificate ./es-ca/ca.crt \ -ca-key ./es-ca/ca.key \ -out ./node1 \ -ip-addresses 127.0.0.1,172.20.240.1 \ -dns-names localhost,node1.eventstore ``` #### Running with Docker You could also run the tool using Docker interactive container: ```bash docker run --rm -i eventstore/es-gencert-cli ``` One useful scenario is to use the Docker Compose file tool to generate all the necessary certificates before starting cluster nodes. Sample: ```yaml version: "3.5" services: setup: image: eventstore/es-gencert-cli:1.0.2 entrypoint: bash user: "1000:1000" command: > -c "mkdir -p ./certs && cd /certs && es-gencert-cli create-ca && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,node1.eventstore && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.2 -dns-names localhost,node2.eventstore && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.3 -dns-names localhost,node3.eventstore && find . -type f -print0 | xargs -0 chmod 666" container_name: setup volumes: - ./certs:/certs ``` See more in the [complete sample of docker-compose secured cluster configuration.](installation.md#use-docker-compose) ### Certificate installation on a client environment To connect to EventStoreDB, you need to install the auto-generated CA certificate file on the client machine (e.g. machine where the client is hosted, or your dev environment). #### Linux (Ubuntu, Debian) 1. Copy auto-generated CA file to dir `/usr/local/share/ca-certificates/`, e.g. using command: ```bash sudo cp ca.crt /usr/local/share/ca-certificates/event_store_ca.crt ``` 2. Update the CA store: ```bash sudo update-ca-certificates ``` #### Windows 1. You can manually import it to the local CA cert store through `Certificates Local Machine Management Console`. To do that select **Run** from the **Start** menu, and then enter `certmgr.msc`. Then import certificate to `Trusted Root Certification`. 2. You can also run the PowerShell script instead: ```powershell Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root ``` #### MacOS 1. In the Keychain Access app on your Mac, select either the login or System keychain. Drag the certificate file onto the Keychain Access app. If you're asked to provide a name and password, type the name and password for an administrator user on this computer. 2. You can also run the bash script: ```bash sudo security add-certificates -k /Library/Keychains/System.keychain ca.crt ``` ### Intermediate CA certificates Intermediate CA certificates are supported by loading them from a [PEM](https://datatracker.ietf.org/doc/html/rfc1422) or [PKCS #12](https://datatracker.ietf.org/doc/html/rfc7292) bundle specified by the [`CertificateFile` configuration parameter](#certificate-file). To make sure that the configuration is correct, the certificate chain is validated on startup with the node's own certificate. If you've used the [certificate generation tool](#certificate-generation-tool) with the default settings to generate your CA and node certificates, then you're not using intermediate CA certificates. However, if you're using a public certificate authority (e.g [Let's Encrypt](https://letsencrypt.org/)) to generate your node certificates there is a chance that you're using intermediate CA certificates without knowing. This is due to the [Authority Information Access (AIA)](https://datatracker.ietf.org/doc/html/rfc4325#section-2) extension which allows intermediate certificates to be fetched from a remote server. To verify if your certificate is using the AIA extension, you need to verify if there is a section named: `Authority Information Access` in the certificate. ::: tabs#os @tab Linux Use `openssl` to find the section in the certificate file: ``` openssl x509 -in /path/to/node.crt -text | grep 'Authority Information Access' -A 1 ``` @tab Windows Open the certificate, go to the `Details` tab and look for the `Authority Information Access` field. If the extension is present, you can manually download the intermediate certificate from the URL present under the `CA Issuers` entry. Note that you will usually need to convert the downloaded certificate from the `DER` to the `PEM` format. This can be done with the following `openssl` command: ```bash openssl x509 -inform der -in /path/to/cert.der > /path/to/cert.pem ``` or with [an online service](https://www.sslshopper.com/ssl-converter.html) if you don't have openssl installed. ::: It's possible that there are more than one intermediate CA certificates in the chain - so you need to verify if the certificate you've just downloaded also uses the AIA extension. If yes, you need to download the next intermediate CA certificate in the chain by repeating the same process above until you eventually reach a publicly trusted root certificate (i.e. the `Subject` and `Issuer` fields will match). In practice, there'll usually be at most two intermediate certificates in the chain. #### Bundling the intermediate certificates The node's certificate should be first in the bundle, followed by the intermediates. Intermediates can be in any order but it would be good to keep it from leaf to root, as per the usual convention. The root certificate should not be bundled. In the examples below, intermediate certificates are numbered from 1 to N starting from the leaf and going up. ##### PEM format If your node's certificate and the intermediate CA certificates are both PEM formatted, that is they begin with `-----BEGIN CERTIFICATE-----` and end with `-----END CERTIFICATE-----` then you can simply append the contents of the intermediate certificate files to the end of the node's certificate file to create the bundle. ::: tabs#os @tab Linux ```bash cat /path/to/intermediate1.crt >> /path/to/node.crt ... cat /path/to/intermediateN.crt >> /path/to/node.crt ``` @tab Windows ```powershell type C:\path\to\intermediate1.crt >> C:\path\to\node.crt ... type C:\path\to\intermediateN.crt >> C:\path\to\node.crt ``` ::: ##### PKCS #12 format If you want to generate a PKCS #12 bundle from PEM formatted certificate files, please follow the steps below. ```bash cat /path/to/intermediate1.crt >> ./ca_bundle.crt ... cat /path/to/intermediateN.crt >> ./ca_bundle.crt openssl pkcs12 -export -in /path/to/node.crt -inkey /path/to/node.key -certfile ./ca_bundle.crt -out /path/to/node.p12 -passout pass: ``` #### Adding intermediate certificates to the certificate store Intermediate certificates also need to be added to the current user's certificate store. This is required for two reasons:\ i) For the full certificate chain to be sent when TLS connections are established\ ii) To improve performance by preventing certificate downloads if your certificate uses the AIA extension ::: tabs#os @tab Linux The following script assumes EventStoreDB is running under the `eventstore` account. ```bash sudo su eventstore --shell /bin/bash dotnet tool install --global dotnet-certificate-tool ~/.dotnet/tools/certificate-tool add -s CertificateAuthority -l CurrentUser --file /path/to/intermediate.crt ``` @tab Windows To import the intermediate certificate in the `Intermediate Certification Authorities` certificate store, run the following PowerShell command under the same account as EventStoreDB is running: ```powershell Import-Certificate -FilePath .\path\to\intermediate.crt -CertStoreLocation Cert:\CurrentUser\CA ``` Optionally, to import the intermediate certificate in the `Local Computer` store, run the following as `Administrator`: ```powershell Import-Certificate -FilePath .\ca.crt -CertStoreLocation Cert:\LocalMachine\CA ``` ::: ### TCP protocol security Although TCP is disabled by default for external connections (clients), cluster nodes still use TCP for replication. If you aren't running EventStoreDB in insecure mode, all TCP communication will use TLS using the same certificates as SSL. You can, however, disable TLS for both internal and external TCP. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-internal-tcp-tls` | | YAML | `DisableInternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_INTERNAL_TCP_TLS` | **Default**: `false` | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-external-tcp-tls` | | YAML | `DisableExternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_EXTERNAL_TCP_TLS` | **Default**: `false` ## Authentication EventStoreDB supports authentication based on usernames and passwords out of the box. The Enterprise version also supports LDAP as the authentication source. Authentication is applied to all HTTP endpoints, except `/info`, `/ping`, `/stats`, `/elections` (only `GET`) , `/gossip` (only `GET`) and static web content. ### Default users EventStoreDB provides two default users, `$ops` and `$admin`. `$admin` has full access to everything in EventStoreDB. It can read and write to protected streams, which is any stream that starts with $, such as `$projections-master`. Protected streams are usually system streams, for example, `$projections-master` manages some projections' states. The `$admin` user can also run operational commands, such as scavenges and shutdowns on EventStoreDB. An `$ops` user can do everything that an `$admin` can do except manage users and read from system streams ( except for `$scavenges` and `$scavenges-streams`). ### New users New users created in EventStoreDB are standard non-authenticated users. Non-authenticated users are allowed `GET` access to the `/info`, `/ping`, `/stats`, `/elections`, and `/gossip` system streams. `POST` access to the `/elections` and `/gossip` system streams is only allowed on the internal HTTP service. By default, any user can read any non-protected stream unless there is an ACL preventing that. ### Externalised authentication You can also use the trusted intermediary header for externalized authentication that allows you to integrate almost any authentication system with EventStoreDB. Read more about [the trusted intermediary header](#trusted-intermediary). ### Disable HTTP authentication It is possible to disable authentication on all protected HTTP endpoints by setting the `DisableFirstLevelHttpAuthorization` setting to `true`. The setting is set to `false` by default. When enabled, the setting will force EventStoreDB to use the supplied credentials only to check the stream access using [ACLs](#access-control-lists). ## Access control lists By default, authenticated users have access to the whole EventStoreDB database. In addition to that, it allows you to use Access Control Lists (ACLs) to set up more granular access control. In fact, the default access level is also controlled by a special ACL, which is called the [default ACL](#default-acl). ### Stream ACL EventStoreDB keeps the ACL of a stream in the stream metadata as JSON with the below definition: ```json { "$acl": { "$w": "$admins", "$r": "$all", "$d": "$admins", "$mw": "$admins", "$mr": "$admins" } } ``` These fields represent the following: * `$w` The permission to write to this stream. * `$r` The permission to read from this stream. * `$d` The permission to delete this stream. * `$mw` The permission to write the metadata associated with this stream. * `$mr` The permission to read the metadata associated with this stream. You can update these fields with either a single string or an array of strings representing users or groups (`$admins`, `$all`, or custom groups). It's possible to put an empty array into one of these fields, and this has the effect of removing all users from that permission. ::: tip We recommend you don't give people access to `$mw` as then they can then change the ACL. ::: ### Default ACL The `$settings` stream has a special ACL used as the default ACL. This stream controls the default ACL for streams without an ACL and also controls who can create streams in the system, the default state of these is shown below: ```json { "$userStreamAcl": { "$r": "$all", "$w": "$all", "$d": "$all", "$mr": "$all", "$mw": "$all" }, "$systemStreamAcl": { "$r": "$admins", "$w": "$admins", "$d": "$admins", "$mr": "$admins", "$mw": "$admins" } } ``` You can rewrite these to the `$settings` stream with the following request: @[code{curl}](@samples/server/default-settings.sh) The `$userStreamAcl` controls the default ACL for user streams, while all system streams use the `$systemStreamAcl` as the default. ::: tip The `$w` in `$userStreamAcl` also applies to the ability to create a stream. Members of `$admins` always have access to everything, you cannot remove this permission. ::: When you set a permission on a stream, it overrides the default values. However, it's not necessary to specify all permissions on a stream. It's only necessary to specify those which differ from the default. Here is an example of the default ACL that has been changed: ```json { "$userStreamAcl": { "$r": "$all", "$w": "ouro", "$d": "ouro", "$mr": "ouro", "$mw": "ouro" }, "$systemStreamAcl": { "$r": "$admins", "$w": "$admins", "$d": "$admins", "$mr": "$admins", "$mw": "$admins" } } ``` This default ACL gives `ouro` and `$admins` create and write permissions on all streams, while everyone else can read from them. Be careful allowing default access to system streams to non-admins as they would also have access to `$settings` unless you specifically override it. Refer to the documentation of the HTTP API or SDK of your choice for more information about changing ACLs programmatically. ## Trusted intermediary The trusted intermediary header helps EventStoreDB to support a common security architecture. There are thousands of possible methods for handling authentication and it is impossible for us to support them all. The header allows you to configure a trusted intermediary to handle the authentication instead of EventStoreDB. A sample configuration is to enable OAuth2 with the following steps: * Configure EventStoreDB to run on the local loopback. * Configure nginx to handle OAuth2 authentication. * After authenticating the user, nginx rewrites the request and forwards it to the loopback to EventStoreDB that serves the request. The header has the form of `{user}; group, group1` and the EventStoreDB ACLs use the information to handle security. ```http:no-line-numbers ES-TrustedAuth: "root; admin, other" ``` Use the following option to enable this feature: | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--enable-trusted-auth` | | YAML | `EnableTrustedAuth` | | Environment variable | `EVENTSTORE_ENABLE_TRUSTED_AUTH` | --- --- url: 'https://docs.kurrent.io/server/v22.10/streams.md' --- # Event streams EventStoreDB is purpose-built for event storage. Unlike traditional state-based databases, which retain only the most recent entity state, EventStoreDB allows you to store each state alteration as an independent event. These **events** are logically organized into **streams**, typically only one stream per entity. ## Metadata and reserved names ### Stream metadata Every stream in EventStoreDB has metadata stream associated with it, prefixed by `$$`, so the metadata stream from a stream called `foo` is `$$foo`. EventStoreDB allows you to change some values in the metadata, and you can write your own data into stream metadata that you can refer to in your code. ### Reserved names All internal data used by EventStoreDB is prefixed with a `$` character (for example `$maxCount` on a stream's metadata). Because of this you should not use names with a `$` prefix as event names, metadata keys, or stream names, except where detailed below. The supported internal settings are: | Property name | Description | | :-------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$maxAge` | Sets a sliding window based on dates. When data reaches a certain age it disappears automatically from the stream and is considered eligible for scavenging. This value is set as an integer representing the number of seconds. This value must be >= 1. | | `$maxCount` | Sets a sliding window based on the number of items in the stream. When data reaches a certain length it disappears automatically from the stream and is considered eligible for scavenging. This value is set as an integer representing the count of items. This value must be >= 1. | | `$cacheControl` | This controls the cache of the head of a stream. Most URIs in a stream are infinitely cacheable but the head by default will not cache. It may be preferable in some situations to set a small amount of caching on the head to allow intermediaries to handle polls (say 10 seconds). The argument is an integer representing the seconds to cache. This value must be >= 1. | ::: tip If you set both `$maxAge` and `$maxCount` then events will become eligible for scavenging when either criteria is met. For example, if you set `$maxAge` to 10 and `$maxCount` to 50,000, events will be marked as eligible for scavenging after either 10 seconds, or 50,000 events, have passed. Deleted items will only be removed once the scavenge process runs. ::: Security access control lists are also included in the `$acl` section of the stream metadata. | Property name | Description | | :------------ | :---------------------------------------------------------- | | `$r` | The list of users with read permissions | | `$w` | The list of users with write permissions | | `$d` | The list of users with delete permissions | | `$mw` | The list of users with write permissions to stream metadata | | `$mr` | The list of users with read permissions to stream metadata | You can find more details on access control lists can [here](security.md#access-control-lists). ### Event metadata Every event in EventStoreDB has metadata associated with it. EventStoreDB allows you to change some values in the metadata, and you can write your own data into event metadata that you can refer to in your code. All names starting with `$` are reserved space for internal use. The currently supported reserved internal names are: | Property name | Description | | :--------------- | :----------------------------------------------------------------- | | `$correlationId` | The application level correlation ID associated with this message. | | `$causationId` | The application level causation ID associated with this message. | Projections honor both the `correlationId` and `causationId` patterns for any events it produces internally ( linkTo, emit, etc.). ## Deleting streams and events Metadata in EventStoreDB defines whether an event is deleted or not. You can use [stream metadata](#metadata-and-reserved-names) such as `TruncateBefore`, `MaxAge` and `MaxCount` to filter events considered deleted. When reading a stream, the index checks the stream's metadata to determine whether any of its events have been deleted. You cannot delete events from the middle of the stream. EventStoreDB only allows to *truncate* the stream. When you delete a stream, you can use either a soft delete or hard delete. When a stream is soft-deleted, all events from the stream get scavenged during the next scavenging run. It means that you can reopen the stream by appending to it again. When using hard delete, the stream gets closed with a tombstone event. Such an event tells the database that the stream cannot be reopened, so any attempt to append to the hard-deleted stream will fail. The tombstone event doesn't get scavenged. The `$all` stream bypasses the index, meaning that it does not check the metadata to determine whether events exist or not. As such, events that have been deleted are still be readable until a scavenge has removed them. There are requirements for a scavenge to successfully remove events, for more information about this, read the [scavenging guide](operations.md#scavenging). EventStoreDB will always keep one event in the stream even if the stream was deleted, to indicate the stream existence and the last event version. Therefore, we advise you to append a specific event like `StreamDeleted` and then set the max count to one to keep the stream or delete the stream. Keep that in mind when deleting streams that contain sensitive information that you really want to remove without a trace. ### Soft delete and `TruncateBefore` `TruncateBefore` and `$tb` considers any event with an event number lower than its value as deleted. For example, if you had the following events in a stream : ``` 0@test-stream 1@test-stream 2@test-stream 3@test-stream ``` If you set the `TruncateBefore` or `$tb` value to 3, a read of the stream would result in only reading the last event: ``` 3@test-stream ``` A **soft delete** makes use of `TruncateBefore` and `$tb`. When you delete a stream, its `TruncateBefore` or `$tb` is set to the [max long/Int64 value](https://docs.microsoft.com/en-us/dotnet/api/system.int64.maxvalue?view=net-5.0): 9223372036854775807. When you read a soft deleted stream, the read returns a `StreamNotFound` or `404` result. After deleting the stream, you are able to append to it again, continuing from where it left off. For example, if you soft deleted the above example stream, the `TruncateBefore` or `$tb` is set to 9223372036854775807. If you were to append to the stream again, the next event is appended with event number 4. Only events from event number 4 (last stream revision before deleting, incremented by one) onwards are visible when you read this stream. ### Max count and Max age **Max count** (`$maxCount` and `MaxCount`) limits the number of events that you can read from a stream. If you try to read a stream that has a max count of 5, you are only able to read the last 5 events, regardless of how many events are in the stream. **Max age** (`$maxAge` and `MaxAge`) specifies the number of seconds an event can live for. The age is calculated at the time of the read. So if you read a stream with a `MaxAge` of 3 minutes and one of the events in the stream has existed for 4 minutes at the time of the read, it is not returned. ### Hard delete A **hard delete** appends a `tombstone` event to the stream, permanently deleting it. You cannot recreate the stream, or append to it again. Tombstone events are appended with the event type `$streamDeleted`. When you read a hard deleted stream, the read returns a `StreamDeleted` or `410` result. The events in the deleted stream are liable to be removed in a scavenge, but the tombstone event remains. A hard delete of a stream is permanent. You cannot append to the stream or recreate it. As such, you should generally soft delete streams unless you have a specific need to permanently delete the stream. ### Deleted events and projections If you are intending on using projections and deleting streams, there are some things to take into consideration: * Due to the nature of `$all`, projections using `fromAll` read any deleted events that have not been scavenged. They also receive any tombstone events from hard deletes. * System projections like [by category](projections.md#by-category) or [by event type](projections.md#by-event-type) projections produce new (link) events that are stored in the database in addition to the original event. When you delete the original events, then link events will remain in the projected streams, but their links won't be resolved (will have undefined value). You can ignore those events in the code logic. ## System events and streams ### `$persistentSubscriptionConfig` `$persistentSubscriptionConfig` is a special paged stream that contains all configuration events, for all persistent subscriptions. It uses the `PersistentConfig` system event type, which records a configuration event. The event data contains: * `version`: Version of event data * `updated`: Updated date * `updatedBy`: User who updated configuration * `maxCount`: The number of configuration events to save * `entries`: Configuration items set by event. ### `$all` `$all` is a special paged stream for all events. You can use the same paged form of reading described above to read all events for a node by pointing the stream at */streams/$all*. As it's a stream like any other, you can perform all operations, except posting to it. ### `$settings` The `$settings` stream has a special ACL used as the default ACL. This stream controls the default ACL for streams without an ACL and also controls who can create streams in the system. Learn more about the default ACL in the [access control lists](security.md#default-acl) documentation. ### `$stats` EventStoreDB has debug and statistics information available about a cluster in the `$stats` stream, find out more in [the stats guide](diagnostics.md#statistics). ### `$scavenges` `$scavenges` is a special paged stream for all scavenge related events. It uses the following system event types: * `$scavengeIndexInitialized`: An event that records the initialisation of the scavenge index. * `$scavengeStarted`: An event that records the beginning of a scavenge event, the event data contains: * `scavengeId`: Scavenge event ID * `nodeEndpoint`: Node address * `$scavengeCompleted`: An event that records the completion of a scavenge event, the event data contains: * `scavengeId`: Scavenge event ID * `nodeEndpoint`: Node address * `result`: `Success`, `Failed`, `Stopped` * `error`: Error details * `timeTaken`: Time taken for the scavenge event in milliseconds * `spaceSaved`: Space saved by scavenge event in bytes --- --- url: 'https://docs.kurrent.io/server/v22.10/upgrade-guide.md' --- # Upgrade Guide ## Upgrade guide for EventStoreDB 22.10 Packages are available on our [website](https://www.eventstore.com/downloads) and can be installed via [Packagecloud](https://packagecloud.io/EventStore/EventStore-OSS), [Chocolatey](https://chocolatey.org/packages/eventstore-oss), or [Docker](https://hub.docker.com/r/eventstore/eventstore/tags?page=1\&name=22.10). See detailed commands/instructions for each platform. ### Upgrade procedure An online rolling upgrade can be done from these versions of EventStoreDB, directly to 22.10.4: * Previous versions of 22.10.x * 22.6 * 21.10 * 20.10 Follow the upgrade procedure below on each node, starting with a follower node: 1. Stop the node. 2. Upgrade EventstoreDB to 22.10 and update configuration. 3. Start the node. 4. Wait for the node to become a follower or read-only replica. 5. Repeat on the next node. As illustrated below: ![EventStoreDB upgrade procedure for each node](images/upgrade-procedure.png) Upgrading the cluster in this manner keeps the cluster online and able to service requests. There may still be disruptions to your services during the upgrade, namely: * Client connections may be disconnected when nodes go offline, or when elections take place. * The cluster is less fault-tolerant while a node is offline for an upgrade because the cluster requires a quorum of nodes to be online to service write requests. * Replicating large amounts of data to a node can have a performance impact on the Leader in the cluster. ### Breaking changes from version 21.10 If you are upgrading from 21.10 or earlier, then you need to be aware of a breaking change in the TCP proto. #### Proto2 upgraded to Proto3 (TCP) The server now uses Proto3 for messages sent over TCP. This affects both replication and the legacy dotnet client. EventStoreDB nodes on version 22.10 cannot replicate data to version 21.10 and below, but older nodes can still replicate to version 22.10 and above. Follow the [upgrade procedure](#upgrade-procedure) and ensure that the Leader node is the last node to be upgraded to avoid any issues. ### Breaking changes from version 20.10 If you are upgrading from 20.10, then you need to be aware of the following breaking changes: #### Log files rotation Proper log rotation to on disk logging was added in 21.10. To accomplish this, the file names for both the stats and regular logs now include a date-stamp. i.e., log-statsYYYYMMDD.json and logYYYYMMDD.json, respectively. #### Intermediate Certificates Support for intermediate certificates was added in EventStoreDB version 21.10. If you are using intermediate certificates on a previous version and rely on the AIA URL to build the full chain, the configuration will no longer work and will print the following error on startup: > Failed to build the certificate chain with the node's own certificate up to the root --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/index.md' --- # Configuring the server EventStoreDB has a number of configuration options that can be changed. You can find all the options described in details in this section. When you don't change the configuration, EventStoreDB will use sensible defaults, but they might not suit your needs. You can always instruct EventStoreDB to use a different set of options. There are multiple ways to configure EventStoreDB server, described below. ## Version and help You can check what version of EventStoreDB you have installed by using the `--version` parameter in the command line. For example: ::: tabs#os @tab Linux ```bash $ eventstored --version EventStoreDB version 23.10.0.0 (oss-v23.10.0/8660912b8, 2023-10-24T16:13:49+01:00) ``` @tab Windows ``` > EventStore.ClusterNode.exe --version EventStoreDB version 23.10.0.0 (oss-v23.10.0/8660912b8, 2023-10-24T16:13:49+01:00) ``` ::: The full list of available options is available from the currently installed server by using the `--help` option in the command line. ## Configuration file You would use the configuration file when you want the server to run with the same set of options every time. YAML files are better for large installations as you can centrally distribute and manage them, or generate them from a configuration management system. The configuration file has YAML-compatible format. The basic format of the YAML configuration file is as follows: ```yaml:no-line-numbers --- Db: "/volumes/data" Log: "/esdb/logs" ``` ::: tip You need to use the three dashes and spacing in your YAML file. ::: The default configuration file name is `eventstore.conf`. It is located in `/etc/eventstore/` on Linux and the server installation directory on Windows. You can either change this file or create another file and instruct EventStoreDB to use it. To tell the EventStoreDB server to use a different configuration file, you pass the file path on the command line with `--config=filename`, or use the `CONFIG` environment variable. ## Environment variables You can also set all arguments with environment variables. All variables are prefixed with `EVENTSTORE_` and normally follow the pattern `EVENTSTORE_{option}`. For example, setting the `EVENTSTORE_LOG` variable would instruct the server to use a custom location for log files. Environment variables override all the options specified in configuration files. ## Command line You can also override options from both configuration files and environment variables using the command line. For example, starting EventStoreDB with the `--log` option will override the default log files location: ::: tabs#os @tab Linux ```bash:no-line-numbers eventstored --log /tmp/eventstore/logs ``` @tab Windows ```bash:no-line-numbers EventStore.ClusterNode.exe --log C:\Temp\EventStore\Logs ``` ::: ## Testing the configuration If more than one method is used to configure the server, it might be hard to find out what the effective configuration will be when the server starts. To help to find out just that, you can use the `--what-if` option. When you run EventStoreDB with this option, it will print out the effective configuration applied from all available sources (default and custom configuration file, environment variables and command line parameters) and print it out to the console. ::: details Click here to see a WhatIf example ``` $ eventstored --what-if [68443, 1,17:38:39.178,INF] [68443, 1,17:38:39.186,INF] "OS ARCHITECTURE:" X64 [68443, 1,17:38:39.207,INF] "OS:" Linux ("Unix 5.19.0.1025") [68443, 1,17:38:39.210,INF] "RUNTIME:" ".NET 6.0.21/e40b3abf1" (64-bit) [68443, 1,17:38:39.211,INF] "GC:" "3 GENERATIONS" "IsServerGC: False" "Latency Mode: Interactive" [68443, 1,17:38:39.212,INF] "LOGS:" "/var/log/eventstore" [68443, 1,17:38:39.251,INF] MODIFIED OPTIONS: Application Options: WHAT IF: true (Command Line) DEFAULT OPTIONS: Application Options: ALLOW ANONYMOUS ENDPOINT ACCESS: True () ALLOW ANONYMOUS STREAM ACCESS: True () ALLOW UNKNOWN OPTIONS: False () CONFIG: /etc/eventstore/eventstore.conf () DISABLE HTTP CACHING: False () ENABLE HISTOGRAMS: False () HELP: False () LOG FAILED AUTHENTICATION ATTEMPTS: False () LOG HTTP REQUESTS: False () MAX APPEND SIZE: 1048576 () SKIP INDEX SCAN ON READS: False () STATS PERIOD SEC: 30 () VERSION: False () WORKER THREADS: 0 () Authentication/Authorization Options: AUTHENTICATION CONFIG: () AUTHENTICATION TYPE: internal () AUTHORIZATION CONFIG: () AUTHORIZATION TYPE: internal () DISABLE FIRST LEVEL HTTP AUTHORIZATION: False () Certificate Options: CERTIFICATE RESERVED NODE COMMON NAME: eventstoredb-node () TRUSTED ROOT CERTIFICATES PATH: /etc/ssl/certs () Certificate Options (from file): CERTIFICATE FILE: () CERTIFICATE PASSWORD: () CERTIFICATE PRIVATE KEY FILE: () CERTIFICATE PRIVATE KEY PASSWORD: () Certificate Options (from store): CERTIFICATE STORE LOCATION: () CERTIFICATE STORE NAME: () CERTIFICATE SUBJECT NAME: () CERTIFICATE THUMBPRINT: () TRUSTED ROOT CERTIFICATE STORE LOCATION: () TRUSTED ROOT CERTIFICATE STORE NAME: () TRUSTED ROOT CERTIFICATE SUBJECT NAME: () TRUSTED ROOT CERTIFICATE THUMBPRINT: () Cluster Options: CLUSTER DNS: fake.dns () CLUSTER GOSSIP PORT: 2113 () CLUSTER SIZE: 1 () DEAD MEMBER REMOVAL PERIOD SEC: 1800 () DISCOVER VIA DNS: True () GOSSIP ALLOWED DIFFERENCE MS: 60000 () GOSSIP INTERVAL MS: 2000 () GOSSIP SEED: () GOSSIP TIMEOUT MS: 2500 () LEADER ELECTION TIMEOUT MS: 1000 () NODE PRIORITY: 0 () QUORUM SIZE: 1 () READ ONLY REPLICA: False () STREAM INFO CACHE CAPACITY: 0 () UNSAFE ALLOW SURPLUS NODES: False () Database Options: ALWAYS KEEP SCAVENGED: False () CACHED CHUNKS: -1 () CHUNK INITIAL READER COUNT: 5 () CHUNK SIZE: 268435456 () CHUNKS CACHE SIZE: 536871424 () COMMIT TIMEOUT MS: 2000 () DB: /var/lib/eventstore () DB LOG FORMAT: V2 () DISABLE SCAVENGE MERGING: False () HASH COLLISION READ LIMIT: 100 () INDEX: () INDEX CACHE DEPTH: 16 () INDEX CACHE SIZE: 0 () INITIALIZATION THREADS: 1 () MAX AUTO MERGE INDEX LEVEL: 2147483647 () MAX MEM TABLE SIZE: 1000000 () MAX TRUNCATION: 268435456 () MEM DB: False () MIN FLUSH DELAY MS: 2 () OPTIMIZE INDEX MERGE: False () PREPARE TIMEOUT MS: 2000 () READER THREADS COUNT: 0 () REDUCE FILE CACHE PRESSURE: False () SCAVENGE BACKEND CACHE SIZE: 67108864 () SCAVENGE BACKEND PAGE SIZE: 16384 () SCAVENGE HASH USERS CACHE CAPACITY: 100000 () SCAVENGE HISTORY MAX AGE: 30 () SKIP DB VERIFY: False () SKIP INDEX VERIFY: False () STATS STORAGE: File () STREAM EXISTENCE FILTER SIZE: 256000000 () UNBUFFERED: False () UNSAFE DISABLE FLUSH TO DISK: False () UNSAFE IGNORE HARD DELETE: False () USE INDEX BLOOM FILTERS: True () WRITE STATS TO DB: False () WRITE THROUGH: False () WRITE TIMEOUT MS: 2000 () Default User Options: DEFAULT ADMIN PASSWORD: **** () DEFAULT OPS PASSWORD: **** () Dev mode Options: DEV: False () REMOVE DEV CERTS: False () gRPC Options: KEEP ALIVE INTERVAL: 10000 () KEEP ALIVE TIMEOUT: 10000 () Interface Options: ADVERTISE HOST TO CLIENT AS: () ADVERTISE HTTP PORT TO CLIENT AS: 0 () ADVERTISE NODE PORT TO CLIENT AS: 0 () ADVERTISE TCP PORT TO CLIENT AS: 0 () CONNECTION PENDING SEND BYTES THRESHOLD: 10485760 () CONNECTION QUEUE SIZE THRESHOLD: 50000 () DISABLE ADMIN UI: False () DISABLE EXTERNAL TCP TLS: False () DISABLE GOSSIP ON HTTP: False () DISABLE INTERNAL TCP TLS: False () DISABLE STATS ON HTTP: False () ENABLE ATOM PUB OVER HTTP: False () ENABLE EXTERNAL TCP: False () ENABLE TRUSTED AUTH: False () ENABLE UNIX SOCKET: False () EXT HOST ADVERTISE AS: () EXT IP: 127.0.0.1 () EXT TCP HEARTBEAT INTERVAL: 2000 () EXT TCP HEARTBEAT TIMEOUT: 1000 () EXT TCP PORT: 1113 () EXT TCP PORT ADVERTISE AS: 0 () GOSSIP ON SINGLE NODE: () HTTP PORT: 2113 () HTTP PORT ADVERTISE AS: 0 () INT HOST ADVERTISE AS: () INT IP: 127.0.0.1 () INT TCP HEARTBEAT INTERVAL: 700 () INT TCP HEARTBEAT TIMEOUT: 700 () INT TCP PORT: 1112 () INT TCP PORT ADVERTISE AS: 0 () NODE HEARTBEAT INTERVAL: 2000 () NODE HEARTBEAT TIMEOUT: 1000 () NODE HOST ADVERTISE AS: () NODE IP: 127.0.0.1 () NODE PORT: 2113 () NODE PORT ADVERTISE AS: 0 () NODE TCP PORT: 1113 () NODE TCP PORT ADVERTISE AS: 0 () REPLICATION HEARTBEAT INTERVAL: 700 () REPLICATION HEARTBEAT TIMEOUT: 700 () REPLICATION HOST ADVERTISE AS: () REPLICATION IP: 127.0.0.1 () REPLICATION PORT: 1112 () REPLICATION TCP PORT ADVERTISE AS: 0 () Logging Options: DISABLE LOG FILE: False () LOG: /var/log/eventstore () LOG CONFIG: logconfig.json () LOG CONSOLE FORMAT: Plain () LOG FILE INTERVAL: Day () LOG FILE RETENTION COUNT: 31 () LOG FILE SIZE: 1073741824 () LOG LEVEL: Default () Projection Options: FAULT OUT OF ORDER PROJECTIONS: False () PROJECTION COMPILATION TIMEOUT: 500 () PROJECTION EXECUTION TIMEOUT: 250 () PROJECTION THREADS: 3 () PROJECTIONS QUERY EXPIRY: 5 () RUN PROJECTIONS: None () START STANDARD PROJECTIONS: False () ``` ::: ::: note Version 21.6 introduced a stricter configuration check: the server will *not start* when an unknown configuration options is passed in either the configuration file, environment variable or command line. E.g: the following will prevent the server from starting: * `--UnknownConfig` on the command line * `EVENTSTORE_UnknownConfig` through environment variable * `UnknownConfig: value` in the config file And will output on `stdout` only: *Error while parsing options: The option UnknownConfig is not a known option. (Parameter 'UnknownConfig')*. ::: ## Autoconfigured options Some options are configured at startup to make better use of the available resources on larger instances or machines. These options are `StreamInfoCacheCapacity`, `ReaderThreadsCount`, and `WorkerThreads`. ### StreamInfoCacheCapacity This option sets the maximum number of entries to keep in the stream info cache. This is the lookup that contains the information of any stream that has recently been read or written to. Having entries in this cache significantly improves write and read performance to cached streams on larger databases. By default, the cache dynamically resizes according to the amount of free memory. The minimum that it can be set to is 100,000 entries. | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--stream-info-cache-capacity` | | YAML | `StreamInfoCacheCapacity` | | Environment variable | `EVENTSTORE_STREAM_INFO_CACHE_CAPACITY` | The option is set to 0 by default, which enables dynamic resizing. The default on previous versions of EventStoreDb was 100,000 entries. ::: note The default value of 0 for `StreamInfoCacheCapacity` might not always be the best value for optimal performance. Ideally, it should be set to double the number of streams in the anticipated working set. The total number of streams can be obtained by checking the event count in the `$streams` system stream. This stream is created by the [$streams system projection](https://developers.eventstore.com/server/v24.2/projections.html#streams-projection). It should be noted that the total number of streams does not necessarily give you the anticipated working set. The working set of streams is the set of streams that you intend on actively reading, writing, and/or subscribing to. This can be much lower than the total number of streams in certain cases, especially in systems that have many short-lived streams. ::: ### ReaderThreadsCount This option configures the number of reader threads available to EventStoreDb. Having more reader threads allows more concurrent reads to be processed. The reader threads count will be set at startup to twice the number of available processors, with a minimum of 4 and a maximum of 16 threads. | Format | Syntax | |:---------------------|:----------------------------------| | Command line | `--reader-threads-count` | | YAML | `ReaderThreadsCount` | | Environment variable | `EVENTSTORE_READER_THREADS_COUNT` | The option is set to 0 by default, which enables autoconfiguration. The default on previous versions of EventStoreDb was 4 threads. ::: warning Increasing the reader threads count too high can cause read timeouts if your disk cannot handle the increased load. ::: ### WorkerThreads The `WorkerThreads` option configures the number of threads available to the pool of worker services. At startup the number of worker threads will be set to 10 if there are more than 4 reader threads. Otherwise, it will be set to have 5 threads available. | Format | Syntax | |:---------------------|:----------------------------| | Command line | `--worker-threads` | | YAML | `WorkerThreads` | | Environment variable | `EVENTSTORE_WORKER_THREADS` | The option is set to 0 by default, which enables autoconfiguration. The default on previous versions of EventStoreDb was 5 threads. --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/cluster.md' --- # Highly-available cluster EventStoreDB allows you to run more than one node in a cluster for high availability. ::: info Cluster member authentication EventStoreDB starts in secure mode by default, which requires configuration [settings for certificates](security.md#certificates-configuration). Cluster members authenticate each other using the certificate Common Name. All the cluster nodes must have the same common name in their certificates. ::: ## Cluster nodes EventStoreDB clusters follow a "shared nothing" philosophy, meaning that clustering requires no shared disks. Instead, each node has a copy of the data to ensure it is not lost in case of a drive failure or a node crashing. ::: tip Lean more about [node roles](#node-roles). ::: EventStoreDB uses a quorum-based replication model, in which a majority of nodes in the cluster must acknowledge that they have received a copy of the write before the write is acknowledged to the client. This means that to be able to tolerate the failure of *n* nodes, the cluster must be of size *(2n + 1)*. A three node cluster can continue to accept writes if one node is unavailable. A five node cluster can continue to accept writes if two nodes are unavailable, and so forth. ## Cluster size For any cluster configuration, you first need to decide how many nodes you want, provision each node and set the cluster size option on each node. The cluster size is a pre-defined value. The cluster expects the number of nodes to match this predefined number, otherwise the cluster would be incomplete and therefore unhealthy. The cluster cannot be dynamically scaled. If you need to change the number of cluster nodes, the cluster size setting must be changed on all nodes before the new node can join. Use the `ClusterSize` option to tell each cluster node about how many nodes the cluster should have. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--cluster-size` | | YAML | `ClusterSize` | | Environment variable | `EVENTSTORE_CLUSTER_SIZE` | **Default**: `1` (single node, no high-availability). Common values for the `ClusterSize` setting are three or five (to have a majority of two nodes and a majority of three nodes). We recommended setting this to an odd number of nodes to minimise the chance of a tie during elections, which would lengthen the election process. ## Internal communication When setting up a cluster, the nodes must be able to reach each other over both the HTTP channel, and the internal TCP channel. You should ensure that these ports are open on firewalls on the machines and between the machines. Learn more about [replication configuration](networking.md#tcp-configuration) and [HTTP configuration](networking.md#http-configuration) to set up the cluster properly. ## Discovering cluster members Cluster nodes use the gossip protocol to discover each other and elect the cluster leader. Cluster nodes need to know about one another to gossip. To start this process, you provide gossip seeds for each node. Configure cluster nodes to discover other nodes in one of two ways: * [via a DNS entry](#cluster-with-dns) and a well-known [gossip port](#gossip-port) * [via a list of addresses](#cluster-with-gossip-seeds) of other cluster nodes The multi-address DNS name cluster discovery only works for clusters that use certificates signed by a private certificate authority, or run insecure. For other scenarios you need to provide the gossip seed using hostnames of other cluster nodes. ### Cluster with DNS When you tell EventStoreDB to use DNS for its gossip, the server will resolve the DNS name to a list of IP addresses and connect to each of those addresses to find other nodes. This method is very flexible because you can change the list of nodes on your DNS server without changing the cluster configuration. The DNS method is also useful in automated deployment scenarios when you control both the cluster deployment and the DNS server from your infrastructure-as-code scripts. To use DNS discovery, you need to set the `ClusterDns` option to the DNS name that allows making an HTTP call to it. When the server starts, it will attempt to make a gRPC call using the `https://:` URL (`http` if the cluster is insecure). When using a certificate signed by a publicly trusted CA, you'd normally use the wildcard certificate. Ensure that the cluster DNS name fits the wildcard, otherwise the request will fail on SSL check. When using certificates signed by a private CA, the cluster DNS name must be included to the certificate of each node as SAN (subject alternative name). You also need to have the `DiscoverViaDns` option to be set to `true` but it is its default value. | Format | Syntax | |:---------------------|:-------------------------| | Command line | `--cluster-dns` | | YAML | `ClusterDns` | | Environment variable | `EVENTSTORE_CLUSTER_DNS` | **Default**: `fake.dns`, which doesn't resolve to anything. You have to set it to a proper DNS name when used in combination to the DNS discovery (next setting). | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--discover-via-dns` | | YAML | `DiscoverViaDns` | | Environment variable | `EVENTSTORE_DISCOVER_VIA_DNS` | **Default**: `true`, the DNS discovery is enabled by default. It will be used only if the cluster has more than one node. You must set the `ClusterDns` setting to a proper DNS name. When using DNS for cluster gossip, you might need to set the `GossipPort` setting to the HTTP port if the external HTTP port setting is not set to `2113` default port. Refer to [gossip port](#gossip-port) option documentation to learn more. ### Cluster with gossip seeds If you don't want or cannot use the DNS-based configuration, it is possible to tell cluster nodes to call other nodes using their IP addresses. This method is a bit more cumbersome, because each node has to have the list of addresses for other nodes configured, but not its own address. The setting accepts a comma-separated list of IP addresses or host names with their gossip port values. | Format | Syntax | |:---------------------|:-------------------------| | Command line | `--gossip-seed` | | YAML | `GossipSeed` | | Environment variable | `EVENTSTORE_GOSSIP_SEED` | ## Gossip protocol EventStoreDB uses a quorum-based replication model. When working normally, a cluster has one node known as a leader, and the remaining nodes are followers. The leader node is responsible for coordinating writes while it is the leader. Cluster nodes use a consensus algorithm to determine which node should be the leader and which should be followers. EventStoreDB bases the decision as to which node should be the leader on a number of factors. For a cluster node to have this information available to them, the nodes gossip with other nodes in the cluster. Gossip runs over HTTP interfaces of cluster nodes. The gossip protocol configuration can be changed using the settings listed below. Pay attention to the settings related to time, like intervals and timeouts, when running in a cloud environment. ### Gossip port The gossip port is used for constructing the URL for making a gossip request to other nodes that are discovered via DNS. It is not used when using gossip seeds, because in that case the list contains IP addresses and the port. ::: warning Normally, the cluster gossip port is the same as the HTTP port, so you don't need to change this setting. ::: | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--cluster-gossip-port` | | YAML | `ClusterGossipPort` | | Environment variable | `EVENTSTORE_CLUSTER_GOSSIP_PORT` | **Default**: HTTP port ### Gossip interval Cluster nodes try to ensure that the communication with their neighbour nodes is not broken. They use the gossip protocol and call each other after a specified period of time. This period is called the gossip interval. You can change the `GossipInvervalMs` setting so cluster nodes check in with each other more or less frequently. The default value is two seconds (2000 ms). | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--gossip-interval-ms` | | YAML | `GossipIntervalMs` | | Environment variable | `EVENTSTORE_GOSSIP_INTERVAL_MS` | **Default**: `2000` (in milliseconds), which is two seconds. ### Time difference toleration EventStoreDB expects the time on cluster nodes to be in sync within a given tolerance. If different nodes have their clock out of sync for a number of milliseconds that exceeds the value of this setting, the gossip is rejected and the node will not be accepted as a cluster member. | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--gossip-allowed-difference-ms` | | YAML | `GossipAllowedDifferenceMs` | | Environment variable | `EVENTSTORE_GOSSIP_ALLOWED_DIFFERENCE_MS` | **Default**: `60000` (in milliseconds), which is one minute. ### Gossip timeout When nodes call each other using the gossip protocol to understand the cluster status, a busy node might delay the response. When a node is not getting a response from another node, it might consider that other node as dead. Such a situation might trigger the election process. If your cluster network is congested, you might increase the gossip timeout using the `GossipTimeoutMs` setting, so nodes will be more tolerant to delayed gossip responses. The default value is 2.5 seconds (2500 ms). | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--gossip-timeout-ms` | | YAML | `GossipTimeoutMs` | | Environment variable | `EVENTSTORE_GOSSIP_TIMEOUT_MS` | **Default**: `2500` (in milliseconds). ### Gossip on single node You can connect using gossip seeds regardless of whether you have a cluster or not. In the previous versions of EventStoreDB gossip on a single node was disabled. Starting from 21.2 it is enabled by default. ::: warning Please note that the `GossipOnSingleNode` option is deprecated and will be removed in a future version. The gossip endpoint is now unconditionally available for any deployment topology. ::: | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--gossip-on-single-node` | | YAML | `GossipOnSingleNode` | | Environment variable | `EVENTSTORE_GOSSIP_ON_SINGLE_NODE` | **Default**: `true` ### Leader election timeout The leader elections are separate to the node gossip, and are used to elect a node as Leader. In some cases the leader election messages may be delayed, which can result in elections taking longer than they should. If you start seeing election timeouts in the logs or if you have needed to increase the gossip timeout due to a congested network, then you should consider increasing the leader election timeout as well. | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--leader-election-timeout-ms` | | YAML | `LeaderElectionTimeoutMs` | | Environment variable | `EVENTSTORE_LEADER_ELECTION_TIMEOUT_MS` | **Default**: `1000` (in milliseconds). ## Node roles Every node in a stable EventStoreDB deployment settles into one of three roles: Leader, Follower, and ReadOnlyReplica. The cluster is composed of the Leader and Followers. ### Leader The leader ensures that writes are persisted to its own disk, replicated to a majority of cluster nodes, and indexed on the leader so that they can be read from the leader, before acknowledging the write as successful to the client. ### Follower A cluster assigns the follower role based on an election process. A cluster uses one or more nodes with the follower role to form the quorum, or the majority of nodes necessary to confirm that the write is persisted. ### Read-only replica You can add read-only replica nodes, which will not become cluster members and will not take part in elections. Read-only replicas can be used for scaling up reads if you have many catch-up subscriptions and want to off-load cluster members. A cluster asynchronously replicates data one way to a node with the read-only replica role. The read-only replica node is not part of the cluster, so does not add to the replication requirements needed to acknowledge a write. For this reason a node with a read-only replica role does not add much overhead to the other nodes. You need to explicitly configure the node as a read-only replica using this setting: | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--read-only-replica` | | YAML | `ReadOnlyReplica` | | Environment variable | `EVENTSTORE_READ_ONLY_REPLICA` | **Default**: `false`, set to `true` for a read-only replica node. The replica node needs to have the cluster gossip DNS or seed configured. For the gossip seed, use DNS names or IP addresses of all other cluster nodes, except read-only replicas. ## Node priority You can control which clones the cluster promotes with the `NodePriority` setting. The default value is `0`, and the cluster is more likely to promote nodes with higher values. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--node-priority` | | YAML | `NodePriority` | | Environment variable | `EVENTSTORE_NODE_PRIORITY` | **Default**: `0`. ::: tip Changing `NodePriority` does not guarantee that the cluster will not promote the node. It is only one of the criteria that the Election Service considers. ::: --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/db-config.md' --- # Database settings On this page, you find settings that tune the database server behaviour. Only modify these settings if you know what you are doing or when requested by Event Store support personnel. ## Database Settings described below are related to the database behaviour, like the location of the database files, in-memory database, and verification, and caches. ### Database location EventStoreDB has a single database, which is spread across ever-growing number of physical files on the file system. Those files are called chunks and new data is always appended to the end of the latest chunk. When the chunk grows over 256 MiB, the server closes the chunk and opens a new one. Normally, you'd want to keep the database files separated from the OS and other application files. The `Db` setting tells EventStoreDB where to put those chunk files. If the database server doesn't find anything at the specified location, it will create a new database. | Format | Syntax | |:---------------------|:----------------| | Command line | `--db` | | YAML | `Db` | | Environment variable | `EVENTSTORE_DB` | **Default**: the default database location is platform specific. On Windows, the database will be stored in the `data` directory inside the EventStoreDB installation location. On Linux, it will be `/var/lib/eventstore`. ### In-memory database When running EventStoreDB for educational purposes or in an automated test environment, you might want to prevent it from saving any data to the disk. EventStoreDB can keep the data in memory as soon as it has enough available RAM. When you shut down the instance that uses in-memory database, all the data will be lost. | Format | Syntax | |:---------------------|:--------------------| | Command line | `--mem-db` | | YAML | `MemDb` | | Environment variable | `EVENTSTORE_MEM_DB` | **Default**: `false` ### Skip database verification When the database node restarts, it checks the database files to ensure they aren't corrupted. It is a lengthy process and can take hours on a large database. EventStoreDB normally flushes every write to disk, so database files are unlikely to get corrupted. In an environment where nodes restart often for some reason, you might want to disable the database verification to allow faster startup of the node. | Format | Syntax | |:---------------------|:----------------------------| | Command line | `--skip-db-verify` | | YAML | `SkipDbVerify` | | Environment variable | `EVENTSTORE_SKIP_DB_VERIFY` | **Default**: `false` ### Chunk cache You can increase the number of chunk files that are cached if you have free memory on the nodes. The [stats response](../diagnostics/README.md#statistics) contains two fields: `es-readIndex-cachedRecord` and `es-readIndex-notCachedRecord`. Statistic values for those fields tell you how many times a chunk file was retrieved from the cache and from the disk. We expect the two most recent chunks (the current chunk and the previous one) to be most frequently accessed and therefore cached. If you observe that the `es-readIndex-notCachedRecord` stats value gets significantly higher than the `es-readIndex-cachedRecord`, you can try adjusting the chunk cache. The setting `ChunkCacheSize` tells the server how much memory it can use to cache chunks (in bytes). The chunk size is 256 MiB max, so the default cache size (two chunks) is 0.5 GiB. To increase the cache size to four chunks, you can set the value to 1 GiB (`1073741824` bytes). Remember that only the latest chunks get cached. Also consider that the OS has its own file cache and increasing the chunk cache might not bring the desired performance benefit. | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--chunks-cache-size` | | YAML | `ChunksCacheSize ` | | Environment variable | `EVENTSTORE_CHUNKS_CACHE_SIZE` | **Default**: `536871424` You would also need to adjust the `CachedChunks` setting to tell the server how many chunk files you want to keep in cache. For example, to double the number of cached chunks, set the `CachedChunks` setting to `4`. | Format | Syntax | |:---------------------|:---------------------------| | Command line | `--cached-chunks` | | YAML | `CachedChunks ` | | Environment variable | `EVENTSTORE_CACHED_CHUNKS` | **Default**: `-1` (all) ### Prepare and Commit timeouts Having prepare and commit timeouts of 2000 ms (default) means that any write done on the cluster may take up to 2000 ms before the server replies to the client that this write has timed out. Depending on your client operation timeout settings (default is 7 seconds), increasing the timeout may block a client for up to the minimum of those two timeouts which may reduce the throughput if set to a large value. The server also needs to keep track of all these writes and retries in memory until the time out is over. If a large number of them accumulate, it may slow things down. | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--prepare-timeout-ms` | | YAML | `PrepareTimeoutMs` | | Environment variable | `EVENTSTORE_PREPARE_TIMEOUT_MS` | **Default**: `2000` (in milliseconds) | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--commit-timeout-ms` | | YAML | `CommitTimeoutMs` | | Environment variable | `EVENTSTORE_COMMIT_TIMEOUT_MS` | **Default**: `2000` (in milliseconds) ### TCP read timeout Applies to reads received via the TCP client API. When a read has been in the server queue for longer than this, it will be discarded without being executed. If your TCP clients are configured to timeout after X milliseconds, it is advisable to set this server option to be the same, so that the server will not execute reads that the client is no longer waiting for. This does not affect gRPC clients. For gRPC the server-side discarding is driven by the deadline on the read itself without requiring server configuration. | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--tcp-read-timeout-ms` | | YAML | `TcpReadTimeoutMs` | | Environment variable | `EVENTSTORE_TCP_READ_TIMEOUT_MS` | **Default**: `10000` (in milliseconds) ### Disable flush to disk :::warning Using this option might cause data loss. ::: This will prevent EventStoreDB from forcing the flush to disk after writes. Please note that this is unsafe in case of a power outage. With this option enabled, EventStoreDB will still write data to the disk at the application level but not necessarily at the OS level. Usually, the OS should flush its buffers at regular intervals or when a process exits but it is something that's opaque to EventStoreDB. | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--unsafe-disable-flush-to-disk` | | YAML | `UnsafeDisableFlushToDisk` | | Environment variable | `EVENTSTORE_UNSAFE_DISABLE_FLUSH_TO_DISK` | **Default**: `false` ### Minimum flush delay The minimum flush delay in milliseconds. | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--min-flush-delay-ms` | | YAML | `MinFlushDelayMs` | | Environment variable | `EVENTSTORE_MIN_FLUSH_DELAY_MS ` | **Default**: `2` (ms) ## Threading ### Worker threads A variety of undifferentiated work is carried out on general purpose worker threads, including sending over a network, authentication, and completion of HTTP requests. Increasing the number of threads beyond that necessary to satisfy the workload generated by other components in the system may result in additional unnecessary context switching overhead. | Format | Syntax | |:---------------------|:----------------------------| | Command line | `--worker-threads` | | YAML | `WorkerThreads` | | Environment variable | `EVENTSTORE_WORKER_THREADS` | **Default**: `5` ### Reader threads count Reader threads are used for all read operations on data files - whether the requests originate from the client or internal requests to the database. There are a number of things that cause operations to be dispatched to reader threads, including: * All index reads, including those to service stream and event number-based reads and for idempotent handling of write operations when an expected version is specified. * Data to service requests originating from either the HTTP or client APIs. * Projections needing to read data for processing. * Authorization needing to read access control lists from stream metadata. | Format | Syntax | |:---------------------|:----------------------------------| | Command line | `--reader-threads-count` | | YAML | `ReaderThreadsCount` | | Environment variable | `EVENTSTORE_READER_THREADS_COUNT` | **Default**: `4` Reads are queued until a reader thread becomes available to service them. If an operation doesn't complete within an internal deadline window, a disk operation is not dispatched by the worker thread which processes the operation. The size of read operations is dependent on the size of the events appended, not on the database chunk size, which has a fixed logical size. Larger reads (subject to the operating system page size) result in more time being spent in system calls and less availability of reader threads. A higher reader count can be useful, if disks are able to support more concurrent operations. Context switching incurs additional costs in terms of performance. If disks are already saturated, adding more reader threads can exacerbate that issue and lead to more failed requests. Increasing the count of reader threads can improve performance up to a point, but it is likely to rapidly tail off once that limit is reached. --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/indexes.md' --- # Indexes ## Indexing EventStoreDB stores indexes separately from the main data files, accessing records by stream name. ### Overview EventStoreDB creates index entries as it processes commit events. It holds these in memory (called *memtables*) until it reaches the `MaxMemTableSize` and then persisted on disk in the *index* folder along with `.bloomfilter` files and an index map file. The index files are uniquely named, and the index map file called *indexmap*. The index map describes the order and the level of the index file as well as containing the data checkpoint for the last written file, the version of the index map file and a checksum for the index map file. The logs refer to the index files as a *PTable*. Indexes are sorted lists based on the hashes of stream names. To speed up seeking the correct location in the file of an entry for a stream, EventStoreDB keeps midpoints to relate the stream hash to the physical offset in the file. As EventStoreDB saves more files, they are automatically merged together whenever there are 2 or more files at the same level into a single file at the next level. Each index entry is 24 bytes and the index file size is approximately 24Mb per 1M events. The Bloom filter files are approximately 1% of the size of the rest of the index. Level 0 is the level of the *memtable* that is kept in memory. Generally there is only 1 level 0 table unless an ongoing merge operation produces multiple level 0 tables. Assuming the default `MaxMemTableSize` of 1M, the index files by level are: | Level | Number of entries | Size | |-------|-------------------|----------------| | 1 | 1M | 24MB | | 2 | 2M | 48MB | | 3 | 4M | 96MB | | 4 | 8M | 192MB | | 5 | 16M | 384MB | | 6 | 32M | 768MB | | 7 | 64M | 1536MB | | 8 | 128M | 3072MB | | n | 2^(n-1) \* 1M | 2^(n-1) \* 24Mb | Each index entry is 24 bytes and the index file size is approximately 24Mb per M events. ## Indexing in depth For general operation of EventStoreDB the following information is not critical but useful for developers wanting to make changes in the indexing subsystem and for understanding crash recovery and tuning scenarios. ### Index map files *Indexmap* files are text files made up of line delimited values. The line delimiter varies based on operating system, so while you can consider *indexmap* files valid when transferred between operating systems, if you make changes to fix an issue (for example, disk corruption) it is best to make them on the same operating system as the cluster. The *indexmap* structure is as follows: * `hash` - an md5 hash of the rest of the file * `version` - the version of the *indexmap* file * `checkpoint` - the maximum prepare/commit position of the persisted *ptables* * `maxAutoMergeLevel` - either the value of `MaxAutoMergeLevel` or `int32.MaxValue` if it was not set. This is primarily used to detect increases in `MaxAutoMergeLevel`, which is not supported. * `ptable`,`level`,`index`- List of all the *ptables* used by this index map with the level of the *ptable* and it's order. EventStoreDB writes *indexmap* files to a temporary file and then deletes the original and renames the temporary file. EventStoreDB attempts this 5 times before failing. Because of the order, this operation can only fail if there is an issue with the underlying file system or the disk is full. This is a 2 phase process, and in the unlikely event of a crash during this process, EventStoreDB recovers by rebuilding the indexes using the same process used if it detects corrupted files during startup. ### Writing and merging of index files Merging *ptables*, updating the *indexmap* and persisting *memtable* operations happen on a background thread. These operations are performed on a single thread with any additional operations queued and completed later. EventStoreDB runs these operations on a thread pool thread rather than a dedicated thread. Generally there is only one operation queued at a time, but if merging to *ptables* at one level causes 2 tables to be available at the next level, then the next merge operation is immediately queued. While merge operations are in progress, if EventStoreDB is appending large numbers of events, it may queue 1 or more *memtables* for persistence. The number of pending operations is logged. For safety *ptables* EventStoreDB is currently merging are only deleted after the new *ptable* has persisted and the *indexmap* updated. In the event of a crash, EventStoreDB recovers by deleting any files not in the *indexmap* and reindexing from the prepare/commit position stored in the *indexmap* file. ### Manual merging If you have set the maximum level ([`MaxAutoMergeIndexLevel`](#auto-merge-index-level)) for automatically merging indexes, then you need to trigger merging indexes above this level manually by using the `/admin/mergeindexes` endpoint, or the es-cli tool that is available with commercial support. Triggering a manual merge causes EventStoreDB to merge all tables that have a level equal to the maximum merge level or above into a single table. If there is only 1 table at the maximum level or above, no merge is performed. ### Stream existence filter The *Stream Existence Filter* is a pair of files in the index directory. * `/index/stream-existence/streamExistenceFilter.chk` * `/index/stream-existence/streamExistenceFilter.dat` It is made up of a persisted Bloom filter (the `.dat` file), and a checkpoint (the `.chk` file) that records where in the log the filter has processed up to. As per the documented backup procedure, the checkpoint needs to be backed up before the dat file. The Stream Existence Filter is used when reading and writing to quickly determine whether a stream *might exist* or *definitely does not exist*. If a stream definitely does not exist then EventStoreDB can skip looking through the index to find information about it. This is particularly useful when creating new streams (i.e. appending to stream that did not previously exist). Additional information can be found in this [`blog post`](https://www.eventstore.com/blog/bloom-filters). ## Configuration options The configuration options that affect indexing are: | Option | What's it for | |:-------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------| | [`Index`](#index-location) | Where the indexes are stored | | [`MaxMemTableSize`](#memtable-size) | How many entries to have in memory before writing out to disk | | [`IndexCacheDepth`](#index-cache-depth) | Sets the minimum number of midpoints to calculate for an index file | | [`SkipIndexVerify`](#skip-index-verification) | Tells the server to not verify indexes on startup | | [`MaxAutoMergeIndexLevel`](#auto-merge-index-level) | The maximum level of index file to merge automatically before manual merge | | [`StreamExistenceFilterSize`](#stream-existence-filter-size) | Size in bytes of the stream existence filter | | [`IndexCacheSize`](#index-cache-size) | Maximum number of entries in each index LRU cache | | [`UseIndexBloomFilters`](#use-index-bloom-filters) | Feature flag which can be used to disable the index Bloom filters | Read more below to understand these options better. ### Index location | Format | Syntax | |:---------------------|:-------------------| | Command line | `--index` | | YAML | `Index` | | Environment variable | `EVENTSTORE_INDEX` | **Default**: data files location `Index` affects the location of the index files. We recommend you place index files on a separate drive to avoid competition for IO between the data, index and log files. ### Memtable size | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--max-mem-table-size` | | YAML | `MaxMemTableSize` | | Environment variable | `EVENTSTORE_MAX_MEM_TABLE_SIZE` | **Default**: `1000000` `MaxMemTableSize` affects disk IO when EventStoreDB writes files to disk, index seek time and database startup time. The default size is a good tradeoff between low disk IO and startup time. Increasing the `MaxMemTableSize` results in longer database startup time because a node has to read through the data files from the last position in the `indexmap` file and rebuild the in memory index table before it starts. Increasing `MaxMemTableSize` also decreases the number of times EventStoreDB writes index files to disk and how often it merges them together, which increases IO operations. It also reduces the number of seek operations when stream entries span multiple files as EventStoreDB needs to search each file for the stream entries. This affects streams written to over longer periods of time more than streams written to over a shorter time, where time is measured by the number of events created, not time passed. This is because streams written to over longer time periods are more likely to have entries in multiple index files. ### Index cache depth | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--index-cache-depth` | | YAML | `IndexCacheDepth` | | Environment variable | `EVENTSTORE_INDEX_CACHE_DEPTH` | **Default**: `16` `IndexCacheDepth` affects how many midpoints EventStoreDB calculates for an index file which affects file size slightly, but can affect lookup times significantly. Looking up a stream entry in a file requires a binary search on the midpoints to find the nearest midpoint, and then a seek through the entries to find the entry or entries that match. Increasing this value decreases the second part of the operation and increase the first for extremely large indexes. **The default value of 16** results in files up to about 1.5 GB in size being fully searchable through midpoints. After that a maximum distance between midpoints of 4096 bytes for the seek, which is buffered from disk, up to a maximum level of 2 TB where the seek distance starts to grow. Reducing this value can relieve a small amount of memory pressure in highly constrained environments. Increasing it causes index files larger than 1.5 GB, and less than 2 TB to have more dense midpoint populations which means the binary search is not used for long before switching back to scanning the entries between. The maximum number of entries scanned in this way is `distance/24b`, so with the default setting and a 2TB index file this is approximately 170 entries. Most clusters should not need to change this setting. ### Skip index verification | Format | Syntax | |:---------------------|:-------------------------------| | Command line | `--skip-index-verify` | | YAML | `SkipIndexVerify` | | Environment variable | `EVENTSTORE_SKIP_INDEX_VERIFY` | **Default**: `false` `SkipIndexVerify` skips reading and verification of index file hashes during startup. Instead of recalculating midpoints when EventStoreDB reads the file, it reads the midpoints directly from the footer of the index file. You can set `SkipIndexVerify` to `true` to reduce startup time in exchange for the acceptance of a small risk that the index file becomes corrupted. This corruption could lead to a failure if you read the corrupted entries, and a message saying the index needs to be rebuilt. You can safely disable this setting for ZFS on Linux as the filesystem takes care of file checksums. In the event of corruption indexes will be rebuilt by reading through all the chunk files and recreating the indexes from scratch. ### Auto-merge index level | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--max-auto-merge-index-level` | | YAML | `MaxAutoMergeIndexLevel` | | Environment variable | `EVENTSTORE_MAX_AUTO_MERGE_INDEX_LEVEL` | **Default**: `2147483647` `MaxAutoMergeIndexLevel` allows you to specify the maximum index file level to automatically merge. By default EventStoreDB merges all levels. Depending on the specification of the host running EventStoreDB, at some point index merges will use a large amount of disk IO. For example: > Merging 2 level 7 files results in at least 3072 MB reads (2 \* 1536 MB), and 3072 MB writes while merging 2 level 8 files together results in at least 6144 MB reads (2 \* 3072 MB) and 6144 MB writes. Setting `MaxAutoMergeLevel` to 7 allows all levels up to and including level 7 to be automatically merged, but to merge the level 8 files together, you need to trigger a manual merge. This manual merge allows better control over when these larger merges happen and which nodes they happen on. Due to the replication process, all nodes tend to merge at about the same time. ### Stream existence filter size | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--stream-existence-filter-size` | | YAML | `StreamExistenceFilterSize` | | Environment variable | `EVENTSTORE_STREAM_EXISTENCE_FILTER_SIZE` | **Default**: `256000000` `StreamExistenceFilterSize` is the amount of memory & disk space, in bytes, to use for the stream existence filter. This should be set to roughly the maximum number of streams you expect to have in your database, i.e if you expect to have a max of 500 million streams, use a value of 500000000. The value you select should also fit entirely in memory to avoid any performance degradation. Use 0 to disable the filter. Upgrading to a version of EventStoreDB that supports the Stream Existence Filter requires the filter to be built - unless it is disabled. This will take approximately as long as it takes to read through the whole index. Resizing the filter will also cause a full rebuild of the filter. ### Index cache size | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--index-cache-size` | | YAML | `IndexCacheSize` | | Environment variable | `EVENTSTORE_INDEX_CACHE_SIZE` | **Default**: `0` `IndexCacheSize` is the maximum number of entries in each index LRU cache. The cache size is set to 0 (off) by default because it has an associated memory overhead and can be detrimental to workloads that produce a lot of cache misses. The cache is, however, well suited to read-heavy workloads of long-lived streams. The index LRU cache is only created for index files that have Bloom filters. ### Use index Bloom filters | Format | Syntax | |:---------------------|:-------------------------------------| | Command line | `--use-index-bloom-filters` | | YAML | `UseIndexBloomFilters` | | Environment variable | `EVENTSTORE_USE_INDEX_BLOOM_FILTERS` | **Default**: `true` `UseIndexBloomFilters` is a feature flag which can be used to disable the index Bloom filters. This should not be necessary and this flag will be removed in a future release, but is provided for safety since the index Bloom filters are a new feature. Please contact EventStore if you discover some need to disable this feature. Unless this flag is set to false, EventStoreDB creates a `.bloomfilter` file for each new PTable. The Bloom filter describes which streams are present in the PTable. This speeds up stream reads since EventStoreDB can avoid searching in index files do not contain the stream. Note that immediately after upgrading to a version of EventStoreDB that produces index Bloom filters, no Bloom filters will yet exist. Either wait for new PTables to be produced with Bloom filters in the natural course of writing/merging/scavenging PTables, or rebuild the index for immediate generation. ## Tuning indexes For most EventStoreDB clusters, the default settings are enough to give consistent and good performance. For clusters with larger numbers of events, or those that run in constrained environments the configuration options allow for some tuning to meet operational constraints. The most common optimization needed is to set a `MaxAutoMergeLevel` to avoid large merges occurring across all nodes at approximately the same time. Large index merges use a lot of IOPS and in IOPS constrained environments it is often desirable to have better control over when these happen. Because increasing this value requires an index rebuild you should start with a higher value and decrease until the desired balance between triggering manual merges (operational cost) and automatic merges (IOPS) cost. The exact value to set this varies between environments due to IOPS generated by other operations such as read and write load on the cluster. For example: > A cluster with 3000 256b IOPS can read/write about 0.73Gb/sec (This level of IOPS represents a small cloud instance). Assuming sustained read/write throughput of 0.73Gb/s. When an index merge of level 7 or above starts, it consumes as many IOPS up to all on the node until it completes. Because EventStoreDB has a shared nothing architecture for clustering this operation is likely to cause all nodes to appear to stall simultaneously as they all try and perform an index merge at the same time. By setting `MaxAutoMergeLevel` to 6 or below you can avoid this, and you can run the merge on each node individually keeping read/write latency in the cluster consistent. --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/networking.md' --- # Network configuration EventStoreDB provides two interfaces: * HTTP(S) for gRPC communication and REST APIs * TCP for cluster replication (internal) and legacy clients (external) Nodes in the cluster replicate with each other using the TCP protocol, but use gRPC for [discovering other cluster nodes](cluster.md#discovering-cluster-members). Server nodes separate internal and external TCP communication explicitly, but use a single HTTP binding. Replication between the cluster nodes is considered internal and all the TCP clients that connect to the database are external. EventStoreDB allows you to separate network configurations for internal and external communication. For example, you can use different network interfaces on the node. All the internal communication can go over the isolated private network but access for external clients can be configured on another interface, which is connected to a more open network. You can set restrictions on those external interfaces to make your deployment more secure. For gRPC and HTTP, there's no internal vs external separation of traffic. ## HTTP configuration HTTP is the primary protocol for EventStoreDB. It is used in gRPC communication and HTTP APIs (management, gossip and diagnostics). Unlike for [TCP protocol](#tcp-configuration), there is no separation between internal and external communication. The HTTP endpoint always binds to the IP address configured in the `NodeIp` setting (previously referred to as `ExtIp`). | Format | Syntax | |:---------------------|:---------------------| | Command line | `--node-ip` | | YAML | `NodeIp` | | Environment variable | `EVENTSTORE_NODE_IP` | When the `NodeIp` setting is not provided, EventStoreDB will use the first available non-loopback address. You can also bind HTTP to all available interfaces using `0.0.0.0` as the setting value. If you do that, you'd need to configure the `NodeHostAdvertiseAs` (previously `ExtHostAdvertiseAs`) setting (read more [here](#network-address-translation)), since `0.0.0.0` is not a valid IP address to connect from the outside world. ::: warning Please note that the `ExtIp` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodeIp` parameter instead. ::: The default HTTP port is `2113`. Depending on the [security settings](security.md) of the node, it either responds over plain HTTP or via HTTPS. There is no HSTS redirect, so if you try reaching a secure node via HTTP, you can get an empty response. You can change the HTTP port using the `NodePort` setting (previously `HttpPort` setting) : | Format | Syntax | |:---------------------|:-----------------------| | Command line | `--node-port` | | YAML | `NodePort` | | Environment variable | `EVENTSTORE_NODE_PORT` | **Default**: `2113` ::: warning Please note that the `HttpPort` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodePort` parameter instead. ::: If your network setup requires any kind of IP address, DNS name and port translation for internal or external communication, you can use available [address translation](#network-address-translation) settings. ### Keep-alive pings The reliability of the connection between the client application and database is crucial for the stability of the solution. If the network is not stable or has some periodic issues, the client may drop the connection. Stability is essential for the [stream subscriptions](@clients/grpc/subscriptions.md) where a client is listening to database notifications. Having an existing connection open when an app resumes activity allows for the initial gRPC calls to be made quickly, without any delay caused by the reestablished connection. EventStoreDB supports the built-in gRPC mechanism for keeping the connection alive. If the other side does not acknowledge the ping within a certain period, the connection will be closed. Note that pings are only necessary when there's no activity on the connection. Keepalive pings are enabled by default, with the default interval set to 10 seconds. The default value is based on the [gRPC proposal](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md#extending-for-basic-health-checking) that suggests 10 seconds as the minimum. It's a compromise value to ensure that the connection is open and not making too many redundant network calls. You can customise the following Keepalive settings: #### KeepAliveInterval After a duration of `keepAliveInterval` (in milliseconds), if the server doesn't see any activity, it pings the client to see if the transport is still alive. | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--keep-alive-interval` | | YAML | `KeepAliveInterval` | | Environment variable | `EVENTSTORE_KEEP_ALIVE_INTERVAL` | **Default**: `10000` (ms, 10 sec) #### KeepAliveTimeout After having pinged for keepalive check, the server waits for a duration of `keepAliveTimeout` (in milliseconds). If the connection doesn't have any activity even after that, it gets closed. | Format | Syntax | |:---------------------|:--------------------------------| | Command line | `--keep-alive-timeout` | | YAML | `KeepAliveTimeout` | | Environment variable | `EVENTSTORE_KEEP_ALIVE_TIMEOUT` | **Default**: `10000` (ms, 10 sec) As a general rule, we do not recommend putting EventStoreDB behind a load balancer. However, if you are using it and want to benefit from the Keepalive feature, then you should make sure if the compatible settings are properly set. Some load balancers may also override the Keepalive settings. Most of them require setting the idle timeout larger/longer than the `keepAliveTimeout`. We suggest checking the load balancer documentation before using Keepalive pings. ### AtomPub The AtomPub application protocol over HTTP is disabled by default since v20. We plan to deprecate the AtomPub support in the future versions, but we aim to provide a replacement before we finally deprecate AtomPub. In Event Store Cloud, the AtomPub protocol is enabled. For self-hosted instances, use the configuration setting to enable AtomPub. | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--enable-atom-pub-over-http` | | YAML | `EnableAtomPubOverHttp` | | Environment variable | `EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP` | **Default**: `false` (AtomPub is disabled) ### Kestrel Settings It's generally not expected that you'll need to update the Kestrel configuration that EventStoreDB has set by default, but it's good to know that you can update the following settings if needed. Kestrel uses the `kestrelsettings.json` configuration file. This file should be located in the [default configuration directory](README.md#configuration-file). #### MaxConcurrentConnections Sets the maximum number of open connections. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentconnections?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.MaxConcurrentConnections` in the settings file. #### MaxConcurrentUpgradedConnections Sets the maximum number of open, upgraded connections. An upgraded connection is one that has been switched from HTTP to another protocol, such as WebSockets. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.kestrelserverlimits.maxconcurrentupgradedconnections?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.MaxConcurrentUpgradedConnections` in the settings file. #### Http2 InitialConnectionWindowSize Sets how much request body data the server is willing to receive and buffer at a time aggregated across all requests (streams) per connection. Note requests are also limited by `KestrelInitialStreamWindowSize` The value must be greater than or equal to 65,535 and less than 2^31. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialconnectionwindowsize?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.Http2.InitialConnectionWindowSize` in the settings file. #### Http2 InitialStreamWindowSize Sets how much request body data the server is willing to receive and buffer at a time per stream. Note connections are also limited by `KestrelInitialConnectionWindowSize` Value must be greater than or equal to 65,535 and less than 2^31. See the docs [here](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.server.kestrel.core.http2limits.initialstreamwindowsize?view=aspnetcore-5.0). This is configured with `Kestrel.Limits.Http2.InitialStreamWindowSize` in the settings file. ## TCP configuration The TCP protocol is used internally for cluster nodes to replicate with each other. It happens over the [internal](#internal) TCP communication. In addition, you can enable [external](#external) TCP if you use the TCP client library in your applications. ### Internal Internal TCP binds to the IP address specified in the `ReplicationIp` setting (previously `IntIp`). It must be configured if you run a multi-node cluster. By default, EventStoreDB binds its internal networking on the loopback interface only (`127.0.0.1`). You can change this behaviour and tell EventStoreDB to listen on a specific internal IP address. To do that set the `ReplicationIp` to `0.0.0.0` or the IP address of the network interface. | Format | Syntax | |:---------------------|:---------------------------| | Command line | `--replication-ip` | | YAML | `ReplicationIp` | | Environment variable | `EVENTSTORE_REPLICATION_IP`| **Default**: `127.0.0.1` (loopback). If you keep this setting to its default value, cluster nodes won't be able to talk to each other. ::: warning Please note that the `IntIp` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `ReplicationIp` parameter instead. ::: By default, EventStoreDB uses port `1112` for internal TCP. You can change this by specifying the `ReplicationPort` setting (previously `IntTcpPort` setting). | Format | Syntax | |:---------------------|:-----------------------------| | Command line | `--replication-port` | | YAML | `ReplicationPort` | | Environment variable | `EVENTSTORE_REPLICATION_PORT`| **Default**: `1112` ::: warning Please note that the `IntTcpPort` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `ReplicationPort` parameter instead. ::: ### External ::: warning EventStoreDB v23.10 is the last version that supports external TCP for all users. It is removed in future versions. If you need the TCP client protocol support, please contact Event Store. ::: By default, TCP protocol is not exposed externally. If you use a TCP client library in your applications, you need to enable external TCP explicitly using the setting below. | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--enable-external-tcp` | | YAML | `EnableExternalTcp` | | Environment variable | `EVENTSTORE_ENABLE_EXTERNAL_TCP` | **Default**: `false`, TCP is disabled externally. When enabled, the external TCP will be exposed on the `NodeIp` address (described [here](#http-configuration)) using port `1113`. You can change the external TCP port using the `NodeTcpPort` setting. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--node-tcp-port` | | YAML | `NodeTcpPort` | | Environment variable | `EVENTSTORE_NODE_TCP_PORT`| **Default**: `1113` ::: warning Please note that the `ExtTcpPort` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodeTcpPort` parameter instead. ::: ### Security When the node is secured (by default), all the TCP traffic will use TLS. You can disable TLS for TCP internally and externally using the settings described below. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-internal-tcp-tls` | | YAML | `DisableInternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_INTERNAL_TCP_TLS` | **Default**: `false` | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-external-tcp-tls` | | YAML | `DisableExternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_EXTERNAL_TCP_TLS` | **Default**: `false` If your network setup requires any kind of IP address, DNS name and port translation for internal or external communication, you can use available [address translation](#network-address-translation) settings. ## Network address translation Due to NAT (network address translation), or other reasons a node may not be bound to the address it is reachable from other nodes. For example, the machine has an IP address of `192.168.1.13`, but the node is visible to other nodes as `10.114.12.112`. Options described below allow you to tell the node that even though it is bound to a given address it should not gossip that address. When returning links over HTTP, EventStoreDB will also use the specified addresses instead of physical addresses, so the clients that use HTTP can follow those links. Another case when you might want to specify the advertised address although there's no address translation involved. When you configure EventStoreDB to bind to `0.0.0.0`, it will use the first non-loopback address for gossip. It might or might not be the address you want it to use. Whilst the best way to avoid such a situation is to configure the binding properly using the `NodeIp` and `ReplicationIp` settings, you can also use address translation setting with the correct IP address or DNS name. Also, even if you specified the `NodeIp` and `ReplicationIp` settings in the configuration, you might still want to override the advertised address if you want to use hostnames and not IP addresses. That might be needed when running a secure cluster with certificates that only contain DNS names of the nodes. The only place where these settings make any effect is the [gossip](cluster.md#gossip-protocol) endpoint response. ## HTTP translations By default, a cluster node will advertise itself using `NodeIp` and `NodePort`. You can override the advertised HTTP port using the `NodePortAdvertiseAs` setting (previously `HttpPortAdvertiseAs` setting). | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--node-port-advertise-as` | | YAML | `NodePortAdvertiseAs` | | Environment variable | `EVENTSTORE_NODE_PORT_ADVERTISE_AS`| ::: warning Please note that the `HttpPortAdvertiseAs` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodePortAdvertiseAs` parameter instead. ::: If you want the node to advertise itself using the hostname rather than its IP address, use the `NodeHostAdvertiseAs` setting (previously `ExtHostAdvertiseAs` setting). | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--node-host-advertise-as` | | YAML | `NodeHostAdvertiseAs` | | Environment variable | `EVENTSTORE_NODE_HOST_ADVERTISE_AS` | ::: warning Please note that the `ExtHostAdvertiseAs` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodeHostAdvertiseAs` parameter instead. ::: ### TCP translations Both internal and external TCP ports can be advertised using custom values: | Format | Syntax | |:---------------------|:----------------------------------------------| | Command line | `--replication-tcp-port-advertise-as` | | YAML | `ReplicationTcpPortAdvertiseAs` | | Environment variable | `EVENTSTORE_REPLICATION_TCP_PORT_ADVERTISE_AS`| | Format | Syntax | |:---------------------|:---------------------------------------| | Command line | `--node-tcp-port-advertise-as` | | YAML | `NodeTcpPortAdvertiseAs` | | Environment variable | `EVENTSTORE_NODE_TCP_PORT_ADVERTISE_AS`| ::: warning Please note that the `IntTcpPortAdvertiseAs` and `ExtTcpPortAdvertiseAs` parameters have been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `ReplicationTcpPortAdvertiseAs` and `NodeTcpPortAdvertiseAs` parameters instead, respectively. ::: If you want to change how the node TCP address is advertised internally, use the `ReplicationHostAdvertiseAs` setting (previously `IntHostAdvertiseAs` setting). You can use an IP address or a hostname. | Format | Syntax | |:---------------------|:-------------------------------------------| | Command line | `--replication-host-advertise-as` | | YAML | `ReplicationHostAdvertiseAs` | | Environment variable | `EVENTSTORE_REPLICATION_HOST_ADVERTISE_AS` | ::: warning Please note that the `IntHostAdvertiseAs` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `ReplicationHostAdvertiseAs` parameter instead. ::: Externally, TCP is advertised using the address specified in the `NodeIp` or `NodeHostAdvertiseAs` (as for HTTP). ### Advertise to clients In some cases, the cluster needs to advertise itself to clients using a completely different set of addresses and ports. Usually, you need to do it because addresses and ports configured for the HTTP protocol are not available as-is to the outside world. One of the examples is running a cluster in Docker Compose. In such environment, HTTP uses internal hostnames in the Docker network, which isn't accessible on the host. So, in order to connect to the cluster from the host machine, you need to use `localhost` and translated HTTP ports to reach the cluster nodes. To configure how the cluster nodes advertise to clients, use the `Advertise<*>ToClient` settings listed below. Specify the advertised hostname or IP address: | Format | Syntax | |:---------------------|:-----------------------------------------| | Command line | `--advertise-host-to-client-as` | | YAML | `AdvertiseHostToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_HOST_TO_CLIENT_AS` | Specify the advertised HTTP(S) port (previously `AdvertiseHttpPortToClientAs` setting): | Format | Syntax | |:---------------------|:----------------------------------------------| | Command line | `--advertise-node-port-to-client-as` | | YAML | `AdvertiseNodePortToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_NODE_PORT_TO_CLIENT_AS` | ::: warning Please note that the `AdvertiseHttpPortToClientAs` parameter has been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `AdvertiseNodePortToClientAs` parameter instead. ::: Specify the advertised TCP port (only if external TCP is enabled): | Format | Syntax | |:---------------------|:---------------------------------------------| | Command line | `--advertise-tcp-port-to-client-as` | | YAML | `AdvertiseTcpPortToClientAs` | | Environment variable | `EVENTSTORE_ADVERTISE_TCP_PORT_TO_CLIENT_AS` | ## Heartbeat timeouts EventStoreDB uses heartbeats over all TCP connections to discover dead clients and nodes. Heartbeat timeouts should not be too short, as short timeouts will produce false positives. At the same time, setting too long timeouts will prevent discovering dead nodes and clients in time. Each heartbeat has two points of configuration. The first is the *interval*, this represents how often the system should consider a heartbeat. EventStoreDB doesn't send a heartbeat for every interval, but only if it has not heard from a node within the configured interval. On a busy cluster, you may never see any heartbeats. The second point of configuration is the *timeout*. This determines how long EventStoreDB server waits for a client or node to respond to a heartbeat request. Different environments need different values for these settings. The defaults are likely fine on a LAN. If you experience frequent elections in your environment, you can try to increase both interval and timeout, for example: * An interval of 5000ms. * A timeout of 1000ms. ::: tip If in doubt, choose higher numbers. This adds a small period of time to discover a dead client or node and is better than the alternative, which is false positives. ::: Replication/Internal TCP heartbeat (between cluster nodes): | Format | Syntax | |:---------------------|:-------------------------------------------| | Command line | `--replication-heartbeat-interval` | | YAML | `ReplicationHeartbeatInterval` | | Environment variable | `EVENTSTORE_REPLICATION_HEARTBEAT_INTERVAL`| **Default**: `700` (ms) | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--replication-heartbeat-timeout` | | YAML | `ReplicationHeartbeatTimeout` | | Environment variable | `EVENTSTORE_REPLICATION_HEARTBEAT_TIMEOUT`| **Default**: `700` (ms) ::: warning Please note that the `IntTcpHeartbeatInterval` and `IntTcpHeartbeatTimeout` parameters have been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `ReplicationHeartbeatInterval` and `ReplicationHeartbeatTimeout` parameters instead, respectively. ::: Node/External TCP heartbeat (between client and server): | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--node-heartbeat-interval` | | YAML | `NodeHeartbeatInterval` | | Environment variable | `EVENTSTORE_NODE_HEARTBEAT_INTERVAL`| **Default**: `2000` (ms) | Format | Syntax | |:---------------------|:-----------------------------------| | Command line | `--node-heartbeat-timeout` | | YAML | `NodeHeartbeatTimeout` | | Environment variable | `EVENTSTORE_NODE_HEARTBEAT_TIMEOUT`| **Default**: `1000` (ms) ::: warning Please note that the `ExtTcpHeartbeatInterval` and `ExtTcpHeartbeatTimeout` parameters have been deprecated as of version 23.10.0 and will be removed in future versions. It is recommended to use the `NodeHeartbeatInterval` and `NodeHeartbeatTimeout` parameters instead, respectively. ::: ### gRPC heartbeats For the gRPC heartbeats, EventStoreDB and its gRPC clients use the protocol feature called *Keepalive ping*. Read more about it on the [HTTP configuration page](#keep-alive-pings). ## Exposing endpoints If you need to disable some HTTP endpoints on the external HTTP interface, you can change some settings below. It is possible to disable the Admin UI, stats and gossip port to be exposed externally. You can disable the Admin UI on external HTTP by setting `AdminOnExt` setting to `false`. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--admin-on-ext` | | YAML | `AdminOnExt` | | Environment variable | `EVENTSTORE_ADMIN_ON_EXT` | **Default**: `true`, Admin UI is enabled on the external HTTP. Exposing the `stats` endpoint externally is required for the Admin UI and can also be useful if you collect stats for an external monitoring tool. | Format | Syntax | |:---------------------|:--------------------------| | Command line | `--stats-on-ext` | | YAML | `StatsOnExt` | | Environment variable | `EVENTSTORE_STATS_ON_EXT` | **Default**: `true`, stats endpoint is enabled on the external HTTP. You can also disable the gossip protocol in the external HTTP interface. If you do that, ensure that the internal interface is properly configured. Also, if you use [gossip with DNS](cluster.md#cluster-with-dns), ensure that the [gossip port](cluster.md#gossip-port) is set to the internal HTTP port. | Format | Syntax | |:---------------------|:---------------------------| | Command line | `--gossip-on-ext` | | YAML | `GossipOnExt` | | Environment variable | `EVENTSTORE_GOSSIP_ON_EXT` | **Default**: `true`, gossip is enabled on the external HTTP. --- --- url: 'https://docs.kurrent.io/server/v23.10/configuration/security.md' --- # Security For production use it is important to configure EventStoreDB security features to prevent unauthorised access to your data. Security features of EventStoreDB include: * [User management](#authentication) for allowing users with different roles to access the database * [Access Control Lists](#access-control-lists) to restrict access to specific event streams * Encryption in-flight using HTTPS and TLS ## Protocol security EventStoreDB supports gRPC and the proprietary TCP protocol for high-throughput real-time communication. It also has some HTTP endpoints for the management operations like scavenging, creating projections and so on. EventStoreDB also uses HTTP for the gossip seed endpoint, both internally for the cluster gossip, and internally for clients that connect to the cluster using discovery mode. All those protocols support encryption with TLS and SSL. Each protocol has its own security configuration, but you can only use one set of certificates for both TLS and HTTPS. The protocol security configuration depends a lot on the deployment topology and platform. We have created an interactive [configuration tool](../quick-start/installation.md), which also has instructions on how to generate and install the certificates and configure EventStoreDB nodes to use them. ## Security options Below you can find more details about each of the available security options. ### Running without security Unlike previous versions, EventStoreDB v20+ is secure by default. It means that you have to supply valid certificates and configuration for the database node to work. We realise that many users want to try out the latest version with their existing applications, and also run a previous version of EventStoreDB without any security in their internal networks. For this to work, you can use the `Insecure` option: | Format | Syntax | |:---------------------|:----------------------| | Command line | `--insecure` | | YAML | `Insecure` | | Environment variable | `EVENTSTORE_INSECURE` | **Default**: `false` ::: warning When running with protocol security disabled, everything is sent unencrypted over the wire. In the previous version it included the server credentials. Sending username and password over the wire without encryption is not secure by definition, but it might give a false sense of security. In order to make things explicit, EventStoreDB v20 **does not use any authentication and authorisation** (including ACLs) when running insecure. ::: ### Running with default admin and ops password We are adding an ability to set default admin and ops passwords on first run of the database. It will not impact the existing credentials, the user can log into their accounts with exising passwords. For this to work, you can use the `DefaultAdminPassword` option: | Format | Syntax | |:---------------------|:------------------------------------| | Environment variable | `EVENTSTORE_DEFAULT_ADMIN_PASSWORD` | **Default**: `changeit` For this to work, you can use the `DefaultOpsPassword` option: | Format | Syntax | |:---------------------|:------------------------------------| | Environment variable | `EVENTSTORE_DEFAULT_OPS_PASSWORD` | **Default**: `changeit` ::: warning Due to security reasons the DefaultAdminPassword and DefaultOpsPassword options can only be set through environment variables. The user will receive the error message if they try to pass the options using command line or config file. ::: ### Anonymous access to streams Historically, anonymous users with network access have been allowed to read/write streams that do not have access control lists. This is now disabled by default but can be enabled by setting `AllowAnonymousStreamAccess` to `true`. | Format | Syntax | |:---------------------|:-------------------------------------------| | Command line | `--allow-anonymous-stream-access` | | YAML | `AllowAnonymousStreamAccess` | | Environment variable | `EVENTSTORE_ALLOW_ANONYMOUS_STREAM_ACCESS` | **Default**: `false` ### Anonymous access to endpoints Similarly to streams above, anonymous access has historically been available to some http endpoints. Anonymous access to `/gossip`, `/stats` and the `HTTP OPTIONS` method can now be configured with the following two options. By default `/gossip` is still accessible anonymously but the others are not. Some clients currently rely on anonymous access to `/gossip`. This will likely change in the future. The `AllowAnonymousEndpointAccess` option controls anonymous access to these endpoints. Setting `OverrideAnonymousEndpointAccessForGossip` to `true` allows anonymous access to `/gossip` specifically, overriding the other option. | Format | Syntax | |:---------------------|:---------------------------------------------| | Command line | `--allow-anonymous-endpoint-access` | | YAML | `AllowAnonymousEndpointAccess` | | Environment variable | `EVENTSTORE_ALLOW_ANONYMOUS_ENDPOINT_ACCESS` | **Default**: `false` | Format | Syntax | |:---------------------|:-----------------------------------------------------------| | Command line | `--override-anonymous-endpoint-access-for-gossip` | | YAML | `OverrideAnonymousEndpointAccessForGossip` | | Environment variable | `EVENTSTORE_OVERRIDE_ANONYMOUS_ENDPOINT_ACCESS_FOR_GOSSIP` | **Default**: `true` ::: tip Anonymous access is still always granted to `/ping`, `/info`, the static content of the UI, and http redirects. ::: ## Certificates configuration In this section, you can find settings related to protocol security (HTTPS and TLS). ### Certificate common name SSL certificates can be created with a common name (CN), which is an arbitrary string. Usually it contains the DNS name for which the certificate is issued. When cluster nodes connect to each other, they need to ensure that they indeed talk to another node and not something that pretends to be a node. To achieve that, EventStoreDB authenticates a connecting node by ensuring that it supplies a trusted client certificate having a CN that matches exactly with the CN in its own certificate. This essentially means that the CN must be the same across all node certificates by default. For example, when using the Event Store [certificate generator](#certificate-generation-tool), the CN is set to `eventstoredb-node` in all node certificates. ::: note Prior to version 23.10.0, the `CertificateReservedNodeCommonName` setting needed to be configured if a user had certificates with a CN other than `eventstoredb-node`. EventStoreDB will now, by default, automatically read the CN from the node's certificate and use it as the `CertificateReservedNodeCommonName`. However, if you still choose to specify the `CertificateReservedNodeCommonName` in your configuration, it will take precedence. ::: In practice, it's not always possible to obtain certificates where the CN is the same across all nodes. For instance, when using a public CA, single-domain certificates are very common and these certificates cannot be used on multiple nodes as they are valid for exactly one host. In this case, the `CertificateReservedNodeCommonName` setting can be configured with a wildcard as per the following example: If the domains are `node1.esdb.mycompany.org`, `node2.esdb.mycompany.org` and `node3.esdb.mycompany.org`, then `CertificateReservedNodeCommonName` must be set to `*.esdb.mycompany.org`. | Format | Syntax | |:---------------------|:---------------------------------------------------| | Command line | `--certificate-reserved-node-common-name` | | YAML | `CertificateReservedNodeCommonName` | | Environment variable | `EVENTSTORE_CERTIFICATE_RESERVED_NODE_COMMON_NAME` | ::: warning Server certificates **must** have the internal and external IP addresses (`ReplicationIp` and `NodeIp` respectively) or DNS names as subject alternative names. ::: ### Trusted root certificates When getting an incoming connection, the server needs to ensure if the certificate used for the connection can be trusted. For this to work, the server needs to know where trusted root certificates are located. EventStoreDB will use the default trusted root certificates location of `/etc/ssl/certs` when running on Linux only. So if you are running on Windows or a platform with a different default certificates location, you'd need to explicitly tell the node to use the OS default root certificate store. For certificates signed by a private CA, you just provide the path to the CA certificate file (but not the filename). If you are running on Windows, you can also load the trusted root certificate from the Windows Certificate Store. The available options for configuring this are described [below](#certificate-store-windows). | Format | Syntax | |:---------------------|:--------------------------------------------| | Command line | `--trusted-root-certificates-paths` | | YAML | `TrustedRootCertificatesPath` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATES_PATH` | **Default**: n/a on Windows, `/etc/ssl/certs` on Linux ### Certificate file The `CertificateFile` setting needs to point to the certificate file, which will be used by the cluster node. | Format | Syntax | |:---------------------|:------------------------------| | Command line | `--certificate-file` | | YAML | `CertificateFile` | | Environment variable | `EVENTSTORE_CERTIFICATE_FILE` | If the certificate file is protected by password, you'd need to set the `CertificatePassword` value accordingly, so the server can load the certificate. | Format | Syntax | |:---------------------|:----------------------------------| | Command line | `--certificate-password` | | YAML | `CertificatePassword` | | Environment variable | `EVENTSTORE_CERTIFICATE_PASSWORD` | If the certificate file doesn't contain the certificate private key, you need to tell the node where to find the key file using the `CertificatePrivateKeyFile` setting. The private key can be in RSA, or PKCS8 format. | Format | Syntax | |:---------------------|:------------------------------------------| | Command line | `--certificate-private-key-file` | | YAML | `CertificatePrivateKeyFile` | | Environment variable | `EVENTSTORE_CERTIFICATE_PRIVATE_KEY_FILE` | If the private key file is an encrypted PKCS #8 file, then you need to provide the password with the `CertificatePrivateKeyPassword` option. | Format | Syntax | |:---------------------|:----------------------------------------------| | Command line | `--certificate-private-key-password` | | YAML | `CertificatePrivateKeyPassword` | | Environment variable | `EVENTSTORE_CERTIFICATE_PRIVATE_KEY_PASSWORD` | ### Certificate store (Windows) The certificate store location is the location of the Windows certificate store, for example `CurrentUser`. | Format | Syntax | |:---------------------|:----------------------------------------| | Command line | `--certificate-store-location` | | YAML | `CertificateStoreLocation` | | Environment variable | `EVENTSTORE_CERTIFICATE_STORE_LOCATION` | The certificate store name is the name of the Windows certificate store, for example `My`. | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--certificate-store-name` | | YAML | `CertificateStoreName` | | Environment variable | `EVENTSTORE_CERTIFICATE_STORE_NAME` | You can load a certificate using either its thumbprint or its subject name. If using the thumbprint, the server expects to only find one certificate file matching that thumbprint in the cert store. | Format | Syntax | |:---------------------|:------------------------------------| | Command line | `--certificate-thumbprint` | | YAML | `CertificateThumbprint` | | Environment variable | `EVENTSTORE_CERTIFICATE_THUMBPRINT` | The subject name matches any certificate that contains the specified name. This means that multiple matching certificates could be found. To match any certificate made by the es-gencert-cli, you can set the subject name to `eventstoredb-node`. If multiple matching certificates are found, then the certificate with the latest expiry date will be selected. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--certificate-subject-name` | | YAML | `CertificateSubjectName` | | Environment variable | `EVENTSTORE_CERTIFICATE_SUBJECT_NAME` | When you are loading your node certificates from the Windows cert store, you are likely to want to load the trusted root certificate from the cert store as well. The options to configure this are similar to the ones for node certificates. The trusted root certificate store location is the location of the Windows certificate store in which the trusted root certificate is installed, for example `CurrentUser`. | Format | Syntax | |:---------------------|:-----------------------------------------------------| | Command line | `--trusted-root-certificate-store-location` | | YAML | `TrustedRootCertificateStoreLocation` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_STORE_LOCATION` | The trusted root certificate store name is the name of the Windows certificate store in which the trusted root certificate is installed, for example `Root`. | Format | Syntax | |:---------------------|:-------------------------------------------------| | Command line | `--trusted-root-certificate-store-name` | | YAML | `TrustedRootCertificateStoreName` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_STORE_NAME` | Trusted root certificates can also be loaded using either its thumbprint or its subject name. If using the thumbprint, the server expects to only find one trusted root certificate file matching that thumbprint in the cert store. | Format | Syntax | |:---------------------|:-------------------------------------------------| | Command line | `--trusted-root-certificate-thumbprint` | | YAML | `TrustedRootCertificateThumbprint` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_THUMBPRINT` | The subject name matches any certificate that contains the specified name. This means that multiple matching certificates could be found. To match any root certificate made through the es-gencert-cli, you can set the Subject Name to `EventStoreDB CA`. If multiple matching root certificates are found, then the root certificate with the latest expiry date will be selected. | Format | Syntax | |:---------------------|:---------------------------------------------------| | Command line | `--trusted-root-certificate-subject-name` | | YAML | `TrustedRootCertificateSubjectName` | | Environment variable | `EVENTSTORE_TRUSTED_ROOT_CERTIFICATE_SUBJECT_NAME` | ## Certificate generation tool Event Store provides the interactive Certificate Generation CLI, which creates certificates signed by a private, auto-generated CA for EventStoreDB. You can use the [configuration wizard](../quick-start/installation.md), that will provide you exact CLI commands that you need to run to generate certificates matching your configuration. ### Getting started CLI is available as Open Source project in the [Github Repository](https://github.com/EventStore/es-gencert-cli). The latest release can be found under the [GitHub releases page.](https://github.com/EventStore/es-gencert-cli/releases) We're releasing binaries for Windows, Linux and macOS. We also publish the tool as a Docker image. Basic usage for Certificate Generation CLI: ```bash ./es-gencert-cli [options] [args] ``` Getting help for a specific command: ```bash ./es-gencert-cli -help ``` ::: warning If you are running EventStoreDB on Linux, remember that all certificate files should have restrictive rights, otherwise the OS won't allow using them. Usually, you'd need to change rights for each certificate file to prevent the "permissions are too open" error. You can do it by running the following command: ```bash chmod 600 [file] ``` ::: ### Generating the CA certificate As the first step CA certificate needs to be generated. It'll need to be trusted for each of the nodes and client environment. By default, the tool will create the `ca` directory in the `certs` directory you created. Two keys will be generated: * `ca.crt` - public file that need to be used also for the nodes and client configuration, * `ca.key` - private key file that should be used only in the node configuration. **Do not copy it to client environment**. CA certificate will be generated with pre-defined CN `eventstoredb-node`. To generate CA certificate run: ```bash ./es-gencert-cli create-ca ``` You can customise generated cert by providing following params: | Param | Description | |:--------|:------------------------------------------------------------------| | `-days` | The validity period of the certificate in days (default: 5 years) | | `-out` | The output directory (default: ./ca) | Example: ```bash ./es-gencert-cli create-ca -out ./es-ca ``` ### Generating the Node certificate You need to generate certificates signed by the CA for each node. They should be installed only on the specific node machine. By default, the tool will create the `ca` directory in the `certs` directory you created. Two keys will be generated: * `node.crt` - the public file that needs to be also used for the nodes and client configuration, * `node.key` - the private key file that should be used only in the node's configuration. **Do not copy it to client environment**. To generate node certificate run command: ```bash ./es-gencert-cli -help create_node ``` You can customise generated cert by providing following params: | Param | Description | |:------------------|:------------------------------------------------------------------------------| | `-ca-certificate` | The path to the CA certificate file (default: `./ca/ca.crt`) | | `-ca-key` | The path to the CA key file (default: `./ca/ca.key`) | | `-days` | The output directory (default: `./nodeX` where X is an auto-generated number) | | `-out` | The output directory (default: `./ca`) | | `-ip-addresses` | Comma-separated list of IP addresses of the node | | `-dns-names` | Comma-separated list of DNS names of the node | ::: warning While generating the certificate, you need to remember to pass internal end external: * IP addresses to `-ip-addresses`: e.g. `127.0.0.1,172.20.240.1` and/or * DNS names to `-dns-names`: e.g. `localhost,node1.eventstore` that will match the URLs that you will be accessing EventStoreDB nodes. ::: Sample: ``` ./es-gencert-cli-cli create-node \ -ca-certificate ./es-ca/ca.crt \ -ca-key ./es-ca/ca.key \ -out ./node1 \ -ip-addresses 127.0.0.1,172.20.240.1 \ -dns-names localhost,node1.eventstore ``` ### Running with Docker You could also run the tool using Docker interactive container: ```bash docker run --rm -i eventstore/es-gencert-cli ``` One useful scenario is to use the Docker Compose file tool to generate all the necessary certificates before starting cluster nodes. Sample: ```yaml version: "3.5" services: setup: image: eventstore/es-gencert-cli:1.0.2 entrypoint: bash user: "1000:1000" command: > -c "mkdir -p ./certs && cd /certs && es-gencert-cli create-ca && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.1 -dns-names localhost,node1.eventstore && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.2 -dns-names localhost,node2.eventstore && es-gencert-cli create-node -out ./node1 -ip-addresses 127.0.0.1,172.20.240.3 -dns-names localhost,node3.eventstore && find . -type f -print0 | xargs -0 chmod 666" container_name: setup volumes: - ./certs:/certs ``` See more in the [complete sample of docker-compose secured cluster configuration.](../quick-start/installation.md#use-docker-compose) ## Certificate installation on a client environment To connect to EventStoreDB, you need to install the auto-generated CA certificate file on the client machine (e.g. machine where the client is hosted, or your dev environment). ### Linux (Ubuntu, Debian) 1. Copy auto-generated CA file to dir `/usr/local/share/ca-certificates/`, e.g. using command: ```bash sudo cp ca.crt /usr/local/share/ca-certificates/event_store_ca.crt ``` 2. Update the CA store: ```bash sudo update-ca-certificates ``` ### Windows 1. You can manually import it to the local CA cert store through `Certificates Local Machine Management Console`. To do that select **Run** from the **Start** menu, and then enter `certmgr.msc`. Then import certificate to `Trusted Root Certification`. 2. You can also run the PowerShell script instead: ```powershell Import-Certificate -FilePath ".\certs\ca\ca.crt" -CertStoreLocation Cert:\CurrentUser\Root ``` ### MacOS 1. In the Keychain Access app on your Mac, select either the login or System keychain. Drag the certificate file onto the Keychain Access app. If you're asked to provide a name and password, type the name and password for an administrator user on this computer. 2. You can also run the bash script: ```bash sudo security add-certificates -k /Library/Keychains/System.keychain ca.crt ``` ## Intermediate CA certificates Intermediate CA certificates are supported by loading them from a [PEM](https://datatracker.ietf.org/doc/html/rfc1422) or [PKCS #12](https://datatracker.ietf.org/doc/html/rfc7292) bundle specified by the [`CertificateFile` configuration parameter](#certificate-file). To make sure that the configuration is correct, the certificate chain is validated on startup with the node's own certificate. If you've used the [certificate generation tool](#certificate-generation-tool) with the default settings to generate your CA and node certificates, then you're not using intermediate CA certificates. However, if you're using a public certificate authority (e.g [Let's Encrypt](https://letsencrypt.org/)) to generate your node certificates there is a chance that you're using intermediate CA certificates without knowing. This is due to the [Authority Information Access (AIA)](https://datatracker.ietf.org/doc/html/rfc4325#section-2) extension which allows intermediate certificates to be fetched from a remote server. To verify if your certificate is using the AIA extension, you need to verify if there is a section named: `Authority Information Access` in the certificate. ::: tabs#os @tab Linux Use `openssl` to find the section in the certificate file: ``` openssl x509 -in /path/to/node.crt -text | grep 'Authority Information Access' -A 1 ``` @tab Windows Open the certificate, go to the `Details` tab and look for the `Authority Information Access` field. If the extension is present, you can manually download the intermediate certificate from the URL present under the `CA Issuers` entry. Note that you will usually need to convert the downloaded certificate from the `DER` to the `PEM` format. This can be done with the following `openssl` command: ```bash openssl x509 -inform der -in /path/to/cert.der > /path/to/cert.pem ``` or with [an online service](https://www.sslshopper.com/ssl-converter.html) if you don't have openssl installed. ::: It's possible that there are more than one intermediate CA certificates in the chain - so you need to verify if the certificate you've just downloaded also uses the AIA extension. If yes, you need to download the next intermediate CA certificate in the chain by repeating the same process above until you eventually reach a publicly trusted root certificate (i.e. the `Subject` and `Issuer` fields will match). In practice, there'll usually be at most two intermediate certificates in the chain. ### Bundling the intermediate certificates The node's certificate should be first in the bundle, followed by the intermediates. Intermediates can be in any order but it would be good to keep it from leaf to root, as per the usual convention. The root certificate should not be bundled. In the examples below, intermediate certificates are numbered from 1 to N starting from the leaf and going up. #### PEM format If your node's certificate and the intermediate CA certificates are both PEM formatted, that is they begin with `-----BEGIN CERTIFICATE-----` and end with `-----END CERTIFICATE-----` then you can simply append the contents of the intermediate certificate files to the end of the node's certificate file to create the bundle. ::: tabs#os @tab Linux ```bash cat /path/to/intermediate1.crt >> /path/to/node.crt ... cat /path/to/intermediateN.crt >> /path/to/node.crt ``` @tab Windows ```powershell type C:\path\to\intermediate1.crt >> C:\path\to\node.crt ... type C:\path\to\intermediateN.crt >> C:\path\to\node.crt ``` ::: #### PKCS #12 format If you want to generate a PKCS #12 bundle from PEM formatted certificate files, please follow the steps below. ```bash cat /path/to/intermediate1.crt >> ./ca_bundle.crt ... cat /path/to/intermediateN.crt >> ./ca_bundle.crt openssl pkcs12 -export -in /path/to/node.crt -inkey /path/to/node.key -certfile ./ca_bundle.crt -out /path/to/node.p12 -passout pass: ``` ### Adding intermediate certificates to the certificate store Intermediate certificates also need to be added to the current user's certificate store. This is required for two reasons:\ i) For the full certificate chain to be sent when TLS connections are established\ ii) To improve performance by preventing certificate downloads if your certificate uses the AIA extension ::: tabs#os @tab Linux The following script assumes EventStoreDB is running under the `eventstore` account. ```bash sudo su eventstore --shell /bin/bash dotnet tool install --global dotnet-certificate-tool ~/.dotnet/tools/certificate-tool add -s CertificateAuthority -l CurrentUser --file /path/to/intermediate.crt ``` @tab Windows To import the intermediate certificate in the `Intermediate Certification Authorities` certificate store, run the following PowerShell command under the same account as EventStoreDB is running: ```powershell Import-Certificate -FilePath .\path\to\intermediate.crt -CertStoreLocation Cert:\CurrentUser\CA ``` Optionally, to import the intermediate certificate in the `Local Computer` store, run the following as `Administrator`: ```powershell Import-Certificate -FilePath .\ca.crt -CertStoreLocation Cert:\LocalMachine\CA ``` ::: ## Replication protocol security Although TCP is disabled by default for external connections (clients), cluster nodes still use TCP for replication. If you aren't running EventStoreDB in insecure mode, all TCP communication will use TLS using the same certificates as SSL. You can, however, disable TLS for both internal and external TCP. | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-internal-tcp-tls` | | YAML | `DisableInternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_INTERNAL_TCP_TLS` | **Default**: `false` | Format | Syntax | |:---------------------|:--------------------------------------| | Command line | `--disable-external-tcp-tls` | | YAML | `DisableExternalTcpTls` | | Environment variable | `EVENTSTORE_DISABLE_EXTERNAL_TCP_TLS` | **Default**: `false` ## Authentication EventStoreDB supports authentication based on usernames and passwords out of the box. The Enterprise version also supports LDAP as the authentication source. Authentication is applied to all HTTP endpoints, except `/info`, `/ping`, `/stats`, `/elections` (only `GET`) , `/gossip` (only `GET`) and static web content. ### Default users EventStoreDB provides two default users, `$ops` and `$admin`. `$admin` has full access to everything in EventStoreDB. It can read and write to protected streams, which is any stream that starts with $, such as `$projections-master`. Protected streams are usually system streams, for example, `$projections-master` manages some projections' states. The `$admin` user can also run operational commands, such as scavenges and shutdowns on EventStoreDB. An `$ops` user can do everything that an `$admin` can do except manage users and read from system streams ( except for `$scavenges` and `$scavenges-streams`). ### New users New users created in EventStoreDB are standard non-authenticated users. Non-authenticated users are allowed `GET` access to the `/info`, `/ping`, `/stats`, `/elections`, and `/gossip` system streams. `POST` access to the `/elections` and `/gossip` system streams is only allowed on the internal HTTP service. By default, any user can read any non-protected stream unless there is an ACL preventing that. ### Externalised authentication You can also use the trusted intermediary header for externalized authentication that allows you to integrate almost any authentication system with EventStoreDB. Read more about [the trusted intermediary header](#trusted-intermediary). ### Disable HTTP authentication It is possible to disable authentication on all protected HTTP endpoints by setting the `DisableFirstLevelHttpAuthorization` setting to `true`. The setting is set to `false` by default. When enabled, the setting will force EventStoreDB to use the supplied credentials only to check the stream access using [ACLs](#access-control-lists). ## Access control lists By default, authenticated users have access to the whole EventStoreDB database. In addition to that, it allows you to use Access Control Lists (ACLs) to set up more granular access control. In fact, the default access level is also controlled by a special ACL, which is called the [default ACL](#default-acl). ### Stream ACL EventStoreDB keeps the ACL of a stream in the stream metadata as JSON with the below definition: ```json { "$acl": { "$w": "$admins", "$r": "$all", "$d": "$admins", "$mw": "$admins", "$mr": "$admins" } } ``` These fields represent the following: * `$w` The permission to write to this stream. * `$r` The permission to read from this stream. * `$d` The permission to delete this stream. * `$mw` The permission to write the metadata associated with this stream. * `$mr` The permission to read the metadata associated with this stream. You can update these fields with either a single string or an array of strings representing users or groups (`$admins`, `$all`, or custom groups). It's possible to put an empty array into one of these fields, and this has the effect of removing all users from that permission. ::: tip We recommend you don't give people access to `$mw` as then they can then change the ACL. ::: ### Default ACL The `$settings` stream has a special ACL used as the default ACL. This stream controls the default ACL for streams without an ACL and also controls who can create streams in the system, the default state of these is shown below: ```json { "$userStreamAcl": { "$r": "$all", "$w": "$all", "$d": "$all", "$mr": "$all", "$mw": "$all" }, "$systemStreamAcl": { "$r": "$admins", "$w": "$admins", "$d": "$admins", "$mr": "$admins", "$mw": "$admins" } } ``` You can rewrite these to the `$settings` stream with the following request: @[code{curl}](@samples/server/default-settings.sh) The `$userStreamAcl` controls the default ACL for user streams, while all system streams use the `$systemStreamAcl` as the default. ::: tip The `$w` in `$userStreamAcl` also applies to the ability to create a stream. Members of `$admins` always have access to everything, you cannot remove this permission. ::: When you set a permission on a stream, it overrides the default values. However, it's not necessary to specify all permissions on a stream. It's only necessary to specify those which differ from the default. Here is an example of the default ACL that has been changed: ```json { "$userStreamAcl": { "$r": "$all", "$w": "ouro", "$d": "ouro", "$mr": "ouro", "$mw": "ouro" }, "$systemStreamAcl": { "$r": "$admins", "$w": "$admins", "$d": "$admins", "$mr": "$admins", "$mw": "$admins" } } ``` This default ACL gives `ouro` and `$admins` create and write permissions on all streams, while everyone else can read from them. Be careful allowing default access to system streams to non-admins as they would also have access to `$settings` unless you specifically override it. Refer to the documentation of the HTTP API or SDK of your choice for more information about changing ACLs programmatically. ## Trusted intermediary The trusted intermediary header helps EventStoreDB to support a common security architecture. There are thousands of possible methods for handling authentication and it is impossible for us to support them all. The header allows you to configure a trusted intermediary to handle the authentication instead of EventStoreDB. A sample configuration is to enable OAuth2 with the following steps: * Configure EventStoreDB to run on the local loopback. * Configure nginx to handle OAuth2 authentication. * After authenticating the user, nginx rewrites the request and forwards it to the loopback to EventStoreDB that serves the request. The header has the form of `{user}; group, group1` and the EventStoreDB ACLs use the information to handle security. ```http:no-line-numbers ES-TrustedAuth: "root; admin, other" ``` Use the following option to enable this feature: | Format | Syntax | |:---------------------|:---------------------------------| | Command line | `--enable-trusted-auth` | | YAML | `EnableTrustedAuth` | | Environment variable | `EVENTSTORE_ENABLE_TRUSTED_AUTH` | ## FIPS 140-2 EventStoreDB runs on FIPS 140-2 enabled operating systems, this feature requires a commercial licence. The Federal Information Processing Standards (FIPS) of the United States are a set of publicly announced standards that the National Institute of Standards and Technology (NIST) has developed for use in computer systems of non-military United States government agencies and contractors. The 140 series of FIPS (FIPS 140) are U.S. government computer security standards that specify requirements for cryptography modules. To run EventStoreDB on FIPS 140-2 enabled operating systems, the following is required: 1. The MD5 plugin must be installed. It is available on our [commercial downloads page](https://developers.eventstore.org/). To install it, unzip it in the `plugins` directory inside the server installation directory and restart the server. 2. The node's certificates & keys must be FIPS 140-2 compatible * Our certificate generation tool, [es-gencert-cli](https://github.com/EventStore/es-gencert-cli/releases), generates FIPS 140-2 compatible certificates & keys as from version 1.2.0 (only the linux build). * If you want to manually generate your certificates or keys with openssl, you must use openssl 3 or later. * If you want to use PKCS #12 bundles (.p12/.pfx extension), you must use a FIPS 140-2 compatible encryption algorithm (e.g AES-256) & hash algorithm (e.g SHA-256) to encrypt the bundle's contents. Note that EventStoreDB will also likely run properly on FIPS 140-3 compliant operating systems with the above steps but this cannot reliably be confirmed at the moment as FIPS 140-3 certification is still ongoing for the operating systems themselves. --- --- url: 'https://docs.kurrent.io/server/v23.10/diagnostics/index.md' --- # Diagnostics EventStoreDB provides several ways to diagnose and troubleshoot issues. * [Logging](logs.md): structured or plain-text logs on the console and in log files. * [Metrics](metrics.md): collect standard metrics using Prometheus or OpenTelemetry. * [Stats](#statistics): stats collection and HTTP endpoint. * [Histograms](#histograms): metrics collection and HTTP endpoints. You can also use external tools to measure the performance of EventStoreDB and monitor the cluster health. Learn more on the [Integrations](./integrations.md) page. ## Statistics EventStoreDB servers collect internal statistics and make it available via HTTP over the `https://:2113/stats` in JSON format. Here, `2113` is the default HTTP port. Monitoring applications and metric collectors can use this endpoint to gather the information about the cluster node. The `stats` endpoint only exposes information about the node where you fetch it from and doesn't contain any cluster information. What you see in the `stats` endpoint response is the last collected state of the server. The server collects this information using events that are appended to the statistics stream. Each node has one. We use a reserved name for the stats stream, `$stats-`. For example, for a single node running locally the stream name would be `$stats-127.0.0.1:2113`. As all other events, stats events are also linked in the `$all` stream. These events have a reserved event type `$statsCollected`. ::: details Click here to see an example of a stats event ```json { "proc-startTime": "2020-06-25T10:13:26.8281750Z", "proc-id": 5465, "proc-mem": 118648832, "proc-cpu": 2.44386363, "proc-cpuScaled": 0.152741477, "proc-threadsCount": 10, "proc-contentionsRate": 0.9012223, "proc-thrownExceptionsRate": 0.0, "sys-cpu": 100.0, "sys-freeMem": 25100288, "proc-gc-allocationSpeed": 0.0, "proc-gc-gen0ItemsCount": 8, "proc-gc-gen0Size": 0, "proc-gc-gen1ItemsCount": 2, "proc-gc-gen1Size": 0, "proc-gc-gen2ItemsCount": 0, "proc-gc-gen2Size": 0, "proc-gc-largeHeapSize": 0, "proc-gc-timeInGc": 0.0, "proc-gc-totalBytesInHeaps": 0, "proc-tcp-connections": 0, "proc-tcp-receivingSpeed": 0.0, "proc-tcp-sendingSpeed": 0.0, "proc-tcp-inSend": 0, "proc-tcp-measureTime": "00:00:19.0534210", "proc-tcp-pendingReceived": 0, "proc-tcp-pendingSend": 0, "proc-tcp-receivedBytesSinceLastRun": 0, "proc-tcp-receivedBytesTotal": 0, "proc-tcp-sentBytesSinceLastRun": 0, "proc-tcp-sentBytesTotal": 0, "es-checksum": 1613144, "es-checksumNonFlushed": 1613144, "sys-drive-/System/Volumes/Data-availableBytes": 545628151808, "sys-drive-/System/Volumes/Data-totalBytes": 2000481927168, "sys-drive-/System/Volumes/Data-usage": "72%", "sys-drive-/System/Volumes/Data-usedBytes": 1454853775360, "es-queue-Index Committer-queueName": "Index Committer", "es-queue-Index Committer-groupName": "", "es-queue-Index Committer-avgItemsPerSecond": 0, "es-queue-Index Committer-avgProcessingTime": 0.0, "es-queue-Index Committer-currentIdleTime": "0:00:00:29.9895180", "es-queue-Index Committer-currentItemProcessingTime": null, "es-queue-Index Committer-idleTimePercent": 100.0, "es-queue-Index Committer-length": 0, "es-queue-Index Committer-lengthCurrentTryPeak": 0, "es-queue-Index Committer-lengthLifetimePeak": 0, "es-queue-Index Committer-totalItemsProcessed": 0, "es-queue-Index Committer-inProgressMessage": "", "es-queue-Index Committer-lastProcessedMessage": "", "es-queue-MainQueue-queueName": "MainQueue", "es-queue-MainQueue-groupName": "", "es-queue-MainQueue-avgItemsPerSecond": 14, "es-queue-MainQueue-avgProcessingTime": 0.0093527972027972021, "es-queue-MainQueue-currentIdleTime": "0:00:00:00.8050567", "es-queue-MainQueue-currentItemProcessingTime": null, "es-queue-MainQueue-idleTimePercent": 99.986616840364917, "es-queue-MainQueue-length": 0, "es-queue-MainQueue-lengthCurrentTryPeak": 3, "es-queue-MainQueue-lengthLifetimePeak": 6, "es-queue-MainQueue-totalItemsProcessed": 452, "es-queue-MainQueue-inProgressMessage": "", "es-queue-MainQueue-lastProcessedMessage": "Schedule", "es-queue-MonitoringQueue-queueName": "MonitoringQueue", "es-queue-MonitoringQueue-groupName": "", "es-queue-MonitoringQueue-avgItemsPerSecond": 0, "es-queue-MonitoringQueue-avgProcessingTime": 1.94455, "es-queue-MonitoringQueue-currentIdleTime": "0:00:00:19.0601186", "es-queue-MonitoringQueue-currentItemProcessingTime": null, "es-queue-MonitoringQueue-idleTimePercent": 99.980537727681721, "es-queue-MonitoringQueue-length": 0, "es-queue-MonitoringQueue-lengthCurrentTryPeak": 0, "es-queue-MonitoringQueue-lengthLifetimePeak": 0, "es-queue-MonitoringQueue-totalItemsProcessed": 14, "es-queue-MonitoringQueue-inProgressMessage": "", "es-queue-MonitoringQueue-lastProcessedMessage": "GetFreshTcpConnectionStats", "es-queue-PersistentSubscriptions-queueName": "PersistentSubscriptions", "es-queue-PersistentSubscriptions-groupName": "", "es-queue-PersistentSubscriptions-avgItemsPerSecond": 1, "es-queue-PersistentSubscriptions-avgProcessingTime": 0.010400000000000001, "es-queue-PersistentSubscriptions-currentIdleTime": "0:00:00:00.8052015", "es-queue-PersistentSubscriptions-currentItemProcessingTime": null, "es-queue-PersistentSubscriptions-idleTimePercent": 99.998954276430226, "es-queue-PersistentSubscriptions-length": 0, "es-queue-PersistentSubscriptions-lengthCurrentTryPeak": 0, "es-queue-PersistentSubscriptions-lengthLifetimePeak": 0, "es-queue-PersistentSubscriptions-totalItemsProcessed": 32, "es-queue-PersistentSubscriptions-inProgressMessage": "", "es-queue-PersistentSubscriptions-lastProcessedMessage": "PersistentSubscriptionTimerTick", "es-queue-Projection Core #0-queueName": "Projection Core #0", "es-queue-Projection Core #0-groupName": "Projection Core", "es-queue-Projection Core #0-avgItemsPerSecond": 0, "es-queue-Projection Core #0-avgProcessingTime": 0.0, "es-queue-Projection Core #0-currentIdleTime": "0:00:00:29.9480513", "es-queue-Projection Core #0-currentItemProcessingTime": null, "es-queue-Projection Core #0-idleTimePercent": 100.0, "es-queue-Projection Core #0-length": 0, "es-queue-Projection Core #0-lengthCurrentTryPeak": 0, "es-queue-Projection Core #0-lengthLifetimePeak": 0, "es-queue-Projection Core #0-totalItemsProcessed": 2, "es-queue-Projection Core #0-inProgressMessage": "", "es-queue-Projection Core #0-lastProcessedMessage": "SubComponentStarted", "es-queue-Projections Master-queueName": "Projections Master", "es-queue-Projections Master-groupName": "", "es-queue-Projections Master-avgItemsPerSecond": 0, "es-queue-Projections Master-avgProcessingTime": 0.0, "es-queue-Projections Master-currentIdleTime": "0:00:00:29.8467445", "es-queue-Projections Master-currentItemProcessingTime": null, "es-queue-Projections Master-idleTimePercent": 100.0, "es-queue-Projections Master-length": 0, "es-queue-Projections Master-lengthCurrentTryPeak": 0, "es-queue-Projections Master-lengthLifetimePeak": 3, "es-queue-Projections Master-totalItemsProcessed": 10, "es-queue-Projections Master-inProgressMessage": "", "es-queue-Projections Master-lastProcessedMessage": "RegularTimeout", "es-queue-Storage Chaser-queueName": "Storage Chaser", "es-queue-Storage Chaser-groupName": "", "es-queue-Storage Chaser-avgItemsPerSecond": 94, "es-queue-Storage Chaser-avgProcessingTime": 0.0043385023898035047, "es-queue-Storage Chaser-currentIdleTime": "0:00:00:00.0002530", "es-queue-Storage Chaser-currentItemProcessingTime": null, "es-queue-Storage Chaser-idleTimePercent": 99.959003031702224, "es-queue-Storage Chaser-length": 0, "es-queue-Storage Chaser-lengthCurrentTryPeak": 0, "es-queue-Storage Chaser-lengthLifetimePeak": 0, "es-queue-Storage Chaser-totalItemsProcessed": 2835, "es-queue-Storage Chaser-inProgressMessage": "", "es-queue-Storage Chaser-lastProcessedMessage": "ChaserCheckpointFlush", "es-queue-StorageReaderQueue #1-queueName": "StorageReaderQueue #1", "es-queue-StorageReaderQueue #1-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #1-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #1-avgProcessingTime": 0.22461000000000003, "es-queue-StorageReaderQueue #1-currentIdleTime": "0:00:00:00.9863988", "es-queue-StorageReaderQueue #1-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #1-idleTimePercent": 99.988756844383616, "es-queue-StorageReaderQueue #1-length": 0, "es-queue-StorageReaderQueue #1-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #1-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #1-totalItemsProcessed": 15, "es-queue-StorageReaderQueue #1-inProgressMessage": "", "es-queue-StorageReaderQueue #1-lastProcessedMessage": "ReadStreamEventsBackward", "es-queue-StorageReaderQueue #2-queueName": "StorageReaderQueue #2", "es-queue-StorageReaderQueue #2-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #2-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #2-avgProcessingTime": 8.83216, "es-queue-StorageReaderQueue #2-currentIdleTime": "0:00:00:00.8051068", "es-queue-StorageReaderQueue #2-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #2-idleTimePercent": 99.557874170777851, "es-queue-StorageReaderQueue #2-length": 0, "es-queue-StorageReaderQueue #2-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #2-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #2-totalItemsProcessed": 16, "es-queue-StorageReaderQueue #2-inProgressMessage": "", "es-queue-StorageReaderQueue #2-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageReaderQueue #3-queueName": "StorageReaderQueue #3", "es-queue-StorageReaderQueue #3-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #3-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #3-avgProcessingTime": 6.4189888888888893, "es-queue-StorageReaderQueue #3-currentIdleTime": "0:00:00:02.8228372", "es-queue-StorageReaderQueue #3-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #3-idleTimePercent": 99.710808119472517, "es-queue-StorageReaderQueue #3-length": 0, "es-queue-StorageReaderQueue #3-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #3-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #3-totalItemsProcessed": 14, "es-queue-StorageReaderQueue #3-inProgressMessage": "", "es-queue-StorageReaderQueue #3-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageReaderQueue #4-queueName": "StorageReaderQueue #4", "es-queue-StorageReaderQueue #4-groupName": "StorageReaderQueue", "es-queue-StorageReaderQueue #4-avgItemsPerSecond": 0, "es-queue-StorageReaderQueue #4-avgProcessingTime": 0.36447, "es-queue-StorageReaderQueue #4-currentIdleTime": "0:00:00:01.8144419", "es-queue-StorageReaderQueue #4-currentItemProcessingTime": null, "es-queue-StorageReaderQueue #4-idleTimePercent": 99.981747643099709, "es-queue-StorageReaderQueue #4-length": 0, "es-queue-StorageReaderQueue #4-lengthCurrentTryPeak": 0, "es-queue-StorageReaderQueue #4-lengthLifetimePeak": 0, "es-queue-StorageReaderQueue #4-totalItemsProcessed": 14, "es-queue-StorageReaderQueue #4-inProgressMessage": "", "es-queue-StorageReaderQueue #4-lastProcessedMessage": "ReadStreamEventsForward", "es-queue-StorageWriterQueue-queueName": "StorageWriterQueue", "es-queue-StorageWriterQueue-groupName": "", "es-queue-StorageWriterQueue-avgItemsPerSecond": 0, "es-queue-StorageWriterQueue-avgProcessingTime": 0.0, "es-queue-StorageWriterQueue-currentIdleTime": "0:00:00:29.9437790", "es-queue-StorageWriterQueue-currentItemProcessingTime": null, "es-queue-StorageWriterQueue-idleTimePercent": 100.0, "es-queue-StorageWriterQueue-length": 0, "es-queue-StorageWriterQueue-lengthCurrentTryPeak": 0, "es-queue-StorageWriterQueue-lengthLifetimePeak": 0, "es-queue-StorageWriterQueue-totalItemsProcessed": 6, "es-queue-StorageWriterQueue-inProgressMessage": "", "es-queue-StorageWriterQueue-lastProcessedMessage": "WritePrepares", "es-queue-Subscriptions-queueName": "Subscriptions", "es-queue-Subscriptions-groupName": "", "es-queue-Subscriptions-avgItemsPerSecond": 1, "es-queue-Subscriptions-avgProcessingTime": 0.057019047619047622, "es-queue-Subscriptions-currentIdleTime": "0:00:00:00.8153708", "es-queue-Subscriptions-currentItemProcessingTime": null, "es-queue-Subscriptions-idleTimePercent": 99.993992971356, "es-queue-Subscriptions-length": 0, "es-queue-Subscriptions-lengthCurrentTryPeak": 0, "es-queue-Subscriptions-lengthLifetimePeak": 0, "es-queue-Subscriptions-totalItemsProcessed": 31, "es-queue-Subscriptions-inProgressMessage": "", "es-queue-Subscriptions-lastProcessedMessage": "CheckPollTimeout", "es-queue-Timer-queueName": "Timer", "es-queue-Timer-groupName": "", "es-queue-Timer-avgItemsPerSecond": 14, "es-queue-Timer-avgProcessingTime": 0.038568989547038329, "es-queue-Timer-currentIdleTime": "0:00:00:00.0002752", "es-queue-Timer-currentItemProcessingTime": null, "es-queue-Timer-idleTimePercent": 99.94364205726194, "es-queue-Timer-length": 17, "es-queue-Timer-lengthCurrentTryPeak": 17, "es-queue-Timer-lengthLifetimePeak": 17, "es-queue-Timer-totalItemsProcessed": 419, "es-queue-Timer-inProgressMessage": "", "es-queue-Timer-lastProcessedMessage": "ExecuteScheduledTasks", "es-queue-Worker #1-queueName": "Worker #1", "es-queue-Worker #1-groupName": "Workers", "es-queue-Worker #1-avgItemsPerSecond": 2, "es-queue-Worker #1-avgProcessingTime": 0.076058695652173922, "es-queue-Worker #1-currentIdleTime": "0:00:00:00.8050943", "es-queue-Worker #1-currentItemProcessingTime": null, "es-queue-Worker #1-idleTimePercent": 99.982484504768721, "es-queue-Worker #1-length": 0, "es-queue-Worker #1-lengthCurrentTryPeak": 0, "es-queue-Worker #1-lengthLifetimePeak": 0, "es-queue-Worker #1-totalItemsProcessed": 73, "es-queue-Worker #1-inProgressMessage": "", "es-queue-Worker #1-lastProcessedMessage": "ReadStreamEventsForwardCompleted", "es-queue-Worker #2-queueName": "Worker #2", "es-queue-Worker #2-groupName": "Workers", "es-queue-Worker #2-avgItemsPerSecond": 2, "es-queue-Worker #2-avgProcessingTime": 0.19399347826086957, "es-queue-Worker #2-currentIdleTime": "0:00:00:00.8356863", "es-queue-Worker #2-currentItemProcessingTime": null, "es-queue-Worker #2-idleTimePercent": 99.955350254886739, "es-queue-Worker #2-length": 0, "es-queue-Worker #2-lengthCurrentTryPeak": 0, "es-queue-Worker #2-lengthLifetimePeak": 0, "es-queue-Worker #2-totalItemsProcessed": 69, "es-queue-Worker #2-inProgressMessage": "", "es-queue-Worker #2-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #3-queueName": "Worker #3", "es-queue-Worker #3-groupName": "Workers", "es-queue-Worker #3-avgItemsPerSecond": 2, "es-queue-Worker #3-avgProcessingTime": 0.068475555555555567, "es-queue-Worker #3-currentIdleTime": "0:00:00:00.8356754", "es-queue-Worker #3-currentItemProcessingTime": null, "es-queue-Worker #3-idleTimePercent": 99.984583460721979, "es-queue-Worker #3-length": 0, "es-queue-Worker #3-lengthCurrentTryPeak": 0, "es-queue-Worker #3-lengthLifetimePeak": 0, "es-queue-Worker #3-totalItemsProcessed": 68, "es-queue-Worker #3-inProgressMessage": "", "es-queue-Worker #3-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #4-queueName": "Worker #4", "es-queue-Worker #4-groupName": "Workers", "es-queue-Worker #4-avgItemsPerSecond": 2, "es-queue-Worker #4-avgProcessingTime": 0.040221428571428575, "es-queue-Worker #4-currentIdleTime": "0:00:00:00.8356870", "es-queue-Worker #4-currentItemProcessingTime": null, "es-queue-Worker #4-idleTimePercent": 99.99154911144629, "es-queue-Worker #4-length": 0, "es-queue-Worker #4-lengthCurrentTryPeak": 0, "es-queue-Worker #4-lengthLifetimePeak": 0, "es-queue-Worker #4-totalItemsProcessed": 65, "es-queue-Worker #4-inProgressMessage": "", "es-queue-Worker #4-lastProcessedMessage": "PurgeTimedOutRequests", "es-queue-Worker #5-queueName": "Worker #5", "es-queue-Worker #5-groupName": "Workers", "es-queue-Worker #5-avgItemsPerSecond": 2, "es-queue-Worker #5-avgProcessingTime": 0.17759268292682928, "es-queue-Worker #5-currentIdleTime": "0:00:00:00.8052165", "es-queue-Worker #5-currentItemProcessingTime": null, "es-queue-Worker #5-idleTimePercent": 99.9635548548067, "es-queue-Worker #5-length": 0, "es-queue-Worker #5-lengthCurrentTryPeak": 0, "es-queue-Worker #5-lengthLifetimePeak": 0, "es-queue-Worker #5-totalItemsProcessed": 70, "es-queue-Worker #5-inProgressMessage": "", "es-queue-Worker #5-lastProcessedMessage": "IODispatcherDelayedMessage", "es-writer-lastFlushSize": 0, "es-writer-lastFlushDelayMs": 0.0134, "es-writer-meanFlushSize": 0, "es-writer-meanFlushDelayMs": 0.0134, "es-writer-maxFlushSize": 0, "es-writer-maxFlushDelayMs": 0.0134, "es-writer-queuedFlushMessages": 0, "es-readIndex-cachedRecord": 676, "es-readIndex-notCachedRecord": 0, "es-readIndex-cachedStreamInfo": 171, "es-readIndex-notCachedStreamInfo": 32, "es-readIndex-cachedTransInfo": 0, "es-readIndex-notCachedTransInfo": 0 } ``` ::: Stats stream has the max time-to-live set to 24 hours, so all the events that are older than 24 hours will be deleted. ### Stats period Using this setting you can control how often stats events are generated. By default, the node will produce one event in 30 seconds. If you want to decrease network pressure on subscribers to the `$all` stream, you can tell EventStoreDB to produce stats less often. | Format | Syntax | | :------------------- | :---------------------------- | | Command line | `--stats-period-sec` | | YAML | `StatsPeriodSec` | | Environment variable | `EVENTSTORE_STATS_PERIOD_SEC` | **Default**: `30` ### Write stats to database As mentioned before, stats events are quite large and whilst it is sometimes beneficial to keep the stats history, it is most of the time not necessary. Therefore, we do not write stats events to the database by default. When this option is set to `true`, all the stats events will be persisted. As mentioned before, stats events have a TTL of 24 hours and when writing stats to the database is enabled, you'd need to scavenge more often to release the disk space. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--write-stats-to-db` | | YAML | `WriteStatsToDb` | | Environment variable | `EVENTSTORE_WRITE_STATS_TO_DB` | **Default**: `false` ## Histograms Histograms give a distribution in percentiles of the time spent on several metrics. This can be used to diagnose issues in the system. It is not recommended enabling this in production environment. When enabled, histogram stats are available at their corresponding http endpoints. For example, you could ask for a stream reader histograms like this: ```bash:no-line-numbers curl http://localhost:2113/histogram/reader-streamrange -u admin:changeit ``` That would give a response with the stats distributed across histogram buckets: ``` Value Percentile TotalCount 1/(1-Percentile) 0.022 0.000000000000 1 1.00 0.044 0.100000000000 30 1.11 0.054 0.200000000000 59 1.25 0.074 0.300000000000 88 1.43 0.092 0.400000000000 118 1.67 0.108 0.500000000000 147 2.00 0.113 0.550000000000 162 2.22 0.127 0.600000000000 176 2.50 0.140 0.650000000000 191 2.86 0.155 0.700000000000 206 3.33 0.168 0.750000000000 220 4.00 0.179 0.775000000000 228 4.44 0.197 0.800000000000 235 5.00 0.219 0.825000000000 242 5.71 0.232 0.850000000000 250 6.67 0.277 0.875000000000 257 8.00 0.327 0.887500000000 261 8.89 0.346 0.900000000000 264 10.00 0.522 0.912500000000 268 11.43 0.836 0.925000000000 272 13.33 0.971 0.937500000000 275 16.00 1.122 0.943750000000 277 17.78 1.153 0.950000000000 279 20.00 1.217 0.956250000000 281 22.86 2.836 0.962500000000 283 26.67 2.972 0.968750000000 284 32.00 3.607 0.971875000000 285 35.56 4.964 0.975000000000 286 40.00 8.536 0.978125000000 287 45.71 11.035 0.981250000000 288 53.33 11.043 0.984375000000 289 64.00 11.043 0.985937500000 289 71.11 34.013 0.987500000000 290 80.00 34.013 0.989062500000 290 91.43 41.812 0.990625000000 292 106.67 41.812 0.992187500000 292 128.00 41.812 0.992968750000 292 142.22 41.812 0.993750000000 292 160.00 41.812 0.994531250000 292 182.86 41.812 0.995312500000 292 213.33 41.812 0.996093750000 292 256.00 41.812 0.996484375000 292 284.44 41.878 0.996875000000 293 320.00 41.878 1.000000000000 293 #[Mean = 0.854, StdDeviation = 4.739] #[Max = 41.878, Total count = 293] #[Buckets = 20, SubBuckets = 2048] ``` ### Reading histograms The histogram response tells you some useful metrics like mean, max, standard deviation and also that in 99% of cases reads take about 41.8ms, as in the example above. ### Using histograms You can enable histograms in a development environment and run a specific task to see how it affects the database, telling you where and how the time is spent. Execute a `GET` HTTP call to a cluster node using the `http://:2113/histogram/` path to get a response. Here `2113` is the default external HTTP port. ### Available metrics | Endpoint | Measures time spent | | :------------------------------- | :-------------------------------------------------------- | | `reader-streamrange` | `ReadStreamEventsForward` and `ReadStreamEventsBackwards` | | `writer-flush` | Flushing to disk in the storage writer service | | `chaser-wait` and `chaser-flush` | Storage chaser | | `reader-readevent` | Checking the stream access and reading an event | | `reader-allrange` | `ReadAllEventsForward` and `ReadAllEventsBackward` | | `request-manager` | --- | | `tcp-send` | Sending messages over TCP | | `http-send` | Sending messages over HTTP | ### Enabling histograms Use the option described below to enable histograms. Because collecting histograms uses CPU resources, they are disabled by default. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--enable-histograms` | | YAML | `EnableHistograms` | | Environment variable | `EVENTSTORE_ENABLE_HISTOGRAMS` | **Default**: `false` --- --- url: 'https://docs.kurrent.io/server/v23.10/diagnostics/integrations.md' --- # Monitoring integrations EventStoreDB supports several methods to integrate with external monitoring and observability tools. Those include: * [Prometheus](#prometheus): collect metrics in Prometheus * [Datadog](#datadog): monitor and measure the cluster with Datadog * [ElasticSearch](#elastic): this section describes how to collect EventStoreDB logs in ElasticSearch * [Vector](#vector): collect metrics and logs to your APM tool using Vector ## Vector > Vector is a lightweight and ultra-fast tool for building observability pipelines. > (from Vector website) You can use [Vector] for extracting metrics or logs from your self-managed EventStore server. It's also possible to collect metrics from the Event Store Cloud managed cluster or instance, as long as the Vector agent is running on a machine that has a direct connection to the EventStoreDB server. You cannot, however, fetch logs from Event Store Cloud using your own Vector agent. ### Installation Follow the [installation instructions](https://vector.dev/docs/setup/installation/) provided by Vector to deploy the agent. You can deploy and run it on the same machine where you run EventStoreDB server. If you run EventStoreDB in Kubernetes, you can run Vector as a sidecar for each of the EventStoreDB pods. ### Configuration Each Vector instance needs to be configured with sources and sinks. When configured properly, it will collect information from each source, apply the necessary transformation (if needed), and send the transformed information to the configured sink. [Vector] provides [many different sinks], you most probably will find your preferred monitoring platform among those sinks. #### Collecting metrics There is an official [EventStoreDB source] that you can use to pull relevant metrics from your database. Below you can find an example that you can use in your `vector.toml` configuration file: ```toml [sources.eventstoredb_metrics] type = "eventstoredb_metrics" endpoint = "https://{hostname}:{http_port}/stats" scrape_interval_secs = 3 ``` Here `hostname` is the EventStoreDB node hostname or the cluster DNS name, and `http_port` is the configured HTTP port, which is `2113` by default. #### Collecting logs To collect logs, you can use the [file source] and configure it to target EventStoreDB log file. For log collection, Vector must run on the same machine as EventStoreDB server as it collects the logs from files on the local file system. ```toml [sources.eventstoredb_logs] type = "file" # If you changed the default log location, please update the filepath accordingly. include = ["/var/log/eventstore"] read_from = "end" ``` #### Example In this example, Vector runs on the same machine as EventStoreDB, collects metrics and logs, and then sends them to Datadog. Notice that despite the EventStoreDB HTTP is, in theory, accessible via `localhost`, it won't work if the server SSL certificate doesn't have `localhost` in the certificate CN or SAN. ```toml [sources.eventstoredb_metrics] type = "eventstoredb_metrics" endpoint = "https://node1.esdb.acme.company:2113/stats" scrape_interval_secs = 10 [sources.eventstoredb_logs] type = "file" include = ["/var/log/eventstore"] read_from = "end" [sinks.dd_metrics] type = "datadog_metrics" inputs = ["eventstoredb_metrics"] api_key = "${DD_API_KEY}" default_namespace = "service" [sinks.dd_logs] type = "datadog_logs" inputs = ["sources.eventstoredb_logs"] default_api_key = "${DD_API_KEY}" compression = "gzip" ``` ## Prometheus You can collect EventStoreDB metrics to Prometheus and configure Grafana dashboards to monitor your deployment. Event Store provides Prometheus support out of the box since version 23.6. Refer to [metrics](metrics.md) documentation to learn more. ## Datadog Event Store doesn't provide Datadog integration out of the box, but you can use the community-supported integration to collect EventStoreDB logs and metrics in Datadog. Find out more details about the integration in [Datadog documentation](https://docs.datadoghq.com/integrations/eventstore/). [Vector]: https://vector.dev/docs/ [EventStoreDB source]: https://vector.dev/docs/reference/configuration/sources/eventstoredb_metrics/ [file source]: https://vector.dev/docs/reference/configuration/sources/file/ [many different sinks]: https://vector.dev/docs/reference/configuration/sinks/ [Console]: https://vector.dev/docs/reference/configuration/sinks/console/ ## Elastic Elastic Stack is one of the most popular tools for ingesting and analyzing logs and statistics: * [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/8.2/index.html) was built for advanced filtering and text analysis. * [Filebeat](https://www.elastic.co/guide/en/beats/filebeat/8.2/index.html) allow tailing files efficiently. * [Logstash](https://www.elastic.co/guide/en/logstash/current/getting-started-with-logstash.html) enables log transformations and processing pipelines. * [Kibana](https://www.elastic.co/guide/en/kibana/8.2/index.html) is a dashboard and visualization UI for Elasticsearch data. EventStoreDB exposes structured information through its logs and statistics, allowing straightforward integration with mentioned tooling. ### Logstash Logstash is the plugin based data processing component of the Elastic Stack which sends incoming data to Elasticsearch. It's excellent for building a text-based processing pipeline. It can also gather logs from files (although Elastic recommends now Filebeat for that, see more in the following paragraphs). Logstash needs to either be installed on the EventStoreDB node or have access to logs storage. The processing pipeline can be configured through the configuration file (e.g. `logstash.conf`). This file contains the three essential building blocks: * input - source of logs, e.g. log files, system output, Filebeat. * filter - processing pipeline, e.g. to modify, enrich, tag log data, * output - place where we'd like to put transformed logs. Typically that contains Elasticsearch configuration. See the sample Logstash 8.2 configuration file. It shows how to take the EventStoreDB log files, split them based on the log type (regular and stats) and output them to separate indices to Elasticsearch: ```ruby ####################################################### # EventStoreDB logs file input ####################################################### input { file { path => "/var/log/eventstore/*/log*.json" start_position => "beginning" codec => json } } ####################################################### # Filter out stats from regular logs # add respecting field with log type ####################################################### filter { # check if log path includes "log-stats" # so pattern for stats if [log][file][path] =~ "log-stats" { mutate { add_field => { "log_type" => "stats" } } } else { mutate { add_field => { "log_type" => "logs" } } } } ####################################################### # Send logs to Elastic # Create separate indexes for stats and regular logs # using field defined in the filter transformation ####################################################### output { elasticsearch { hosts => [ "elasticsearch:9200" ] index => 'eventstoredb-%{[log_type]}' } } ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/Logstash/docker-compose.yml). ### Filebeat Logstash was an initial attempt by Elastic to provide a log harvester tool. However, it appeared to have performance limitations. Elastic came up with the [Beats family](https://www.elastic.co/beats/), which allows gathering data from various specialized sources (files, metrics, network data, etc.). Elastic recommends Filebeat as the log collection and shipment tool off the host servers. Filebeat uses a backpressure-sensitive protocol when sending data to Logstash or Elasticsearch to account for higher volumes of data. Filebeat can pipe logs directly to Elasticsearch and set up a Kibana data view. Filebeat needs to either be installed on the EventStoreDB node or have access to logs storage. The processing pipeline can be configured through the configuration file (e.g. `filebeat.yml`). This file contains the three essential building blocks: * input - configuration for file source, e.g. if stored in JSON format. * output - place where we'd like to put transformed logs, e.g. Elasticsearch, Logstash, * setup - additional setup and simple transformations (e.g. Elasticsearch indices template, Kibana data view). See the sample Filebeat 8.2 configuration file. It shows how to take the EventStoreDB log files, output them to Elasticsearch prefixing index with `eventstoredb` and create a Kibana data view: ```yml ####################################################### # EventStoreDB logs file input ####################################################### filebeat.inputs: - type: log paths: - /var/log/eventstore/*/log*.json json.keys_under_root: true json.add_error_key: true ####################################################### # ElasticSearch direct output ####################################################### output.elasticsearch: index: "eventstoredb-%{[agent.version]}" hosts: ["elasticsearch:9200"] ####################################################### # ElasticSearch dashboard configuration # (index pattern and data view) ####################################################### setup.dashboards: enabled: true index: "eventstoredb-*" setup.template: name: "eventstoredb" pattern: "eventstoredb-%{[agent.version]}" ####################################################### # Kibana dashboard configuration ####################################################### setup.kibana: host: "kibana:5601" ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/Filebeat/docker-compose.yml). ### Filebeat with Logstash Even though Filebeat can pipe logs directly to Elasticsearch and do a basic Kibana setup, you'd like to have more control and expand the processing pipeline. That's why for production, it's recommended to use both. Multiple Filebeat instances (e.g. from different EventStoreDB clusters) can collect logs and pipe them to Logstash, which will play an aggregator role. Filebeat can output logs to Logstash, and Logstash can receive and process these logs with the Beats input. Logstash can transform and route logs to Elasticsearch instance(s). In that configuration, Filebeat should be installed on the EventStoreDB node (or have access to file logs) and define Logstash as output. See the sample Filebeat 8.2 configuration file. ```yml ####################################################### # EventStoreDB logs file input ####################################################### filebeat.inputs: - type: log paths: - /var/log/eventstore/*/log*.json json.keys_under_root: true json.add_error_key: true ####################################################### # Logstash output to transform and prepare logs ####################################################### output.logstash: hosts: ["logstash:5044"] ``` Then the sample Logstash 8.2 configuration file will look like the below. It shows how to take the EventStoreDB logs from Filebeat, split them based on the log type (regular and stats) and output them to separate indices to Elasticsearch: ```ruby ####################################################### # Filebeat input ####################################################### input { beats { port => 5044 } } ####################################################### # Filter out stats from regular logs # add respecting field with log type ####################################################### filter { # check if log path includes "log-stats" # so pattern for stats if [log][file][path] =~ "log-stats" { mutate { add_field => { "log_type" => "stats" } } } else { mutate { add_field => { "log_type" => "logs" } } } } ####################################################### # Send logs to Elastic # Create separate indexes for stats and regular logs # using field defined in the filter transformation ####################################################### output { elasticsearch { hosts => [ "elasticsearch:9200" ] index => 'eventstoredb-%{[log_type]}' } } ``` You can play with such configuration through the [sample docker-compose](https://github.com/EventStore/samples/blob/2829b0a90a6488e1eee73fad0be33a3ded7d13d2/Logging/Elastic/FilebeatWithLogstash/docker-compose.yml). --- --- url: 'https://docs.kurrent.io/server/v23.10/diagnostics/logs.md' --- # Database logs EventStoreDB logs its internal operations to the console (stdout) and to log files. The default location of the log files and the way to change it is described [below](#logs-location). There are a few options to change the way how EventStoreDB produces logs and how detailed the logs should be. ::: warning The EventStoreDB logs may contain sensitive information such as stream names, user names, and projection definitions. ::: ## Log format EventStoreDB uses the structured logging in JSON format that is more machine-friendly and can be ingested by vendor-specific tools like Logstash or Datadog agent. Here is how the structured log looks like: ```json { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Debug", "Logger": "ProjectionManager", "Message": "PROJECTIONS: Starting Projections Manager. (Node State : {state})", "EventProperties": { "state": "Master" } } { "PID": "6940", "ThreadID": "15", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Info", "Logger": "ClusterVNodeController", "Message": "========== [{internalHttp}] Sub System '{subSystemName}' initialized.", "EventProperties": { "internalHttp": "127.0.0.1:2112", "subSystemName": "Projections" } } { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.052976Z", "Level": "Debug", "Logger": "MultiStreamMessageWriter", "Message": "PROJECTIONS: Resetting Worker Writer", "EventProperties": {} } { "PID": "6940", "ThreadID": "23", "Date": "2020-06-16T16:14:02.055000Z", "Level": "Debug", "Logger": "ProjectionCoreCoordinator", "Message": "PROJECTIONS: SubComponent Started: {subComponent}", "EventProperties": { "subComponent": "EventReaderCoreService" } } ``` This format is aligned with [Serilog Compact JSON format](https://github.com/serilog/serilog-formatting-compact). ## Logs location Log files are located in `/var/log/eventstore` for Linux and macOS, and in the `logs` subdirectory of the EventStoreDB installation directory on Windows. You can change the log files location using the `Log` configuration option. ::: tip Moving logs to a separate storage might improve the database performance if you keep the default verbose log level. ::: | Format | Syntax | |:---------------------|:-----------------| | Command line | `--log` | | YAML | `Log` | | Environment variable | `EVENTSTORE_LOG` | For example, adding this line to the `eventstore.conf` file will force writing logs to the `/tmp/eventstore/logs` directory: ```text:no-line-numbers Log: /tmp/eventstore/logs ``` ## Log level You can change the level using the `LogLevel` setting: | Format | Syntax | | :------------------- | :--------------------- | | Command line | `--log-level` | | YAML | `LogLevel` | | Environment variable | `EVENTSTORE_LOG_LEVEL` | Acceptable values are: `Default`, `Verbose`, `Debug`, `Information`, `Warning`, `Error`, and `Fatal`. ## Logging options You can tune the EventStoreDB logging further by using the logging options described below. ### Log configuration file Specifies the location of the file which configures the logging levels of various components. | Format | Syntax | | :------------------- | :---------------------- | | Command line | `--log-config` | | YAML | `LogConfig` | | Environment variable | `EVENTSTORE_LOG_CONFIG` | By default, the application directory (and `/etc/eventstore` on Linux and Mac) are checked. You may specify a full path. ### HTTP requests logging EventStoreDB can also log all the incoming HTTP requests, like many HTTP servers do. Requests are logged before being processed, so unsuccessful requests are logged too. Use one of the following ways to enable the HTTP requests logging: | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--log-http-requests` | | YAML | `LogHttpRequests` | | Environment variable | `EVENTSTORE_LOG_HTTP_REQUESTS` | **Default**: `false`, logging HTTP requests is disabled by default. ### Log failed authentication For security monitoring, you can enable logging failed authentication attempts by setting `LogFailedAuthenticationAttempts` setting to true. | Format | Syntax | | :------------------- | :---------------------------------------------- | | Command line | `--log-failed-authentication-attempts` | | YAML | `LogFailedAuthenticationAttempts` | | Environment variable | `EVENTSTORE_LOG_FAILED_AUTHENTICATION_ATTEMPTS` | **Default**: `false` ### Log console format The format of the console logger. Use `Json` for structured log output. | Format | Syntax | | :------------------- | :------------------------------ | | Command line | `--log-console-format` | | YAML | `LogConsoleFormat` | | Environment variable | `EVENTSTORE_LOG_CONSOLE_FORMAT` | Acceptable values are: `Plain`, `Json` **Default**: `Plain` ### Log file size The maximum size of each log file, in bytes. | Format | Syntax | | :------------------- | :------------------------- | | Command line | `--log-file-size` | | YAML | `LogFileSize` | | Environment variable | `EVENTSTORE_LOG_FILE_SIZE` | **Default**: `1GB` ### Log file interval How often to rotate logs. | Format | Syntax | | :------------------- | :----------------------------- | | Command line | `--log-file-interval` | | YAML | `LogFileInterval` | | Environment variable | `EVENTSTORE_LOG_FILE_INTERVAL` | Acceptable values are: `Minute`, `Hour`, `Day`, `Week`, `Month`, `Year` **Default**: `Day` ### Log file retention count How many log files to hold on to. | Format | Syntax | | :------------------- | :------------------------------- | | Command line | `--log-file-retention-count` | | YAML | `LogFileRetentionCount` | | Environment variable | `EVENTSTORE_LOG_RETENTION_COUNT` | **Default**: `31` ### Disable log file You can completely disable logging to a file by changing the `DisableLogFile` option. | Format | Syntax | | :------------------- | :---------------------------- | | Command line | `--disable-log-file` | | YAML | `DisableLogFile` | | Environment variable | `EVENTSTORE_DISABLE_LOG_FILE` | **Default**: `false` --- --- url: 'https://docs.kurrent.io/server/v23.10/diagnostics/metrics.md' --- # Metrics EventStoreDB collects metrics in [prometheus format](https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format), available on the `/metrics` endpoint. Prometheus can be configured to scrape this endpoint directly. The metrics are configured in `metricsconfig.json`. ::: note Native EventStoreDB metrics do not yet contain all of the metrics for Projections and Persistent Subscriptions. To view these in Prometheus you can still use the [Prometheus exporter](https://github.com/marcinbudny/eventstore_exporter). ::: ## Metrics Reference ### Caches #### Cache hits and misses EventStoreDB tracks cache hits/misses metrics for `stream-info` and `chunk` caches. | Time Series | Type | Description | |:---------------------------------------------------------------------------|:-------------------------|:----------------------------------------| | `eventstore_cache_hits_misses{cache=,kind=<"hits"\|"misses">}` | [Counter](#common-types) | Total hits/misses on *CACHE\_NAME* cache | Example Configuration: ```json "CacheHitsMisses": { "StreamInfo": true, "Chunk": false } ``` Example Output: ``` # TYPE eventstore_cache_hits_misses counter eventstore_cache_hits_misses{cache="stream-info",kind="hits"} 104329 1688157489545 eventstore_cache_hits_misses{cache="stream-info",kind="misses"} 117 1688157489545 ``` #### Dynamic Cache Resources Certain caches that EventStoreDB uses are dynamic in nature i.e. their capacity scales up/down during their lifetime. EventStoreDB records metrics for resources being used by each such dynamic cache. | Time Series | Type | Description | | :---------- | :--------- | :---------- | | `eventstore_cache_resources_bytes{cache=,kind=<"capacity"\|"size">}` | [Gauge](#common-types) | Current capacity/size of *CACHE\_NAME* cache in bytes | | `eventstore_cache_resources_entries{cache=,kind="count"}` | [Gauge](#common-types) | Current number of entries in *CACHE\_NAME* cache | Example Configuration: ``` "CacheResources": true ``` Example Output: ``` # TYPE eventstore_cache_resources_bytes gauge # UNIT eventstore_cache_resources_bytes bytes eventstore_cache_resources_bytes{cache="LastEventNumber",kind="capacity"} 50000000 1688157491029 eventstore_cache_resources_bytes{cache="LastEventNumber",kind="size"} 15804 1688157491029 # TYPE eventstore_cache_resources_entries gauge # UNIT eventstore_cache_resources_entries entries eventstore_cache_resources_entries{cache="LastEventNumber",kind="count"} 75 1688157491029 ``` ### Checkpoints | Time Series | Type | Description | | :---------- | :--------- | :---------- | | `eventstore_checkpoints{name=,read="non-flushed"}` | [Gauge](#common-types) | Value for *CHECKPOINT\_NAME* checkpoint| Example Configuration: ```json "Checkpoints": { "Replication": true, "Chaser": false, "Epoch": false, "Index": false, "Proposal": false, "Truncate": false, "Writer": false, "StreamExistenceFilter": false } ``` Example Output: ``` # TYPE eventstore_checkpoints gauge eventstore_checkpoints{name="replication",read="non-flushed"} 613363 1688054162478 ``` ### Events These metrics track events written to and read from the server, including reads from caches. | Time Series | Type | Description | |:-----------------------------------------------------|:-------------------------|:--------------------| | `eventstore_io_bytes{activity="read"}` | [Counter](#common-types) | Event bytes read | | `eventstore_io_events{activity=<"read"\|"written">}` | [Counter](#common-types) | Events read/written | Example Configuration: ```json "Events": { "Read": false, "Written": true } ``` Example Output: ``` # TYPE eventstore_io_events counter # UNIT eventstore_io_events events eventstore_io_events{activity="written"} 320 1687963622074 ``` ### Gossip Measures the round trip latency and processing time of gossip. Usually a node pushes new gossip to other nodes periodically or when its view of the cluster changes. Sometimes nodes pull gossip from each other if there is a suspected network problem. #### Gossip Latency | Time Series | Type | Description | | :---------- | :--------- | :---------- | | `eventstore_gossip_latency_seconds_bucket{activity="pull-from-peer",status=<"successful"\|"failed">,le=}` | [Histogram](#common-types) | Number of gossips pulled from peers with latency less than or equal to *DURATION* in seconds | | `eventstore_gossip_latency_seconds_bucket{activity="push-to-peer",status=<"successful"\|"failed">,le=}` | [Histogram](#common-types) | Number of gossips pushed to peers with latency less than or equal to *DURATION* in seconds | #### Gossip Processing | Time Series | Type | Description | |:---------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------| | `eventstore_gossip_processing_duration_seconds_bucket{``activity="push-from-peer",``status=<"successful"\|"failed">,``le=}` | [Histogram](#common-types) | Number of gossips pushed from peers that took less than or equal to *DURATION* in seconds to process | | `eventstore_gossip_processing_duration_seconds_bucket{``activity="request-from-peer",``status=<"successful"\|"failed">,``le=}` | [Histogram](#common-types) | Number of gossip requests from peers that took less than or equal to *DURATION* in seconds to process | | `eventstore_gossip_processing_duration_seconds_bucket{``activity="request-from-grpc-client",``status=<"successful"\|"failed">,``le=}` | [Histogram](#common-types) | Number of gossip requests from gRPC clients that took less than or equal to *DURATION* in seconds to process | | `eventstore_gossip_processing_duration_seconds_bucket{``activity="request-from-http-client",``status=<"successful"\|"failed">,``le=}` | [Histogram](#common-types) | Number of gossip requests from HTTP clients that took less than or equal to *DURATION* in seconds to process | Example Configuration: ```json "Gossip": { "PullFromPeer": false, "PushToPeer": true, "ProcessingPushFromPeer": false, "ProcessingRequestFromPeer": false, "ProcessingRequestFromGrpcClient": false, "ProcessingRequestFromHttpClient": false } ``` Example Output: ``` # TYPE eventstore_gossip_latency_seconds histogram # UNIT eventstore_gossip_latency_seconds seconds eventstore_gossip_latency_seconds_bucket{activity="push-to-peer",status="successful",le="0.005"} 8 1687972306948 ``` ### Incoming Grpc Calls | Time Series | Type | Description | | :---------- | :--------- | :---------- | | `eventstore_current_incoming_grpc_calls` | [Gauge](#common-types) | Inflight gRPC calls i.e. gRPC requests that have started on the server but not yet stopped | | `eventstore_incoming_grpc_calls{kind="total"}` | [Counter](#common-types) | Total gRPC requests served | | `eventstore_incoming_grpc_calls{kind="failed"}` | [Counter](#common-types) | Total gRPC requests failed | | `eventstore_incoming_grpc_calls{kind="unimplemented"}` | [Counter](#common-types) | Total gRPC requests made to unimplemented methods | | `eventstore_incoming_grpc_calls{kind="deadline-exceeded"}` | [Counter](#common-types) | Total gRPC requests for which deadline have exceeded | Example Configuration: ```json "IncomingGrpcCalls": { "Current": true, "Total": false, "Failed": true, "Unimplemented": false, "DeadlineExceeded": false } ``` Example Output: ``` # TYPE eventstore_current_incoming_grpc_calls gauge eventstore_current_incoming_grpc_calls 1 1687963622074 # TYPE eventstore_incoming_grpc_calls counter eventstore_incoming_grpc_calls{kind="failed"} 1 1687962877623 ``` #### Client protocol gRPC methods In addition, EventStoreDB also records metrics for each of client protocol gRPC methods : `StreamRead`, `StreamAppend`, `StreamBatchAppend`, `StreamDelete` and `StreamTombstone`. They are grouped together according to the mapping defined in the configuration. | Time Series | Type | Description | | :---------- | :--------- | :---------- | | `eventstore_grpc_method_duration_seconds_bucket{activity=