One execution model. Many heterogeneous operations.
HertaSDK is an in-process execution contract runtime for Go. Operations stay ordinary Go functions and declare what they consume, what side effects they produce, and what failure means — the runtime validates those contracts and arbitrates shared local capacity across otherwise-independent subsystems.
Status. HertaSDK v0.2.0 is an installable Go module:
go get github.com/arahe-dev/hertasdk@v0.2.0The V0 execution contract is frozen:
Wait/Rejectadmission, shared weighted resources, keyed serialization,Effect/Outcomefailure semantics, bounded safe retries, cooperative timeouts, shutdown/drain, and atomic stats. Queue is explicitly NOT part of V0. See ROADMAP.md and CHANGELOG.md.
- What is HertaSDK?
- Why?
- Core model
- Resource arbitration
- Keyed serialization
- Failure semantics
- Lifecycle
- What Herta is NOT
- Quickstart (real API)
- Current validation
- Roadmap
- Design principles
- Name / inspiration
- Docs
- Contributing / Security
- License
HertaSDK is a local, in-process runtime that owns execution policy — not business logic. An operation is a typed wrapper around a normal handler; the operation declares its execution contract and Herta enforces it:
- Operations remain normal Go functions. Herta wraps them; it never owns what they compute, store, or return.
- Herta owns execution policy. Admission, shared resource budgets, keyed serialization, timeouts, retry safety, and shutdown belong to the runtime.
- Contracts are declared up front. Resources consumed, side-effect class, contention behavior, and failure meaning are part of construction — invalid combinations are rejected before serving, not discovered at 3am.
- Arbitration is shared. Independent subsystems (render, events, catalogue, webhooks, model calls) draw from the same named local budgets instead of each inventing its own semaphore, pool, and retry loop.
Start with docs/concepts.md and docs/execution-model.md.
Without a shared model, every subsystem invents slightly different execution plumbing:
| Subsystem | Reinvented plumbing |
|---|---|
| Render | own semaphore / retry logic |
| DB writer | own pool / ordering logic |
| Webhook | own retry logic |
| Catalogue | own per-key locking |
| Model call | own quota handling |
Each one is subtly different, subtly wrong in a different way, and impossible to reason about as a whole.
With Herta, shared policy and shared resources stay explicit:
flowchart LR
Render --> Herta["Herta Runtime"]
Events --> Herta
Catalogue --> Herta
Webhooks --> Herta
ModelCalls["Model calls"] --> Herta
Herta --> Handlers["normal handlers"]
Herta does not replace application-level business logic. It replaces the five slightly-different semaphores, the four slightly-different retry loops, and the three slightly-different shutdown paths with one contract the whole process obeys.
Like nodes on a CAN bus, each worker runs asynchronously at its own pace — different speeds, different shapes — but all obey the same arbitration and error semantics:
| Primitive | Meaning |
|---|---|
Runtime |
Execution authority and lifecycle owner. Owns budgets, admission, shutdown. |
Operation[I, O] |
Typed wrapper around a normal handler. Carries a Policy. |
Resource |
A named finite local capacity (e.g. renderer, db-write). Weighted. |
Policy |
Resources, admission (Wait/Reject), timeout, retry, optional SerializeBy(key). |
Effect |
Side-effect class: Pure, Idempotent, NonIdempotent. The zero value is EffectUnknown — omitting Effect is a construction error (ErrEffectRequired), never a silent claim of safety. |
Outcome |
Failure meaning: Success, Transient, Permanent, Throttled, Uncertain. |
Effect says whether repeating is safe. Outcome says what happened. Retry decisions require both — see Failure semantics and docs/failure-semantics.md.
This is the defining feature — and it is more than "Herta has a semaphore."
Different operations may consume the same resource. A renderer budget of 8
is shared by every render call regardless of caller; a db-write budget is
shared by events and catalogue writes alike.
Concrete behavior, measured in the internal reference implementation
(renderer capacity = 8, 20 concurrent render calls, measured peak inside
the provider: exactly 8):
flowchart LR
subgraph callers ["20 concurrent render calls"]
direction TB
R1["render ×20"]
end
R1 --> Herta["Herta Runtime<br/>renderer capacity = 8"]
Herta -->|"at most 8 inside<br/>peak measured: exactly 8"| Provider["provider"]
And across subsystems sharing one budget:
flowchart LR
Events --> DB["db-write budget<br/>capacity = 4"]
Catalogue --> DB
Herta V0 arbitrates per process. It does not coordinate resources globally across processes — see docs/resource-arbitration.md.
Some operations must serialize per key while staying concurrent across keys:
flowchart LR
A1["CatalogueReplace<br/>brand = A"] --> A2["CatalogueReplace<br/>brand = A"]
B["CatalogueReplace<br/>brand = B"] --> Exec["executes concurrently"]
A2 --> Serial["serializes with the first"]
SerializeBy(key) gives same-key serial / different-key concurrent execution.
Same-key waiters hold zero downstream resource capacity while waiting —
waiting work must not hoard scarce resources.
Retry decisions depend on both what happened and whether repeating is safe. The important case is a contract the runtime rejects at construction:
flowchart TD
E["Effect = NonIdempotent"] --> C["Retry configured for Uncertain"]
O["Outcome = Uncertain"] --> C
C --> X{"contract check"}
X -->|"invalid"| R["rejected before serving"]
Why: the external operation may already have executed, consumed quota, charged money, or changed state. Retrying it speculatively is not a transport decision — it is a business-safety decision, and the contract says it is unsafe.
Rules:
- Unclassified Go errors are treated conservatively: they do not trigger speculative retries.
- Unsafe retry combinations (e.g. retrying
Uncertainon aNonIdempotentoperation) fail construction-time validation. - Caller cancellation is honored; per-attempt timeouts are cooperative contexts, not goroutine termination.
Full treatment in docs/failure-semantics.md.
flowchart TD
S["Shutdown()"] --> A["stop admitting new operations"]
A --> W["wake waiters<br/>admission / resources / keys"]
W --> D["drain operations<br/>already executing"]
- Timeouts are cooperative
context.Contextdeadlines. - Panics release runtime-owned resources, then re-panic — cleanup without swallowing the failure.
- Stats are atomic; lifecycle behavior is race-clean under the Go race detector.
Herta is not:
- a message broker
- a durable queue
- a workflow engine
- a service mesh
- an RPC framework
- an actor framework
- a distributed semaphore
- a daemon
- a scheduler
- a sidecar
Herta is process-local by design. It arbitrates capacity inside one process and does not coordinate a global capacity across processes:
capacity 8 × 1 process ≈ 8 local slots
capacity 8 × 5 processes ≈ 40 independent local slots
Herta does NOT coordinate a global capacity across those five processes. Global quotas and provider limits are the application's deployment responsibility — enforce them at the shared provider, not in Herta.
V0 Queue is deliberately descoped until a real consumer proves it is
needed: Wait (block for capacity) plus the caller's own context deadline
covers the same need today.
Install:
go get github.com/arahe-dev/hertasdk@v0.2.0Runnable example in examples/quickstart
(go run ./examples/quickstart):
rt, _ := hertasdk.NewRuntime(hertasdk.ResourceSpec{Name: "worker", Capacity: 4})
// Idempotent operation: safe to retry Transient failures, waits for capacity.
render, _ := hertasdk.NewOperation(rt, "render",
func(ctx context.Context, job string) (string, error) {
if job == "flaky" {
return "", hertasdk.Fail(hertasdk.Transient, errors.New("upstream hiccup"))
}
return "rendered:" + job, nil
},
hertasdk.Policy[string]{
Effect: hertasdk.Idempotent,
Resources: []hertasdk.Requirement{{Name: "worker", Units: 1}},
Admission: hertasdk.Wait,
Retry: hertasdk.RetryPolicy{MaxAttempts: 3, On: map[hertasdk.Outcome]bool{hertasdk.Transient: true}},
})
// Non-idempotent operation: fails fast when busy, retries nothing.
charge, _ := hertasdk.NewOperation(rt, "charge",
func(ctx context.Context, customer string) (string, error) {
return "charged:" + customer, nil
},
hertasdk.Policy[string]{
Effect: hertasdk.NonIdempotent,
Resources: []hertasdk.Requirement{{Name: "worker", Units: 1}},
Admission: hertasdk.Reject,
})
out, err := render.Do(ctx, "job-42")
fmt.Println(out, err)
fmt.Println(rt.Stats())The package name is execution (imported as
hertasdk "github.com/arahe-dev/hertasdk" above for readability). The
handler is ordinary; the policy is explicit; the runtime does the
arbitrating. Nothing more.
HertaSDK v0.2.0 is implemented, extracted, consumer-validated, and frozen. Release evidence:
- Render consumer validated
- Events consumer validated
- CatalogueReplace consumer validated
- CatalogueReplace landed without Herta semantic changes
- 20 concurrent operations against capacity 8 peak at exactly 8
- Shared resource budget across heterogeneous operations
- Same-key serialization / cross-key parallelism
- Partial acquisition rollback
- Caller cancellation
- Panic cleanup
- Shutdown/drain
- Construction validation
- Heavy race hammer (200 iterations of 16 goroutines × 50 calls against
racing shutdown, asserting
Admitted == Finishedevery iteration) - Go race detector enforced in Linux CI (
go test -race -count=1 ./...in .github/workflows/ci.yml) - Benchmarks included (
BenchmarkContention,BenchmarkKeyedSerialization,BenchmarkMultiResource,BenchmarkRetryOverhead) — benchmarks exist and execute; no performance numbers are claimed here.
The runtime was validated against independent render, event-ingestion, and catalogue-replacement workloads before extraction. The public API/documentation remain application-domain neutral.
- Go V0 implemented
- Race-clean proof suite
- Render workload validation
- Events workload validation
- CatalogueReplace third-consumer validation
- Stable public Go extraction
- Runnable quickstart
- Baseline benchmarks
- v0.2.0
- Additional real-world consumers
- Improve benchmark corpus when evidence warrants
- Evaluate Rust/Tower prototype
- Evaluate language-neutral spec only after multi-language evidence
Explicit non-goals: Queue without consumer evidence, distributed Herta, durable workflow execution, transport ownership. No dates are promised. Details in ROADMAP.md.
- Business logic stays ordinary.
- Shared constraints are explicit.
- Retry safety is semantic, not guessed.
- Waiting work should not hoard scarce resources.
- Shutdown behavior is part of the execution contract.
- Herta remains local until evidence proves a distributed layer is necessary.
HertaSDK is named after Herta from Honkai: Star Rail — "many heterogeneous clones sharing one environment" inspired the metaphor — and CAN-style resource coordination influenced the architecture. Neither implies technical compatibility or affiliation.
Disclaimer. HertaSDK is an independent open-source project and is not affiliated with or endorsed by HoYoverse.
- docs/concepts.md — Runtime, Operation, Resource, Policy, Effect, Outcome
- docs/execution-model.md — lifecycle, admission, ordering, rollback, timeout, shutdown
- docs/failure-semantics.md — classification, retry safety, Uncertain, cancellation
- docs/resource-arbitration.md — shared budgets, weighting, per-process scope
- docs/architecture.md — runtime boundary, integration shape, what Herta is not
Apache-2.0 — see LICENSE.
