Skip to content

Commit 3e1b811

Browse files
Merge pull request #41 from cmdscale/feature/data_retention_policy
Feature/data retention policy
2 parents 931960c + 1483ad2 commit 3e1b811

61 files changed

Lines changed: 8025 additions & 339 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmarks/Eftdb.Benchmarks/Eftdb.Benchmarks.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
<ItemGroup>
1717
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
18-
<PackageReference Include="Testcontainers.PostgreSql" Version="4.9.0" />
19-
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="10.105.0" />
18+
<PackageReference Include="Testcontainers.PostgreSql" Version="4.11.0" />
19+
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="10.105.3" />
2020
</ItemGroup>
2121

2222
<ItemGroup>

benchmarks/Eftdb.Benchmarks/WriteRecordsBenchmarkBase.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ public abstract class WriteRecordsBenchmarkBase<T> where T : class
1111
public int MaxBatchSize;
1212
public int NumberOfWorkers;
1313

14-
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
15-
.WithImage("timescale/timescaledb:latest-pg17")
14+
private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder("timescale/timescaledb:latest-pg17")
1615
.WithDatabase("benchmark_tests_db")
1716
.WithUsername("test_user")
1817
.WithPassword("test_password")
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Retention Policies
2+
3+
A retention policy automatically drops old chunks from a hypertable or continuous aggregate on a scheduled basis. This keeps storage consumption bounded without requiring manual intervention and is the standard approach for managing time-series data lifecycle in TimescaleDB.
4+
5+
Each hypertable or continuous aggregate supports at most one retention policy.
6+
7+
[See also: add_retention_policy](https://docs.tigerdata.com/api/latest/data_retention/add_retention_policy/)
8+
9+
## Drop Modes
10+
11+
Two mutually exclusive drop modes are available:
12+
13+
- **`DropAfter`**: Drops chunks whose data falls outside a time window relative to the current time. This is the standard mode.
14+
- **`DropCreatedBefore`**: Drops chunks created before a specified interval ago, regardless of the data they contain.
15+
16+
Exactly one of `DropAfter` or `DropCreatedBefore` must be specified. Providing both or neither raises an exception.
17+
18+
## Basic Example
19+
20+
Here is a complete example of configuring a retention policy on an `ApplicationLog` hypertable using `DropAfter`.
21+
22+
```csharp
23+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
24+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
25+
using Microsoft.EntityFrameworkCore;
26+
27+
[Hypertable(nameof(Time), ChunkTimeInterval = "1 day")]
28+
[PrimaryKey(nameof(Id), nameof(Time))]
29+
[RetentionPolicy("30 days")]
30+
public class ApplicationLog
31+
{
32+
public Guid Id { get; set; }
33+
public DateTime Time { get; set; }
34+
public string ServiceName { get; set; } = string.Empty;
35+
public string Level { get; set; } = string.Empty;
36+
public string Message { get; set; } = string.Empty;
37+
}
38+
```
39+
40+
## Using `DropCreatedBefore`
41+
42+
Pass `null` as the first argument and provide `dropCreatedBefore` as a named argument:
43+
44+
```csharp
45+
[Hypertable(nameof(Time), ChunkTimeInterval = "1 day")]
46+
[PrimaryKey(nameof(Id), nameof(Time))]
47+
[RetentionPolicy(dropCreatedBefore: "30 days")]
48+
public class ApiRequestLog
49+
{
50+
public Guid Id { get; set; }
51+
public DateTime Time { get; set; }
52+
public string Path { get; set; } = string.Empty;
53+
public int StatusCode { get; set; }
54+
}
55+
```
56+
57+
> :warning: **Note:** Due to a known bug in TimescaleDB ([#9446](https://github.com/timescale/timescaledb/issues/9446)), `alter_job` fails when used with `DropCreatedBefore` policies. The library works around this by skipping the `alter_job` call for `DropCreatedBefore` policies. As a result, job scheduling parameters (`ScheduleInterval`, `MaxRuntime`, `MaxRetries`, `RetryPeriod`) are accepted by the API but have no effect at the database level when `DropCreatedBefore` is used.
58+
59+
## Complete Example
60+
61+
```csharp
62+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
63+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
64+
using Microsoft.EntityFrameworkCore;
65+
66+
[Hypertable(nameof(Time), ChunkTimeInterval = "1 day")]
67+
[PrimaryKey(nameof(Id), nameof(Time))]
68+
[RetentionPolicy("30 days",
69+
InitialStart = "2025-10-01T03:00:00Z",
70+
ScheduleInterval = "1 day",
71+
MaxRuntime = "30 minutes",
72+
MaxRetries = 3,
73+
RetryPeriod = "5 minutes")]
74+
public class ApplicationLog
75+
{
76+
public Guid Id { get; set; }
77+
public DateTime Time { get; set; }
78+
public string ServiceName { get; set; } = string.Empty;
79+
public string Level { get; set; } = string.Empty;
80+
public string Message { get; set; } = string.Empty;
81+
}
82+
```
83+
84+
## Supported Parameters
85+
86+
| Parameter | Description | Type | Database Type | Default Value |
87+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ------------- | ------------------------------------------- |
88+
| `DropAfter` | The interval after which chunks are dropped. Mutually exclusive with `DropCreatedBefore`. Can be passed as the first positional argument. | `string?` | `INTERVAL` ||
89+
| `DropCreatedBefore` | The interval before which chunks created are dropped. Based on chunk creation time. Only supports `INTERVAL`. Not available for integer-based time columns. Mutually exclusive with `DropAfter`. | `string?` | `INTERVAL` ||
90+
| `InitialStart` | The first time the policy job is scheduled to run, specified as a UTC date-time string in ISO 8601 format. If `null`, the first run is based on the `ScheduleInterval`. | `string?` | `TIMESTAMPTZ` | `null` |
91+
| `ScheduleInterval` | The interval at which the retention policy job runs. | `string?` | `INTERVAL` | `'1 day'` |
92+
| `MaxRuntime` | The maximum amount of time the job is allowed to run before being stopped. If `null`, there is no time limit. | `string?` | `INTERVAL` | `'00:00:00'` |
93+
| `MaxRetries` | The number of times the job is retried if it fails. | `int` | `INTEGER` | `-1` |
94+
| `RetryPeriod` | The amount of time the scheduler waits between retries of a failed job. | `string?` | `INTERVAL` | Equal to the `scheduleInterval` by default. |
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Retention Policies
2+
3+
A retention policy automatically drops old chunks from a hypertable or continuous aggregate on a scheduled basis. This keeps storage consumption bounded without requiring manual intervention and is the standard approach for managing time-series data lifecycle in TimescaleDB.
4+
5+
Each hypertable or continuous aggregate supports at most one retention policy.
6+
7+
[See also: add_retention_policy](https://docs.tigerdata.com/api/latest/data_retention/add_retention_policy/)
8+
9+
## Drop Modes
10+
11+
Two mutually exclusive drop modes are available:
12+
13+
- **`dropAfter`**: Drops chunks whose data falls outside a time window relative to the current time. This is the standard mode.
14+
- **`dropCreatedBefore`**: Drops chunks created before a specified interval ago, regardless of the data they contain.
15+
16+
Exactly one of `dropAfter` or `dropCreatedBefore` must be specified. Providing both or neither raises an exception.
17+
18+
## Basic Example
19+
20+
```csharp
21+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
22+
using Microsoft.EntityFrameworkCore;
23+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
24+
25+
public class ApplicationLogConfiguration : IEntityTypeConfiguration<ApplicationLog>
26+
{
27+
public void Configure(EntityTypeBuilder<ApplicationLog> builder)
28+
{
29+
builder.HasKey(x => new { x.Id, x.Time });
30+
31+
builder.IsHypertable(x => x.Time)
32+
.WithChunkTimeInterval("1 day");
33+
34+
// Drop chunks older than 30 days, running the job daily
35+
builder.WithRetentionPolicy(
36+
dropAfter: "30 days",
37+
scheduleInterval: "1 day");
38+
}
39+
}
40+
```
41+
42+
## Using `dropCreatedBefore`
43+
44+
```csharp
45+
public class ApiRequestLogConfiguration : IEntityTypeConfiguration<ApiRequestLog>
46+
{
47+
public void Configure(EntityTypeBuilder<ApiRequestLog> builder)
48+
{
49+
builder.HasKey(x => new { x.Id, x.Time });
50+
51+
builder.IsHypertable(x => x.Time)
52+
.WithChunkTimeInterval("1 day");
53+
54+
// Drop chunks created more than 30 days ago
55+
builder.WithRetentionPolicy(
56+
dropCreatedBefore: "30 days",
57+
scheduleInterval: "1 day");
58+
}
59+
}
60+
```
61+
62+
> :warning: **Note:** Due to a known bug in TimescaleDB ([#9446](https://github.com/timescale/timescaledb/issues/9446)), `alter_job` fails when used with `drop_created_before` policies. The library works around this by skipping the `alter_job` call for `drop_created_before` policies. As a result, job scheduling parameters (`scheduleInterval`, `maxRuntime`, `maxRetries`, `retryPeriod`) are accepted by the API but have no effect at the database level when `dropCreatedBefore` is used.
63+
64+
## Complete Example
65+
66+
```csharp
67+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
68+
using Microsoft.EntityFrameworkCore;
69+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
70+
71+
public class ApplicationLogConfiguration : IEntityTypeConfiguration<ApplicationLog>
72+
{
73+
public void Configure(EntityTypeBuilder<ApplicationLog> builder)
74+
{
75+
builder.HasKey(x => new { x.Id, x.Time });
76+
77+
builder.IsHypertable(x => x.Time)
78+
.WithChunkTimeInterval("1 day");
79+
80+
builder.WithRetentionPolicy(
81+
dropAfter: "30 days",
82+
initialStart: new DateTime(2025, 10, 1, 3, 0, 0, DateTimeKind.Utc),
83+
scheduleInterval: "1 day",
84+
maxRuntime: "30 minutes",
85+
maxRetries: 3,
86+
retryPeriod: "5 minutes");
87+
}
88+
}
89+
90+
public class ApplicationLog
91+
{
92+
public Guid Id { get; set; }
93+
public DateTime Time { get; set; }
94+
public string ServiceName { get; set; } = string.Empty;
95+
public string Level { get; set; } = string.Empty;
96+
public string Message { get; set; } = string.Empty;
97+
}
98+
```
99+
100+
## Supported Parameters
101+
102+
| Parameter | Description | Type | Database Type | Default Value |
103+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ------------- | ------------------------------------------- |
104+
| `dropAfter` | The interval after which chunks are dropped. Mutually exclusive with `dropCreatedBefore`. | `string?` | `INTERVAL` ||
105+
| `dropCreatedBefore` | The interval before which chunks created are dropped. Based on chunk creation time. Only supports `INTERVAL`. Not available for integer-based time columns. Mutually exclusive with `dropAfter`. | `string?` | `INTERVAL` ||
106+
| `initialStart` | The first time the policy job is scheduled to run, as a UTC `DateTime`. If `null`, the first run is based on the `scheduleInterval`. | `DateTime?` | `TIMESTAMPTZ` | `null` |
107+
| `scheduleInterval` | The interval at which the retention policy job runs. | `string?` | `INTERVAL` | `'1 day'` |
108+
| `maxRuntime` | The maximum amount of time the job is allowed to run before being stopped. If `null`, there is no time limit. | `string?` | `INTERVAL` | `'00:00:00'` |
109+
| `maxRetries` | The number of times the job is retried if it fails. | `int?` | `INTEGER` | `-1` |
110+
| `retryPeriod` | The amount of time the scheduler waits between retries of a failed job. | `string?` | `INTERVAL` | Equal to the `scheduleInterval` by default. |
Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFramework>net10.0</TargetFramework>
6-
<ImplicitUsings>enable</ImplicitUsings>
7-
<Nullable>enable</Nullable>
8-
<AssemblyName>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.CodeFirst</AssemblyName>
9-
<RootNamespace>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.CodeFirst</RootNamespace>
10-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<AssemblyName>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.CodeFirst</AssemblyName>
9+
<RootNamespace>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.CodeFirst</RootNamespace>
10+
</PropertyGroup>
1111

12-
<ItemGroup>
13-
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0-rc.2" />
14-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
15-
</ItemGroup>
12+
<ItemGroup>
13+
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
14+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
15+
</ItemGroup>
1616

17-
<ItemGroup>
18-
<ProjectReference Include="..\Eftdb.Samples.Shared\Eftdb.Samples.Shared.csproj" />
19-
<ProjectReference Include="..\..\src\Eftdb.Design\Eftdb.Design.csproj" />
20-
</ItemGroup>
17+
<ItemGroup>
18+
<ProjectReference Include="..\Eftdb.Samples.Shared\Eftdb.Samples.Shared.csproj" />
19+
<ProjectReference Include="..\..\src\Eftdb.Design\Eftdb.Design.csproj" />
20+
</ItemGroup>
2121

22-
<ItemGroup>
23-
<Content Include="appsettings.json">
24-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25-
</Content>
26-
</ItemGroup>
22+
<ItemGroup>
23+
<Content Include="appsettings.json">
24+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25+
</Content>
26+
</ItemGroup>
2727

2828
</Project>
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<ItemGroup>
4-
<ProjectReference Include="..\..\src\Eftdb.Design\Eftdb.Design.csproj" />
5-
</ItemGroup>
3+
<ItemGroup>
4+
<ProjectReference Include="..\..\src\Eftdb.Design\Eftdb.Design.csproj" />
5+
</ItemGroup>
66

7-
<PropertyGroup>
8-
<TargetFramework>net10.0</TargetFramework>
9-
<ImplicitUsings>enable</ImplicitUsings>
10-
<Nullable>enable</Nullable>
11-
<AssemblyName>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.DatabaseFirst</AssemblyName>
12-
<RootNamespace>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.DatabaseFirst</RootNamespace>
13-
</PropertyGroup>
7+
<PropertyGroup>
8+
<TargetFramework>net10.0</TargetFramework>
9+
<ImplicitUsings>enable</ImplicitUsings>
10+
<Nullable>enable</Nullable>
11+
<AssemblyName>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.DatabaseFirst</AssemblyName>
12+
<RootNamespace>CmdScale.EntityFrameworkCore.TimescaleDB.Samples.DatabaseFirst</RootNamespace>
13+
</PropertyGroup>
1414

1515
</Project>

samples/Eftdb.Samples.DatabaseFirst/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
This project demonstrates how to use the **Database-First** approach with [TimescaleDB](https://www.timescale.com/) using the `CmdScale.EntityFrameworkCore.TimescaleDB` package.
44

5+
6+
> [!WARNING]
7+
> Currently the `dotnet ef dbcontext scaffold` command can't be tested because of an issue in `efcore` (see https://github.com/dotnet/efcore/issues/37201).
8+
> <br />TODO: Test as soon as there is a fix available
9+
510
---
611

712
## Required NuGet Packages
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
2+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregate;
3+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregatePolicy;
4+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
5+
using CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models;
6+
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
8+
9+
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Configurations
10+
{
11+
public class ApiRequestAggregateConfiguration : IEntityTypeConfiguration<ApiRequestAggregate>
12+
{
13+
public void Configure(EntityTypeBuilder<ApiRequestAggregate> builder)
14+
{
15+
builder.HasNoKey();
16+
builder.IsContinuousAggregate<ApiRequestAggregate, ApiRequestLog>("api_request_hourly_stats", "1 hour", x => x.Time, true)
17+
.AddAggregateFunction(x => x.AverageDurationMs, x => x.DurationMs, EAggregateFunction.Avg)
18+
.AddAggregateFunction(x => x.MaxDurationMs, x => x.DurationMs, EAggregateFunction.Max)
19+
.AddAggregateFunction(x => x.MinDurationMs, x => x.DurationMs, EAggregateFunction.Min)
20+
.AddGroupByColumn(x => x.ServiceName)
21+
.WithRefreshPolicy(startOffset: "2 days", endOffset: "1 hour", scheduleInterval: "1 hour");
22+
builder.WithRetentionPolicy(
23+
dropAfter: "90 days",
24+
scheduleInterval: "1 day",
25+
maxRetries: 3,
26+
retryPeriod: "15 minutes");
27+
}
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.Hypertable;
2+
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.RetentionPolicy;
3+
using CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
6+
7+
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Configurations
8+
{
9+
public class ApiRequestLogConfiguration : IEntityTypeConfiguration<ApiRequestLog>
10+
{
11+
public void Configure(EntityTypeBuilder<ApiRequestLog> builder)
12+
{
13+
builder.ToTable("ApiRequestLogs");
14+
builder.HasNoKey()
15+
.IsHypertable(x => x.Time)
16+
.WithChunkTimeInterval("1 day");
17+
builder.WithRetentionPolicy(
18+
dropCreatedBefore: "30 days",
19+
scheduleInterval: "1 day",
20+
maxRetries: 5,
21+
retryPeriod: "10 minutes");
22+
}
23+
}
24+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace CmdScale.EntityFrameworkCore.TimescaleDB.Samples.Shared.Models
2+
{
3+
public class ApiRequestAggregate
4+
{
5+
public DateTime TimeBucket { get; set; }
6+
public string ServiceName { get; set; } = string.Empty;
7+
public double AverageDurationMs { get; set; }
8+
public double MaxDurationMs { get; set; }
9+
public double MinDurationMs { get; set; }
10+
}
11+
}

0 commit comments

Comments
 (0)