Skip to content

Herta

HertaSDK HertaSDK

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.

Go Status: v0.2.0 License: Apache-2.0

Status. HertaSDK v0.2.0 is an installable Go module:

go get github.com/arahe-dev/hertasdk@v0.2.0

The V0 execution contract is frozen: Wait / Reject admission, shared weighted resources, keyed serialization, Effect / Outcome failure semantics, bounded safe retries, cooperative timeouts, shutdown/drain, and atomic stats. Queue is explicitly NOT part of V0. See ROADMAP.md and CHANGELOG.md.


Contents


What is HertaSDK?

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:

Caller, Operation, Herta Runtime (resources, admission, serialization, timeout/retry, shutdown), then a normal Go handler Caller, Operation, Herta Runtime (resources, admission, serialization, timeout/retry, shutdown), then a normal Go handler

  • 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.

Why?

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"]
Loading

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:

Five heterogeneous workers at independent rhythms sharing one Herta runtime for budgets, admission, and error semantics Five heterogeneous workers at independent rhythms sharing one Herta runtime for budgets, admission, and error semantics

Core model

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.

Resource arbitration

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.

Render, events, catalogue, webhooks and model calls arbitrate through shared Herta budgets to normal handlers; 20 concurrent renders against capacity 8 peak at exactly 8 Render, events, catalogue, webhooks and model calls arbitrate through shared Herta budgets to normal handlers; 20 concurrent renders against capacity 8 peak at exactly 8

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"]
Loading

And across subsystems sharing one budget:

flowchart LR
    Events --> DB["db-write budget<br/>capacity = 4"]
    Catalogue --> DB
Loading

Herta V0 arbitrates per process. It does not coordinate resources globally across processes — see docs/resource-arbitration.md.

Keyed serialization

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"]
Loading

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.

Failure semantics

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"]
Loading

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 Uncertain on a NonIdempotent operation) fail construction-time validation.
  • Caller cancellation is honored; per-attempt timeouts are cooperative contexts, not goroutine termination.

Full treatment in docs/failure-semantics.md.

Lifecycle

flowchart TD
    S["Shutdown()"] --> A["stop admitting new operations"]
    A --> W["wake waiters<br/>admission / resources / keys"]
    W --> D["drain operations<br/>already executing"]
Loading
  • Timeouts are cooperative context.Context deadlines.
  • 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.

What Herta is NOT

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.

Quickstart (real API)

Install:

go get github.com/arahe-dev/hertasdk@v0.2.0

Runnable 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.

Current validation

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 == Finished every 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.

Roadmap

  • 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.

Design principles

  1. Business logic stays ordinary.
  2. Shared constraints are explicit.
  3. Retry safety is semantic, not guessed.
  4. Waiting work should not hoard scarce resources.
  5. Shutdown behavior is part of the execution contract.
  6. Herta remains local until evidence proves a distributed layer is necessary.

Name / inspiration

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

Contributing / Security

License

Apache-2.0 — see LICENSE.

About

In-process execution contracts and shared resource arbitration for heterogeneous Go operations.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages