# Timer Stores in Rasa

Rasa maintains one active timer per conversation to track inactivity. To ensure timers persist across server restarts and work in multi-pod deployments, Rasa uses a timer store. Rasa provides built-in timer store implementations to suit different deployment scenarios and requirements.

## InMemoryTimerStore (default)

- **Description**  
`InMemoryTimerStore` is the default timer store and it stores timers in memory. Suitable for development and testing as timers are lost on server restart.

- **Configuration**  
To use the `InMemoryTimerStore` no configuration is needed.

## RedisTimerStore

### Description  
`RedisTimerStore` maintains conversation timers using Redis as a persistence layer. This is the recommended option for production deployments as it supports persistence across server restarts and works in multi-pod environments.

With Redis:

- Timers survive server restarts.
- All pods in a horizontally-scaled deployment share the same timer state.
- Timers that expired while the server was down are recovered and processed on startup.

### Configuration  
To set up the timer store with Redis the following steps are required:

1. Add required configuration to your `endpoints.yml`
    
```yaml
timer_store:
       type: "redis"
       host: <host of the redis instance, e.g. localhost>
       port: <port of your redis instance, usually 6379>
       username: <username used for authentication>
       password: <password used for authentication>
       db: <number of your database within redis, e.g. 0. Not used in cluster mode>
       key_prefix: <alphanumeric value to prepend to timer store keys>
       socket_timeout: <time in seconds after which redis commands time out, default 10>
       poll_interval: <interval in seconds between polling for expired timers, default 1.0>
       # transit encryption parameters
       use_ssl: <whether or not the communication is encrypted, default false>
       ssl_keyfile: <path to an ssl private key, only used if use_ssl is true>
       ssl_certfile: <path to an ssl certificate, only used if use_ssl is true>
       ssl_ca_certs: <path to a file of concatenated CA certificates in PEM format, only used if use_ssl is true>
       # high availability parameters
       deployment_mode: <standard, cluster, or sentinel>
       endpoints: <list of redis cluster/sentinel node addresses in the format host:port>
       sentinel_service: <name of the redis sentinel service, only used in sentinel mode>
```

## Fallback Timer Store

If Redis is configured but becomes unavailable, Rasa automatically falls back to the `InMemoryTimerStore`. Sessions continue to be managed without crashing, and a warning is logged when the fallback is activated. When Redis recovers, Rasa resumes using it and logs that the fallback has been deactivated.

## Custom Timer Stores

If you need a timer store which is not available out of the box, you can implement your own. To write a custom timer store, extend `SessionTimerStore` from `rasa.core.timer_stores.timer_store`. Your constructor must accept an `endpoint_config` parameter:

```python
from rasa.core.timer_stores.timer_store import SessionTimerStore
from rasa.utils.endpoints import EndpointConfig

class MyCustomTimerStore(SessionTimerStore):
  def __init__(self, endpoint_config: EndpointConfig = None):
      ...
```

Your custom timer store class must implement all of the following methods:

- `close`: releases any resources (connections, file handles, etc.) held by the store.  
Must respect the following signature:

```python
  def close(self) -> None:
    """Close and clean up the timer store."""
```

- `store_timer`: persists a timer entry for a conversation. Must respect the following signature:

```python
  async def store_timer(
    self,
    sender_id: str,
    session_id: Optional[str],
    scheduled_time: float,
    metadata: Optional[Dict[str, Any]] = None,
  ) -> None:
    """Store a session timer for the given conversation.

Args:
        sender_id: The conversation ID.
        session_id: The current session ID, used to validate the timer
            has not been superseded when it fires.
        scheduled_time: Unix timestamp (seconds) when the timer should fire.
        metadata: Optional key-value data to attach to the timer.
    """
```

- `delete_timer`: removes a timer entry. Must respect the following signature:

```python
  async def delete_timer(
    self,
    sender_id: str,
    only_if_scheduled_time: Optional[float] = None,
  ) -> bool:
    """Delete the timer for the given conversation.

Args:
        sender_id: The conversation ID.
        only_if_scheduled_time: If set, delete only when the stored timer's
            scheduled_time equals this value (for atomic multi-pod claiming).
            If None, delete unconditionally.

Returns:
        True if a timer was found and deleted, False otherwise.
    """
```

- `get_timer`: retrieves the current timer for a conversation, or None if none exists.  
Must respect the following signature:

```python
  async def get_timer(self, sender_id: str) -> Optional[SessionTimer]:
    """Get the timer for the given conversation.

Args:
        sender_id: The conversation ID.

Returns:
        The SessionTimer if one exists for this conversation, None otherwise.
    """
```

- `get_expired_timers`: returns all timers whose `scheduled_time` is at or before `cutoff_time`.  
Must respect the following signature:

```python
  async def get_expired_timers(
    self, cutoff_time: Optional[float] = None
  ) -> List[SessionTimer]:
    """Return all timers that have expired.

Args:
        cutoff_time: Unix timestamp threshold. Timers with
            scheduled_time <= cutoff_time are returned.
            Defaults to the current time if None.

Returns:
        List of expired SessionTimer objects. Must not delete them.
    """
```

The base class provides two helpers that can be used by the `get_expired_timers` method:

- `_check_time_for_expired(cutoff_time)`: returns `cutoff_time` if set, otherwise `time.time()`.
- `_filter_expired(timers, check_time)`: filters an iterable of `SessionTimer` objects to those with `scheduled_time <= check_time`.

Another built-in helper method available for use in your custom timer store is:

- `SessionTimer.create_timer(sender_id, session_id, scheduled_time, metadata)`: constructs a `SessionTimer` from the arguments passed to `store_timer`.

### Configuration

Put the module path to your custom timer store class in `endpoints.yml` under the `timer_store` key. Any additional fields are passed through as part of the `EndpointConfig` and are accessible via `endpoint_config.kwargs`:

```yaml
timer_store:
  type: path.to.your.module.MyCustomTimerStore
  a_parameter: a value
  another_parameter: another value
```
