Time-Domain System Equivalent logoTime-Domain System EquivalentLinear dynamics, solved faster.Discuss Integration

Lifecycle and Ownership

Runtime creation, ownership, state transitions, and shutdown contracts.

Use this section when you need to know who owns a Runtime handle, which API ends that ownership, and which shutdown call fits your host code. This section is about handle lifetime only; the per-step loop itself belongs in Step Execution.

Related Chapters For Builder configuration and pack generation, see Builder and Data Contracts. For the step-loop execution model, see Step Execution. For concurrency rules, see Concurrency and Shutdown.

Use this section when your host integration question is primarily:

  • who owns the tdse_model_t* handle right now
  • which shutdown path fits the host's policy and wait budget
  • when local ownership is gone and all further calls must stop

If your question is instead "what exact calls happen every accepted step," jump to Step Execution first.

Why Lifecycle Comes First

The most important Runtime rule is not the math. It is lifecycle. If you understand when a handle is live, closing, destroying, released, or no longer locally owned, the rest of the API becomes much easier to use correctly.

Where This Fits In A Host Integration

For a minimal host integration, lifecycle is usually one of three patterns:

Host patternRuntime lifecycle concern
single model owned by one solver threadordinary create -> destroy path
worker pool with one model per threadownership must stay per-handle, not per process
supervisory or fault-handling shutdownclose or timeout-bearing destroy policy matters

This chapter only answers the ownership side of those patterns. It does not define the numerical step loop.

The Runtime Lifecycle In One Table

APIPrimary UseWait BehaviorReturns StatusRecommended For Host Code
tdse_model_create(...)create a model from pack bytesn/ayesyes
tdse_model_close(...)explicit non-blocking close attemptdoes not wait for an in-flight same-handle APIyessituational
tdse_model_destroy(...)explicit bounded destroycaller-controlled wait policyyesyes
tdse_model_release(...)terminal cleanup / finalizer pathmay wait until destroy ownership is acquiredyesno

Short version:

  • use create to create
  • use destroy for ordinary host-managed shutdown
  • use close when you need an immediate status-bearing close attempt
  • use release only for terminal cleanup paths that are not driving lifecycle policy

High-level lifecycle state model

Create Semantics

The standard create path is:

tdse_model_create_diagnostics_t diag = tdse_model_create_diagnostics_init();
tdse_model_t* model = NULL;
tdse_status_t st = tdse_model_create(pack_data, pack_size, &diag, &model);

Key rules:

  • pass tdse_model_create_diagnostics_t so create failures remain request-scoped
  • treat diag.pack_error_code as part of the normal error path, not as optional trivia
  • after successful create, the caller owns the handle locally

When the host needs metadata before deciding whether to create, prefer:

  • tdse_pack_inspect(...) for a tdse_model_info_t snapshot decoded from pack bytes
  • tdse_pack_inspect_ex(...) for version and payload summary fields in tdse_pack_summary_t

These APIs validate pack header and required metadata chunks without creating a tdse_model_t.

Owning the handle locally means:

  • your thread or wrapper may enter the handle according to the Runtime rules
  • your code is responsible for selecting the shutdown path
  • another thread has not yet taken over close or destroy

Live-Handle Query APIs

These queries are safe snapshot-style reads on a live handle:

  • tdse_model_info(...)
  • tdse_model_state_info(...)
  • tdse_model_last_error_info(...)

They are intended for metadata, state, and failure diagnosis. If close or destroy has already started on the handle, these queries return TDSE_ERR_INVALID_STATE.

Lifecycle States In Practice

The runtime does not publish a first-class enum for every human-readable lifecycle state, but product integrations should reason in these terms:

Practical StateMeaningSafe Actions
locally owned and idlehandle exists, no active trial stepquery info/state, start a step, destroy, close
trial step activetdse_step_begin(...) succeeded and commit has not yet ended the trialquery op, hr, ir, commit
destroy pending locallycaller has entered destroy and is waiting or finishingobserve destroy result only
ownership already handed offanother thread already started close or destroystop using the handle locally
terminally cleaned upstorage has been releasedno further API use is valid

Step-Lifecycle Ownership

Inside a live handle, the runtime lifecycle is:

  1. tdse_step_begin(...)
  2. one or more trial-safe queries:
    • tdse_step_op(...)
    • tdse_step_hr(...)
    • tdse_step_ir(...)
  3. tdse_step_commit(...)
  4. optional tdse_step_dr(...)

Two rules matter:

  • tdse_step_commit(...) is the only state-advancing call
  • if a trial solve fails, do not call commit; committed state remains unchanged

Lifecycle bugs and step-order bugs usually appear together. If ownership or sequencing is wrong, the first visible symptom is often a bad step call rather than a clearly named lifecycle error.

Close Semantics

tdse_model_close(...) is the explicit non-blocking lifecycle endpoint.

Use it when:

  • the caller needs a synchronous lifecycle result immediately
  • the caller does not want to wait for an in-flight same-handle API

Interpretation:

  • TDSE_OK: close completed and local ownership ended
  • TDSE_ERR_CONCURRENT_API_USE: another same-handle API is still in flight
  • TDSE_ERR_INVALID_STATE: another thread already started close or destroy

close is not the recommended production shutdown path. It is the immediate-answer path.

Typical uses:

  • supervisory code wants a fast lifecycle answer
  • a wrapper wants overlap detection without entering a wait path
  • tests intentionally exercise ownership conflicts

Close is best understood as a guardrail API, not as a general shutdown API.

Destroy Semantics

tdse_model_destroy(...) is the recommended host-managed shutdown path.

Use it when:

  • the caller owns lifecycle policy
  • timeout matters
  • the caller wants structured result details

Typical usage:

tdse_model_destroy_options_t opt = tdse_model_destroy_options_init();
tdse_model_destroy_result_t result = tdse_model_destroy_result_init();
opt.wait_timeout_ms = 250.0;

tdse_status_t st = tdse_model_destroy(model, &opt, &result);

Interpretation:

  • TDSE_OK: destroy completed
  • TDSE_ERR_TIMEOUT: destroy did not acquire ownership before the budget expired; the handle is still valid
  • TDSE_ERR_INVALID_STATE: another thread already started close or destroy; local ownership should be treated as gone

Destroy decision model

Destroy Result Matrix

StatusOwnership After ReturnHandle Still Live?Typical Host Action
TDSE_OKno longer localnoclear references and continue
TDSE_ERR_TIMEOUTstill localyesretry later, escalate, or change policy
TDSE_ERR_INVALID_STATEno longer localunknown to this thread; assume not locally usablestop using the handle locally

Release Semantics

tdse_model_release(...) exists so C and C++ have an explicit finalizer target.

Use it when:

  • a destructor, guard, or finally block must make a best-effort terminal cleanup
  • the caller is not trying to implement a bounded lifecycle policy

Do not use it as the primary shutdown path in ordinary host code.

Interpretation:

  • TDSE_OK: release completed
  • TDSE_ERR_INVALID_STATE: another thread already started close, destroy, or release

This API is intentionally secondary to tdse_model_destroy(...).

Its purpose is very narrow and very important:

  • it gives destructors and unwind paths a clean terminal target
  • it avoids turning finalizer code into a policy engine

Release Result Matrix

StatusInterpretation
TDSE_OKfinalizer cleanup completed
TDSE_ERR_INVALID_STATEanother thread already owns terminal cleanup; local ownership is gone

C++ Wrapper Interpretation

The C++ wrapper follows the same lifecycle rules:

  • tdse::Model::fromMemory(...) wraps tdse_model_create(...)
  • tdse::Model::close() wraps tdse_model_close(...)
  • tdse::Model::destroy(options) wraps tdse_model_destroy(...)
  • tdse::Model::release() wraps tdse_model_release(...) but is noexcept and ignores the returned status
  • runtime status-bearing failures surface as tdse::Error, which preserves both the API name and tdse_status_t
  • tdse::Model::empty() / operator bool() are the supported way to check moved-from or already-cleaned-up wrappers
  • plain C integrations can normalize runtime failures in two steps:
    1. build a unified code with tdse_status_code_make(TDSE_STATUS_DOMAIN_RUNTIME, st)
    2. use tdse_status_code_classify(...) and tdse_status_code_message(...) for cross-module logging

Important rule:

  • tdse::Model::destroy(...) requires explicit destroy options so the wait policy is always intentional
  • moved-from wrappers may be destroyed, checked with empty() / if (model), or assigned again; business APIs still throw on use

That requirement is deliberate. Product code should not drift into implicit infinite-wait shutdown through a convenient wrapper default.

Ownership Handoff Rules

When local ownership is gone, stop using the handle immediately. This is especially important when:

  • tdse_model_close(...) returns TDSE_OK
  • tdse_model_destroy(...) returns TDSE_OK
  • tdse_model_destroy(...) returns TDSE_ERR_INVALID_STATE
  • tdse_model_release(...) returns TDSE_OK
  • tdse_model_release(...) returns TDSE_ERR_INVALID_STATE

The Runtime contract is:

  • if another thread has already started close or destroy, ownership is no longer local
  • the old pointer must not be treated as a reusable live handle

Ownership handoff is the place where many host bugs become latent use-after-destroy bugs. The safest policy is simple: once handoff happens, clear local references immediately.

C And C++ Mapping Table

IntentC APIC++ Wrapper
create with request-scoped diagnosticstdse_model_create(...)tdse::Model::fromMemory(..., &diag)
fast close attempttdse_model_close(...)tdse::Model::close()
canonical bounded shutdowntdse_model_destroy(...)tdse::Model::destroy(options)
terminal finalizer cleanuptdse_model_release(...)tdse::Model::release() and destructor

Anti-Patterns

Avoid these patterns even if they appear to work in a small local test:

  1. using tdse_model_release(...) as the primary host shutdown API
  2. retrying destroy locally after TDSE_ERR_INVALID_STATE
  3. treating TDSE_ERR_TIMEOUT as if the handle were already gone
  4. continuing to use a handle after ownership handoff
  5. assuming close is just a faster destroy

A Good Mental Checklist

Before every lifecycle decision, ask:

  1. Do I still own this handle locally?
  2. Do I need a status-bearing shutdown answer?
  3. Do I need a bounded wait budget?
  4. Am I in business logic or finalizer cleanup?

If the answer is "business logic and I need a bounded answer," the API is almost always tdse_model_destroy(...).

Worked Lifecycle Scenarios

Scenario A. Clean Business Shutdown

  1. caller owns a live handle
  2. no same-handle API is in flight
  3. caller invokes tdse_model_destroy(...)
  4. runtime returns TDSE_OK
  5. caller drops all references

This is the ideal host-managed path.

Scenario B. Destroy Races With A Worker Step

  1. worker thread is still inside a same-handle step call
  2. supervisory code calls tdse_model_destroy(...)
  3. destroy waits within its configured budget
  4. runtime returns either TDSE_OK or TDSE_ERR_TIMEOUT

This is why destroy carries explicit wait policy and result details.

Scenario C. Another Thread Already Took Ownership

  1. thread A starts close or destroy
  2. thread B also attempts close, destroy, or release
  3. thread B receives TDSE_ERR_INVALID_STATE
  4. thread B must stop using the handle locally

This is not a local retry condition. It is ownership handoff.

Recommended:

  • ordinary host shutdown: tdse_model_destroy(...)
  • timeout-aware supervision: tdse_model_destroy(...) plus an explicit policy for TDSE_ERR_TIMEOUT
  • destructor or guard cleanup: tdse_model_release(...)
  • immediate non-waiting close attempt: tdse_model_close(...)

Avoid:

  • using tdse_model_release(...) as the first-choice business-logic shutdown API
  • relying on infinite-wait destroy as the default production pattern without documenting why
  • continuing to use a handle after destroy, release, or ownership handoff