guides/ttl.md

# Time-to-Live (TTL) Support

RocksDB supports automatic expiration of key-value pairs through Time-to-Live (TTL). Keys inserted into a TTL-enabled database will be automatically deleted after a specified duration.

## How TTL Works

When you open a database with TTL enabled:

1. **Timestamp Suffixing**: A 32-bit timestamp (creation time) is automatically appended to each value during `put` operations
2. **Expiration Check**: During compaction, RocksDB checks if `timestamp + ttl < current_time`
3. **Lazy Deletion**: Expired keys are removed only during compaction (not immediately upon expiration)

## Important Behaviors

- **Non-Strict Guarantees**: Keys are guaranteed to exist for at least TTL seconds, but may persist longer until compaction runs
- **Stale Reads**: `get` and iterator operations may return expired entries if compaction hasn't run yet
- **Read-Only Mode**: Opens the database without triggering compactions, so expired keys won't be removed
- **Variable TTL**: Different TTL values can be used across different database opens

## Basic Usage

### Opening a Database with TTL

```erlang
%% Open a database with 1 hour (3600 seconds) TTL
{ok, Db} = rocksdb:open_with_ttl(
    "my_ttl_db",
    [{create_if_missing, true}],
    3600,   % TTL in seconds
    false   % read_only flag
).

%% All standard operations work normally
ok = rocksdb:put(Db, <<"key1">>, <<"value1">>, []),
{ok, <<"value1">>} = rocksdb:get(Db, <<"key1">>, []),

%% Close when done
ok = rocksdb:close(Db).
```

### TTL Expiration Example

```erlang
%% Open with 1 second TTL for demonstration
{ok, Db} = rocksdb:open_with_ttl("ttl_test", [{create_if_missing, true}], 1, false),

%% Insert a key
ok = rocksdb:put(Db, <<"temp_key">>, <<"temp_value">>, []),

%% Key exists immediately
{ok, <<"temp_value">>} = rocksdb:get(Db, <<"temp_key">>, []),

%% Wait for TTL to expire
timer:sleep(2000),

%% Key may still exist (compaction hasn't run)
%% Force compaction to trigger cleanup
ok = rocksdb:compact_range(Db, <<"a">>, <<"z">>, []),

%% Now the key is gone
not_found = rocksdb:get(Db, <<"temp_key">>, []),

ok = rocksdb:close(Db).
```

### Read-Only Mode

```erlang
%% Open in read-only mode - no compactions will run
{ok, Db} = rocksdb:open_with_ttl("my_ttl_db", [], 3600, true),

%% Can read but not write
{ok, Value} = rocksdb:get(Db, <<"key">>, []),

%% Note: Expired keys won't be cleaned up in read-only mode
ok = rocksdb:close(Db).
```

## Column Family Support

### Opening with Multiple Column Families (each with its own TTL)

```erlang
%% Open with column families, each having a different TTL
{ok, Db, [DefaultCF, SessionsCF, CacheCF]} = rocksdb:open_with_ttl_cf(
    "multi_ttl_db",
    [{create_if_missing, true}],
    [
        {"default", [], 86400},      % 24 hours
        {"sessions", [], 3600},      % 1 hour
        {"cache", [], 300}           % 5 minutes
    ],
    false
).

%% Write to different column families
ok = rocksdb:put(Db, DefaultCF, <<"user:1">>, <<"data">>, []),
ok = rocksdb:put(Db, SessionsCF, <<"sess:abc">>, <<"token">>, []),
ok = rocksdb:put(Db, CacheCF, <<"cache:xyz">>, <<"cached">>, []),

ok = rocksdb:close(Db).
```

### Creating a Column Family with TTL

```erlang
%% First open a TTL database
{ok, Db} = rocksdb:open_with_ttl("my_db", [{create_if_missing, true}], 3600, false),

%% Create a new column family with a specific TTL
{ok, NewCF} = rocksdb:create_column_family_with_ttl(
    Db,
    "temp_data",
    [],      % Column family options
    600      % 10 minute TTL
),

%% Use the new column family
ok = rocksdb:put(Db, NewCF, <<"key">>, <<"value">>, []),

ok = rocksdb:close(Db).
```

### Getting and Setting TTL Dynamically

```erlang
{ok, Db, [DefaultCF]} = rocksdb:open_with_ttl_cf(
    "my_db",
    [{create_if_missing, true}],
    [{"default", [], 3600}],
    false
),

%% Get current TTL for a column family
{ok, CurrentTTL} = rocksdb:get_ttl(Db, DefaultCF),
io:format("Current TTL: ~p seconds~n", [CurrentTTL]),

%% Set a new TTL for the column family
ok = rocksdb:set_ttl(Db, DefaultCF, 7200),  % Change to 2 hours

%% Set default TTL for the database
ok = rocksdb:set_ttl(Db, 1800),  % 30 minutes

ok = rocksdb:close(Db).
```

## Alternative: Compaction Filter TTL

For more control over TTL behavior, you can use compaction filters with timestamp-based rules. This is useful when your keys contain embedded timestamps.

```erlang
%% TTL based on timestamp embedded in key
%% Format: {ttl_from_key, Offset, Length, TTLSeconds}
{ok, Db} = rocksdb:open("my_db", [
    {create_if_missing, true},
    {compaction_filter, #{
        rules => [{ttl_from_key, 0, 8, 3600}]  % Read 8 bytes at offset 0 as timestamp
    }}
]),

%% Create keys with embedded timestamps
Timestamp = erlang:system_time(second),
Key = <<Timestamp:64/big, "mydata">>,
ok = rocksdb:put(Db, Key, <<"value">>, []),

ok = rocksdb:close(Db).
```

See the [Compaction Filters Guide](compaction_filters.md) for more details.

## API Reference

### rocksdb:open_with_ttl/4

```erlang
-spec open_with_ttl(Name, DBOpts, TTL, ReadOnly) ->
    {ok, db_handle()} | {error, any()}.
```

Opens a database with TTL support.

| Parameter | Type | Description |
|-----------|------|-------------|
| Name | `file:filename_all()` | Path to the database directory |
| DBOpts | `db_options()` | Database options |
| TTL | `integer()` | Time-to-live in seconds (0 or negative = infinity) |
| ReadOnly | `boolean()` | If true, opens in read-only mode |

### rocksdb:open_with_ttl_cf/4

```erlang
-spec open_with_ttl_cf(Name, DBOpts, CFDescriptors, ReadOnly) ->
    {ok, db_handle(), [cf_handle()]} | {error, any()}.
```

Opens a database with multiple column families, each with its own TTL.

| Parameter | Type | Description |
|-----------|------|-------------|
| Name | `file:filename_all()` | Path to the database directory |
| DBOpts | `db_options()` | Database options |
| CFDescriptors | `[{Name, CFOpts, TTL}]` | List of column family descriptors with TTLs |
| ReadOnly | `boolean()` | If true, opens in read-only mode |

### rocksdb:create_column_family_with_ttl/4

```erlang
-spec create_column_family_with_ttl(DBHandle, Name, CFOpts, TTL) ->
    {ok, cf_handle()} | {error, any()}.
```

Creates a new column family with a specific TTL.

### rocksdb:get_ttl/2

```erlang
-spec get_ttl(DBHandle, CFHandle) -> {ok, integer()} | {error, any()}.
```

Gets the current TTL for a column family.

### rocksdb:set_ttl/2

```erlang
-spec set_ttl(DBHandle, TTL) -> ok | {error, any()}.
```

Sets the default TTL for the database.

### rocksdb:set_ttl/3

```erlang
-spec set_ttl(DBHandle, CFHandle, TTL) -> ok | {error, any()}.
```

Sets the TTL for a specific column family.

## Best Practices

1. **Trigger Compaction for Immediate Cleanup**: If you need expired keys removed immediately, call `rocksdb:compact_range/4`

2. **Use Appropriate TTL Values**: Very short TTLs (< 1 second) may cause excessive data churn

3. **Don't Mix TTL and Non-TTL Opens**: Always use `open_with_ttl` functions to access a TTL database. Using regular `open` will return corrupted values (with timestamp suffix)

4. **Consider Column Family TTLs**: Use different TTLs for different data types by organizing them into column families

5. **Monitor Disk Usage**: Expired keys consume disk space until compaction runs

## Warnings

- **Value Corruption**: Opening a TTL database with regular `rocksdb:open` will return corrupted values because of the timestamp suffix
- **Short TTLs**: Using very small TTL values may delete your entire database quickly
- **No Immediate Expiration**: TTL expiration is lazy - keys persist until compaction