HGF.ClickHouse
0.3.2
dotnet add package HGF.ClickHouse --version 0.3.2
NuGet\Install-Package HGF.ClickHouse -Version 0.3.2
<PackageReference Include="HGF.ClickHouse" Version="0.3.2" />
<PackageVersion Include="HGF.ClickHouse" Version="0.3.2" />
<PackageReference Include="HGF.ClickHouse" />
paket add HGF.ClickHouse --version 0.3.2
#r "nuget: HGF.ClickHouse, 0.3.2"
#:package HGF.ClickHouse@0.3.2
#addin nuget:?package=HGF.ClickHouse&version=0.3.2
#tool nuget:?package=HGF.ClickHouse&version=0.3.2
HangfireCH
ClickHouse job storage for Hangfire. It implements the full
Hangfire storage SPI (JobStorage, connection, write-only transaction, monitoring API),
a polling job queue, and background maintenance (record expiration and counter
aggregation), on top of the Octonica ClickHouse client.
Heads up — ClickHouse is an OLAP column store, not an OLTP database. It has no ACID transactions, no row-level locks, and
UPDATE/DELETEare asynchronous mutations. This provider therefore models mutable state with append-only inserts intoReplacingMergeTreetables (latest version wins, read withargMax) and treats the job queue and distributed locks as best-effort / at-least-once. That matches Hangfire's own delivery model (jobs should be idempotent; Hangfire re-dispatches after a worker times out), but if you need strict exactly-once OLTP semantics, a transactional store such as SQL Server, PostgreSQL or Redis is a better fit. See Design & guarantees.
Install
dotnet add package HGF.ClickHouse
Targets net10.0 and net8.0. The NuGet package ID is HGF.ClickHouse (the Hangfire.*
prefix is reserved on nuget.org); the assembly and namespace are HangfireCH. Use
using HangfireCH; for the types and UseClickHouseStorage(...) (the extension lives in the
Hangfire namespace).
Usage
using Hangfire;
using HangfireCH;
GlobalConfiguration.Configuration
.UseClickHouseStorage("Host=localhost;Port=9000;User=default;Database=hangfire");
// ASP.NET Core
builder.Services.AddHangfire(cfg => cfg
.UseClickHouseStorage(
"Host=localhost;Port=9000;User=default;Database=hangfire",
new ClickHouseStorageOptions
{
QueuePollInterval = TimeSpan.FromSeconds(5),
InvisibilityTimeout = TimeSpan.FromMinutes(30),
JobExpirationCheckInterval = TimeSpan.FromMinutes(30),
CountersAggregateInterval = TimeSpan.FromMinutes(5),
}));
builder.Services.AddHangfireServer();
The connection string is the Octonica format (native protocol, default port 9000):
Host=…;Port=9000;User=…;Password=…;Database=….
Options
| Option | Default | Meaning |
|---|---|---|
DatabaseName |
from connection string | Database the tables live in; created if missing. |
TablePrefix |
"" |
Prefix applied to every table name. |
PrepareSchemaIfNecessary |
true |
Create/verify the schema on startup. |
QueuePollInterval |
15s |
How often the queue is polled for work. |
InvisibilityTimeout |
30m |
How long a fetched job stays invisible before recovery. |
JobExpirationCheckInterval |
30m |
Expiration manager run interval. |
CountersAggregateInterval |
5m |
Counter aggregation interval. |
DistributedLockExpiration |
30m |
TTL for an acquired distributed lock (dead-lock guard). |
BatchWrites |
true |
Send a transaction's same-table inserts as one multi-row INSERT (fewer MergeTree parts). |
UseKeeperMap |
false |
Use the linearizable KeeperMap engine for the lock + queue claim (requires ClickHouse Keeper). |
KeeperMapPathPrefix |
/ |
Keeper path prefix for KeeperMap tables; must match the server's keeper_map_path_prefix. |
ConnectionPoolSize |
32 |
Max pooled ClickHouse connections. |
MutationsSync |
1 |
lightweight_deletes_sync level for the expiration manager (0=async, 1=current, 2=all). |
Design & guarantees
- Jobs / state / parameters / expiration are stored across
ReplacingMergeTreetables keyed by id. A "mutation" (state change, expire, persist, set parameter) is a plainINSERTof a new version row carrying a monotonicver; reads resolve the current value withargMax(col, ver), so they are correct even before background merges collapse old versions. - State history is an append-only
statetable. - Queue: dequeue is an optimistic claim — select the oldest visible entry, insert a
claim row stamped with a unique token, then read back the winning token. Combined with
InvisibilityTimeout, a crashed worker's job becomes visible again. Under heavy contention this is at-least-once (a job may rarely be handed to two workers); keep jobs idempotent. - Distributed locks use the same optimistic-claim pattern with a TTL, so they are best-effort mutual exclusion rather than a hard lock.
- Stronger guarantees (opt-in): set
UseKeeperMap = true(and configure ClickHouse Keeper +keeper_map_path_prefixon the server) to back the lock and the queue claim with the linearizableKeeperMapengine — true mutual exclusion and an atomic dequeue. The storage falls back to the optimistic path when this is off. - Writes are batched per transaction (
BatchWrites) into multi-row inserts to limit MergeTree part growth.async_insertis deliberately not used — Octonica passes parameters as temporary tables that don't survive ClickHouse's deferred async-insert execution. - Partitioning:
job,job_queue,state, andcounterare partitioned by insert-time month. Per-job retention is dynamic, so expiration staysDELETE-based (noDROP PARTITION, which would drop still-live old jobs). - Expiration: every expirable record carries
expire_at. A native ClickHouseTTLreclaims space on merges, and the expiration manager additionally issues lightweightDELETEs on the configured interval. - Counters are append-only deltas summed at read time; the counter aggregator folds
them into an
aggregated_countertable under a storage-wide distributed lock.
Schema
All tables are created in DatabaseName (or the connection's database) with TablePrefix:
schema, job, job_state, job_expiration, job_parameter, state, job_queue,
server, hash, list, set, counter, aggregated_counter, distributed_lock.
Development & tests
Integration tests run against a real ClickHouse spun up with Testcontainers (Docker required):
dotnet test
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Hangfire.Core (>= 1.8.23)
- Newtonsoft.Json (>= 13.0.3)
- Octonica.ClickHouseClient (>= 4.1.4)
-
net8.0
- Hangfire.Core (>= 1.8.23)
- Newtonsoft.Json (>= 13.0.3)
- Octonica.ClickHouseClient (>= 4.1.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
0.3.2: harden against server-side defaults — pin insert_deduplicate=0 (no silent block dedup on replicated clusters) and session_timezone=UTC per connection, and let the dashboard's analytical FINAL scans opt out of max_execution_time. 0.3.1: fix "Unknown table … While executing WaitForAsyncInsert" on servers that default async_insert=1 — the storage now forces async_insert=0 per connection (Octonica parameterized inserts can't survive deferred async execution). 0.3.0: assembly and namespace renamed to HangfireCH (was Hangfire.ClickHouse) so package, assembly, and namespace match — update `using Hangfire.ClickHouse;` to `using HangfireCH;`. 0.2.0: client-side write batching, table partitioning, opt-in KeeperMap lock/queue, FINAL-based dashboard reads, logging, and more options (BatchWrites, UseKeeperMap, ConnectionPoolSize, MutationsSync). Schema v2 (fresh installs). See CHANGELOG.md. Pre-1.0 — API/schema may still change.