Memory & Thread Safety
PostgreSQL extensions run inside a shared-memory, multi-process server. A function that leaks memory degrades the entire backend. A function that uses global state cannot be parallelized. pg_orbit is designed to be a well-behaved citizen: all memory goes through PostgreSQL’s allocator, and no mutable state survives between function calls.
Allocation strategy
Section titled “Allocation strategy”All heap allocation goes through palloc() / pfree(). No malloc(), no new, no static buffers. This is not a convention --- it is a hard requirement. PostgreSQL’s memory context system tracks every allocation and frees entire contexts at transaction boundaries, query completion, or error recovery. Using malloc() would create memory that PostgreSQL cannot reclaim on error, leading to gradual backend bloat.
Single-shot propagation
Section titled “Single-shot propagation”Functions like sgp4_propagate() and tle_distance() follow the simplest pattern:
double *params = palloc(sizeof(double) * N_SAT_PARAMS);
/* Initialize and propagate */SGP4_init(params, &sat);err = SGP4(tsince, &sat, params, pos, vel);
pfree(params);The params array (~92 doubles, ~736 bytes) lives in the current memory context. It is allocated before propagation and freed before the function returns. If an ereport(ERROR) fires between palloc and pfree, PostgreSQL’s error recovery frees the current context automatically.
Set-returning functions
Section titled “Set-returning functions”SRF functions like sgp4_propagate_series(), ground_track(), and predict_passes() must maintain state across multiple calls. They use PostgreSQL’s multi_call_memory_ctx:
typedef struct { tle_t sat; double params[N_SAT_PARAMS]; /* embedded, not separate allocation */ int is_deep; double epoch_jd; int64 start_ts; int64 step_usec;} propagate_series_ctx;The lifecycle:
funcctx = SRF_FIRSTCALL_INIT();oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
ctx = palloc(sizeof(propagate_series_ctx));/* Copy TLE and observer into ctx *//* Initialize propagator */
MemoryContextSwitchTo(oldctx);funcctx->user_fctx = ctx;funcctx = SRF_PERCALL_SETUP();ctx = funcctx->user_fctx;
/* Propagate to next timestep using ctx->params *//* Return result or SRF_RETURN_DONE */PostgreSQL frees multi_call_memory_ctx automatically when the SRF completes (either by returning SRF_RETURN_DONE or via error recovery). No explicit cleanup code needed.
Type I/O functions
Section titled “Type I/O functions”Input functions (tle_in, eci_in, etc.) allocate the result struct with palloc() in the current context. PostgreSQL manages the lifecycle --- the struct may be copied into a tuple for storage or used transiently for a computation.
pg_tle *result = (pg_tle *) palloc(sizeof(pg_tle));/* Parse input text into result */PG_RETURN_POINTER(result);Zero global mutable state
Section titled “Zero global mutable state”There are no file-scope variables, no static locals that accumulate state, no caches. Every function computes from its arguments alone.
This guarantee has three consequences:
PARALLEL SAFE
Section titled “PARALLEL SAFE”All 57 pg_orbit functions are declared PARALLEL SAFE in the SQL definition. This tells PostgreSQL’s query planner that the function can be executed in parallel worker processes without coordination. For bulk operations like propagating 12,000 TLEs, the planner can distribute work across multiple CPU cores:
-- PostgreSQL may parallelize this across available coresSELECT tle_norad_id(tle), eci_x(sgp4_propagate(tle, now())) AS x_kmFROM satellite_catalog;If any function used global state --- even a read-only cache --- PostgreSQL would need to serialize access or copy state between workers. PARALLEL SAFE cannot be declared for functions with global mutable state; doing so risks data races and incorrect results.
No cross-session contamination
Section titled “No cross-session contamination”PostgreSQL backends are long-lived processes that serve multiple sessions. A global variable written by session A persists when session B runs in the same backend. pg_orbit avoids this entirely --- no function call leaves any trace in the process state.
Deterministic computation
Section titled “Deterministic computation”Given the same TLE and timestamp, pg_orbit produces the same result regardless of what queries ran before, how many backends are active, or whether the function is running in a parallel worker. There is no path-dependent behavior.
sat_code’s memory model
Section titled “sat_code’s memory model”sat_code itself has no global mutable state. The propagator state lives entirely in two caller-provided structures:
| Structure | Size | Contains | Owner |
|---|---|---|---|
tle_t | ~200 bytes | Parsed mean elements, identification | Caller (pg_orbit copies from pg_tle) |
params[N_SAT_PARAMS] | ~736 bytes | Initialized propagator coefficients | Caller (pg_orbit pallocs this) |
The SGP4_init() / SDP4_init() functions write into the params array. The SGP4() / SDP4() functions read from params and tle_t, and write position/velocity into caller-provided arrays. No internal state is retained between calls.
This maps cleanly to PostgreSQL’s per-call execution model. There is no object lifecycle to manage, no destructor to call, no persistent state to synchronize.
Fixed-size types
Section titled “Fixed-size types”All seven pg_orbit types are fixed-size with STORAGE = plain:
| Type | Size | ALIGNMENT | TOAST? |
|---|---|---|---|
tle | 112 bytes | double | No |
eci_position | 48 bytes | double | No |
geodetic | 24 bytes | double | No |
topocentric | 32 bytes | double | No |
observer | 24 bytes | double | No |
pass_event | 48 bytes | double | No |
heliocentric | 24 bytes | double | No |
Why fixed-size matters
Section titled “Why fixed-size matters”No TOAST overhead. Variable-length types (varlena) carry a 4-byte header and may be compressed or moved to a secondary TOAST table. Reading a TOASTed value requires a separate heap fetch. Fixed-size types are stored inline in the tuple --- one pointer dereference, no detoasting.
Direct pointer access. PG_GETARG_POINTER(n) returns a pointer directly into the tuple data. No copy, no allocation. The function reads the struct in place.
Predictable memory layout. All types use ALIGNMENT = double because every struct contains double fields. This satisfies the strictest alignment requirement without platform-specific conditionals.
Binary I/O. The tle_recv() / tle_send() functions implement binary protocol support. The fixed layout means binary transfer is a straight memory copy --- no serialization logic, no endianness concerns beyond what PostgreSQL’s binary protocol handles.
TLE: 112 bytes vs raw text
Section titled “TLE: 112 bytes vs raw text”The TLE text format is 138+ bytes (two 69-character lines plus separator). The parsed struct is 112 bytes --- smaller than the text it came from, and it eliminates the ~10x parsing overhead that would be incurred on every propagation call if raw text were stored.
The text representation can be reconstructed from the parsed elements via sat_code’s write_elements_in_tle_format(). The round-trip is lossless for all fields that affect propagation.
Memory usage in practice
Section titled “Memory usage in practice”For a typical catalog query propagating 12,000 TLEs:
| Resource | Per-call | Peak (12K TLEs) |
|---|---|---|
params array | 736 bytes | 736 bytes (reused) |
tle_t conversion | 200 bytes (stack) | 200 bytes |
Result pg_eci | 48 bytes | 48 bytes (returned, then freed) |
| Total transient | ~1 KB | ~1 KB |
The 736-byte params array is the largest per-call allocation. It is freed before the function returns. At no point does pg_orbit hold allocations proportional to the number of rows being processed --- each row is computed and returned independently.
Error recovery
Section titled “Error recovery”When ereport(ERROR) fires inside a pg_orbit function, PostgreSQL’s error recovery mechanism:
- Unwinds the call stack via
longjmp - Frees the current memory context (including any
palloc’d memory) - Rolls back the current transaction
- Returns an error message to the client
Because pg_orbit uses only palloc and has no global state, there is nothing to clean up beyond what PostgreSQL’s context system handles automatically. No file handles, no sockets, no mutex locks, no C++ destructors. The extension is always in a consistent state after error recovery.