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 pattern | Runtime lifecycle concern |
|---|---|
| single model owned by one solver thread | ordinary create -> destroy path |
| worker pool with one model per thread | ownership must stay per-handle, not per process |
| supervisory or fault-handling shutdown | close 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
| API | Primary Use | Wait Behavior | Returns Status | Recommended For Host Code |
|---|---|---|---|---|
tdse_model_create(...) | create a model from pack bytes | n/a | yes | yes |
tdse_model_close(...) | explicit non-blocking close attempt | does not wait for an in-flight same-handle API | yes | situational |
tdse_model_destroy(...) | explicit bounded destroy | caller-controlled wait policy | yes | yes |
tdse_model_release(...) | terminal cleanup / finalizer path | may wait until destroy ownership is acquired | yes | no |
Short version:
- use
createto create - use
destroyfor ordinary host-managed shutdown - use
closewhen you need an immediate status-bearing close attempt - use
releaseonly for terminal cleanup paths that are not driving lifecycle policy
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_tso create failures remain request-scoped - treat
diag.pack_error_codeas 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 atdse_model_info_tsnapshot decoded from pack bytestdse_pack_inspect_ex(...)for version and payload summary fields intdse_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 State | Meaning | Safe Actions |
|---|---|---|
| locally owned and idle | handle exists, no active trial step | query info/state, start a step, destroy, close |
| trial step active | tdse_step_begin(...) succeeded and commit has not yet ended the trial | query op, hr, ir, commit |
| destroy pending locally | caller has entered destroy and is waiting or finishing | observe destroy result only |
| ownership already handed off | another thread already started close or destroy | stop using the handle locally |
| terminally cleaned up | storage has been released | no further API use is valid |
Step-Lifecycle Ownership
Inside a live handle, the runtime lifecycle is:
tdse_step_begin(...)- one or more trial-safe queries:
tdse_step_op(...)tdse_step_hr(...)tdse_step_ir(...)
tdse_step_commit(...)- 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 endedTDSE_ERR_CONCURRENT_API_USE: another same-handle API is still in flightTDSE_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 completedTDSE_ERR_TIMEOUT: destroy did not acquire ownership before the budget expired; the handle is still validTDSE_ERR_INVALID_STATE: another thread already started close or destroy; local ownership should be treated as gone
Destroy Result Matrix
| Status | Ownership After Return | Handle Still Live? | Typical Host Action |
|---|---|---|---|
TDSE_OK | no longer local | no | clear references and continue |
TDSE_ERR_TIMEOUT | still local | yes | retry later, escalate, or change policy |
TDSE_ERR_INVALID_STATE | no longer local | unknown to this thread; assume not locally usable | stop 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 completedTDSE_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
| Status | Interpretation |
|---|---|
TDSE_OK | finalizer cleanup completed |
TDSE_ERR_INVALID_STATE | another thread already owns terminal cleanup; local ownership is gone |
C++ Wrapper Interpretation
The C++ wrapper follows the same lifecycle rules:
tdse::Model::fromMemory(...)wrapstdse_model_create(...)tdse::Model::close()wrapstdse_model_close(...)tdse::Model::destroy(options)wrapstdse_model_destroy(...)tdse::Model::release()wrapstdse_model_release(...)but is noexcept and ignores the returned status- runtime status-bearing failures surface as
tdse::Error, which preserves both the API name andtdse_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:
- build a unified code with
tdse_status_code_make(TDSE_STATUS_DOMAIN_RUNTIME, st) - use
tdse_status_code_classify(...)andtdse_status_code_message(...)for cross-module logging
- build a unified code with
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(...)returnsTDSE_OKtdse_model_destroy(...)returnsTDSE_OKtdse_model_destroy(...)returnsTDSE_ERR_INVALID_STATEtdse_model_release(...)returnsTDSE_OKtdse_model_release(...)returnsTDSE_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
| Intent | C API | C++ Wrapper |
|---|---|---|
| create with request-scoped diagnostics | tdse_model_create(...) | tdse::Model::fromMemory(..., &diag) |
| fast close attempt | tdse_model_close(...) | tdse::Model::close() |
| canonical bounded shutdown | tdse_model_destroy(...) | tdse::Model::destroy(options) |
| terminal finalizer cleanup | tdse_model_release(...) | tdse::Model::release() and destructor |
Anti-Patterns
Avoid these patterns even if they appear to work in a small local test:
- using
tdse_model_release(...)as the primary host shutdown API - retrying destroy locally after
TDSE_ERR_INVALID_STATE - treating
TDSE_ERR_TIMEOUTas if the handle were already gone - continuing to use a handle after ownership handoff
- assuming
closeis just a fasterdestroy
A Good Mental Checklist
Before every lifecycle decision, ask:
- Do I still own this handle locally?
- Do I need a status-bearing shutdown answer?
- Do I need a bounded wait budget?
- 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
- caller owns a live handle
- no same-handle API is in flight
- caller invokes
tdse_model_destroy(...) - runtime returns
TDSE_OK - caller drops all references
This is the ideal host-managed path.
Scenario B. Destroy Races With A Worker Step
- worker thread is still inside a same-handle step call
- supervisory code calls
tdse_model_destroy(...) - destroy waits within its configured budget
- runtime returns either
TDSE_OKorTDSE_ERR_TIMEOUT
This is why destroy carries explicit wait policy and result details.
Scenario C. Another Thread Already Took Ownership
- thread A starts close or destroy
- thread B also attempts close, destroy, or release
- thread B receives
TDSE_ERR_INVALID_STATE - thread B must stop using the handle locally
This is not a local retry condition. It is ownership handoff.
Recommended Patterns
Recommended:
- ordinary host shutdown:
tdse_model_destroy(...) - timeout-aware supervision:
tdse_model_destroy(...)plus an explicit policy forTDSE_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
