A worker resource limit is enforced by the supervisor, but only the worker
replaces the serving database, and it does so before the persistence
artifact export. A limit observed in that window stopped a worker that had
already published, and the response still said serving_index_preserved=true
while the previous index had been replaced.
Publication and resource termination now coordinate through a per-attempt
publication fence: a never-created path next to a reserved unique file in
the private worker directory, which both sides claim by creating it
exclusively (cbm_fopen "wbx": O_EXCL on POSIX, CREATE_NEW on Windows), so
exactly one of them can win.
- The worker claims it through a new pipeline publication gate that
cbm_pipeline_run calls once, after the last pre-publication check and
before the serving database is touched. Losing means the supervisor
already stopped the attempt for a limit: the run aborts before
publication and the previous generation stays in place.
- The supervisor claims it before it terminates the worker for a limit or
a failed RSS probe. Losing means the worker already committed to
publishing: the limit is no longer enforced for that attempt, which runs
to completion, and index.supervisor.resource_limit_after_publication is
logged instead. A claim that can be neither won nor lost is retried on
the next poll rather than risk stopping a committed worker.
- At terminal the supervisor settles the fence into
result.publication_committed. An unquiesced tree keeps the fence, so a
surviving member can never publish after the report.
serving_index_preserved now describes the request, not only its last
attempt: a servable index existed when the request began, no attempt of
the request committed to publishing (an earlier attempt can publish and
then crash into recovery), and that index is still servable. The direct
path and the daemon job both fold every attempt into these request facts;
cbm_mcp_index_worker_resource_response takes them as a third argument, and
NULL claims nothing.
The fence path travels as an optional last pair of the exact worker grammar
(--index-worker-publish-fence), accepted by the worker argv parser and by
the daemon bootstrap role check, stripped by the CLI and installed by both
worker entries.
Limits stay opt-in: without index_max_rss_mb or index_max_duration_seconds
there is no fence, no extra argv, no claim and no extra database read.
Graph content is unchanged either way.
RED (c78c077a plus only the new end-to-end test): a FIFO at the artifact
temp path holds the real worker after the rename, and the duration clock
crosses its limit only once the serving database has been replaced. The
supervisor stops the worker there and answers resource_limit_exceeded with
serving_index_preserved=true over a replaced database. GREEN: the worker
claimed first, the supervisor logs the limit as not enforced, and the
released worker finishes and reports the published index.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_subprocess_tree_rss_bytes skipped any non-root process it could not
measure and still returned OK with a smaller total, so a descendant the
platform refused to measure silently dropped out of the RSS watchdog.
Now a member is skipped only when the platform says it no longer exists:
ESRCH from proc_pid_rusage on macOS, ENOENT or ESRCH for /proc/<pid>/stat on
Linux, ERROR_INVALID_PARAMETER from OpenProcess (or an exit code from a
handle whose memory query failed) on Windows. Any other failure fails the
probe, which the supervisor already turns into resource_probe_failed after
three consecutive errors. The root stays strict as before.
On macOS the members now come from proc_listpids(PROC_PGRP_ONLY): the old
all-process scan read every process with proc_pidinfo, which answers EPERM
for every process of another user (178 of 6438 on this machine), so it could
not tell a foreign process from an unreadable member and fail-closed
semantics would have failed every probe there. Linux keeps its /proc scan;
stat files are world-readable on a default mount, and a hidepid=1 mount now
fails closed, which the doc states.
A test seam makes every non-root measurement fail with one platform error.
RED (old skip-any-failure decision): the live-member test returns OK where
ERROR is required. GREEN with the fix; the exited-member test passes in both,
so verified exit races are still ignored.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
subprocess_tree_rss_measures_contained_descendants asserted only that the
tree total was above zero, which the root alone satisfies, and it was
skipped on Windows although its skip text said it ran there.
The test runner gains two roles: __cbm_rss_tree_root starts one
__cbm_rss_tree_hold descendant (fork on POSIX, CreateProcessW into the same
Job Object on Windows) that touches a controlled allocation and publishes its
pid by an atomic rename. The test measures two otherwise identical trees, one
whose descendant holds nothing and one whose descendant holds 128 MiB, and
requires the second total to be at least 96 MiB higher. Only a probe that
measures past the root can pass. The test now runs natively on Windows.
The waits have no deadline: they end when the descendant is ready or the
tree stops running, and the root exits when its descendant dies.
Binding check: with the macOS probe reduced to the root only, the test fails
(the loaded tree is not 96 MiB above the idle one). GREEN on the real probe
(idle 43 MiB, loaded 187 MiB under ASan).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
index_supervisor_quiet_timeout_remains_hang_with_duration_enabled ran the
duration watchdog on the real clock with a 60 s limit next to a 1 s quiet
timeout. On a starved host the duration limit can be crossed before the
quiet timeout fires, and the verdict then turns from HANG into a resource
kill, so the test depended on how fast the machine was.
The resource clock is now pinned at the request origin through the existing
clock seam. The quiet timeout runs on the subprocess clock, so it is the only
way the hung worker can end, however slowly the host runs. The limit is set
equal to the quiet timeout on purpose: more real time than the limit always
passes and it still cannot fire. A new assertion checks that the duration
check read the pinned clock, so HANG wins against an armed watchdog.
RED (real clock, limit crossed first): FAIL ASSERT(remained_hang) with
index.supervisor.resource_limit resource=duration_ms. GREEN with the pin.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Watchdogs now sample the OS-charged footprint instead of resident size on
Darwin, and index_max_duration_seconds covers the whole request so a
crash/hang respawn cannot reset the clock.
Signed-off-by: 刘冲 <mail@liuchong.dev>
PR 2257 still named only the two discovery keys in the worker-scope script.
The worker now fail-closes unless every known policy key is present, so the
fixture must stand in for the supervisor with cbm_test_index_worker_policy_json.
Signed-off-by: 刘冲 <mail@liuchong.dev>
Discovery limits bound what indexing accepts, not what it then costs. A
repository well inside those bounds can still exhaust the host through parser
memory, or simply never finish, and a supervised worker that hangs leaves the
parent waiting with nothing to report.
Add index_max_rss_mb and index_max_duration_seconds, enforced by the parent
against the worker process tree rather than the worker process alone, so a
runaway child cannot hide behind a small parent. Resident memory is sampled
through the platform interface on macOS, Linux and Windows. Crossing a limit
terminates the tree and yields one trusted, structured terminal result that
attributes the failure to the resource that caused it.
Both limits default to off. A measurement that cannot be taken fails the
attempt instead of passing it: a watchdog that quietly stops watching is
worse than no watchdog at all.
The shell fixture that stands in for the supervisor names the two new keys.
The worker accepts only a policy that spells out every key it knows, which is
what keeps a stale supervisor from starting a worker it cannot bound.
Signed-off-by: 刘冲 <mail@liuchong.dev>
py_lsp.c: #2697 moved py_lookup_attribute below the C3 code (own members, C3 order, then the depth-first walk) and declares it up front; the trivial definition this branch still carried next to its module-member lookup is dropped, the module-member block is kept, and the C3 section takes main's header.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
py_lsp.c: the module-member lookup of this branch and the C3 super() lookup of #2696 were added at the same place; both are kept.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A small incremental closure resolves each changed file through a per-file
selection of defs (its own and its imported modules') instead of the shared
registry. A package that re-exports a name keeps only the record there; the
definition lives in the module the record points at, possibly through more
re-exports, so a call through the package found nothing after an
incremental re-index of the caller alone. cbm_pxc_filter_defs_for_file now
follows every selected re-export or star record of a Python module to the
module it names (relative exactly, absolute under the longest matching
prefix, a member as a submodule) and selects that module's defs too, once
per record, so the per-file LSP folds the same chain the shared registry
does.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The re-export records of a package come from its namespace facts. An
incremental re-index that touches only the caller must still find them for
the unchanged packages (restored from their stored facts). Both drivers now
also run as a full index followed by an incremental re-index of app/main.py
alone, with the same expectations.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The first D1 change fed the Go cross-file registrar the extractor's
return_types. Those are cleaned names for type-reference edges:
clean_type_name strips `[]` and `*`. A cross-file single result `[]T`
therefore reached the registrar as `T`. `xs := pkg.Generate(); xs[0].M()`
lost its typing and fell back to a name guess. On prometheus the base
binds `sample[0].T()` to chunks.Sample.T, but the first D1 change bound
it to tsdb.T.
pass_lsp_cross.c now leaves a single result's raw text alone, as before
D1; the registrar already parses `*T` and `[]T`. Only a result LIST
`(...)` is split, at its top-level commas, into raw element types:
- `(*T, error)` -> `*T|error`;
- the named `(t *T, err error)` -> `*T|error`;
- the grouped `(a, b int)` -> `int|int`.
`chan`, `func`, `map`, `struct` and `interface` never count as result
names. A list that does not split cleanly keeps its raw text.
Tests: the D1 fixture gains three call sites, each with two same-named
decoys:
- a single slice result indexed (`xs[0].TS()`);
- a tuple with a slice element;
- grouped named results.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Python surface-envelope test (pipeline round 0) checks that the
persisted row is the versioned codec envelope by looking for the
literal "v":2. With SURFACE_CODEC_VERSION at 3, the envelope reads
"v":3. The assertion keeps its meaning (the row is the versioned
envelope and the def is present) and follows the version.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The cross-file pass handed every registrar the raw result text of a
def (pass_lsp_cross.c: `dst->return_types = src->return_type`). The Go
registrar splits that string on '|' (go_lsp.c split_pipe_types), so it
got `(*Encoder, error)` as one piece. That piece did not parse, and a
cross-file callee with more than one result, or with a named result
`(e *Encoder)`, had no usable return type. As a result,
`e, err := enc.NewEncoder()`, `h, _ := newHead()` and
`_, d := diag.Run()` all left the variable untyped, and calls on it fell
to name guesses. Same-file calls were fine, because they read the AST.
Go defs now pass the parsed result list (extract_go_multi_return),
'|'-joined: `*Encoder|error`, and `*Encoder` for the named result. Other
languages keep the single raw string.
The persisted LSP surface carries this string, so SURFACE_CODEC_VERSION
goes from 2 to 3: existing caches re-extract Go surfaces once. The
literal version-2 surfaces in the codec tests move to 3, so that they
keep testing the envelope rule they name rather than the version
mismatch.
Tests: pipeline_go_multi_return_receiver_is_typed (+_parallel). A tuple
result, a named result, a tuple-bound local reached through a field,
and a named slice type each must bind the right method with an lsp_*
strategy and none of its two same-named decoys.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The memory-core linter forbids a new raw allocator site. Growing the def
array for a Python file's namespace records needed a realloc because a
spilled result's facts were taken as not peekable; its header carries the
fact stream's status and size, though. cbm_pxc_collect_all_defs now adds
each Python module's record bound to the first count (one header peek per
spilled result, as the def count already took), writes the records into
that reserved space, and needs no growth: the realloc and its bookkeeping
are gone. An import-only package (zero defs) still gets its records.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The memory-core linter counts raw allocator calls per file and fails when a
file grows. The TYPE_CHECKING change freed the copied local name of an
annotation-only import with a new raw free in two import-map builders.
build_import_map now asks the predicate on the name's span in the edge
properties before copying it (cbm_imports_local_span_binds_runtime_name,
which cbm_imports_local_binds_runtime_name now wraps), and the cross-LSP
builder routes an annotation-only name through its existing no-value path.
Same behaviour, no new raw allocator site.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A namespace fact stream is checked by cbm_py_namespace_facts_valid, which
accepts a shadow only for the value, decorated, unproven-class and
unsupported reasons. The withdrawn TYPE_CHECKING re-export carried the
compound reason, so the whole stream was rejected and the package published
no names at all (a lazy-module package lost its TYPE_CHECKING exports). It
now carries the unsupported reason: the builder cannot tell which source the
name has at runtime. python_namespace_facts_type_checking_alternatives pins
both shapes as complete, valid streams.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The re-export test checks a table of calls in one loop; a failure only
reported the assertion line. Each failing call is now named, with its
venue.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A package namespace read in the type-checker view takes the consequence of
`if TYPE_CHECKING:` as executed (lazy-module packages publish their names
there). The compat shape imports a name there and again in `else: try:
from runtime.place import name`: the module's runtime export comes from the
else branch, so publishing the TYPE_CHECKING source as the re-export target
was a guess. Every name an elif/else import binds now becomes a shadow fact
(the re-export is withdrawn, as for a later assignment); a star import or an
import form the builder does not read there makes the whole namespace
unknown. The branch is walked iteratively.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_run_php_lsp never finalized its type registry, so every lookup during
single-file resolution scanned the stdlib plus every definition of the
file linearly: O(lookups x definitions), quadratic in the size of one
file. The cross-file path already indexes its registry.
The registry is now finalized with first-registered semantics (the first
registration of a duplicate QN wins), which keeps exactly the answers of
the linear scan, and re-indexed once after trait flattening adds methods.
The index lives in a scratch arena that dies with the call.
A test-only work ceiling (CBM_LSP_WORK_CEILING_HIT, seam builds only) lets
the work-counter test stop a reverted, unbounded loop deterministically
instead of hanging the runner; product builds have no ceiling.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_pxc_collect_all_defs sizes the def array by every file's def count and
appends a Python file's namespace records behind its defs. The growth check
only made room for the records themselves, so the records consumed the slack
reserved for later files and their defs overflowed the array (ASan:
heap-buffer-overflow in pipeline_python_package_reexports_resolve_by_lsp).
The array now grows to keep every later file's defs reserved on top of the
records placed so far, geometrically, so the copying stays linear.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A package `__init__.py` re-exports its submodules' names (`from .fields
import *`, `from .x import Orig as Renamed`), and callers reach them as
module attributes (`models.CharField()`) or through `from pkg import Name`.
The Python namespace facts already capture each module's top-level bindings
in order; nothing consumed them, so those calls fell to name guesses that
pick the first same-named definition under the package.
- Records: cbm_pxc_collect_all_defs replays each module's COMPLETE fact
stream into re-export, star-import and `__all__` records next to its
defs, including for an import-only `__init__.py` (zero defs). Records
carry the raw module spec; the Python registry resolves it (relative
exactly, absolute by unique scope tail, else unknown).
- Fold: the shared Python registry folds the records once, after finalize
and before the seal, into a sorted per-module export table (binary
search; no lookup in the registration loop). Order-deterministic; a
cycle contributes what it has so far.
- States (conservative): an incomplete fact stream publishes nothing; a
shadow, delete or whole-namespace unknown (a top-level try/if/with)
withdraws the names it covers; a star whose source cannot be enumerated
publishes nothing; a literal `__all__` restricts a star, else public
names only.
- Consult: the Python LSP looks a module attribute up in the export table
on a miss (calls, expression types, callable values) and when binding
`from pkg import Y` where Y is no def of pkg.
- Namespace facts read a module-level `if TYPE_CHECKING:` consequence as
executed: lazy-module packages publish their names there.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A lazy-module package publishes its names only under `if TYPE_CHECKING:`
(the else branch installs the lazy module): `lz.Lazy()` resolves to the
re-exported class. The compat shape imports a name under TYPE_CHECKING and
again in `else: try:`: the runtime binding comes from the else branch, so
`cmp.Shim()` must not reach the TYPE_CHECKING-side class.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A package __init__.py re-exports its submodules' names (explicit, star,
`__all__`-restricted, renamed, chained through a subpackage, cyclic stars,
the django `from .fields import *` shape) and callers reach them as module
attributes (`pkg.Explicit()`) or through `from pkg import Name`. Each call
must resolve by an lsp_* strategy to the re-exported definition. Names a
star does not export (`__all__`-unlisted, private), a re-export the module
shadows later, and one before a whole-namespace barrier must not. Plain
from-imports of the defining module are the control. Both venues.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The compat shape imports a name under `if TYPE_CHECKING:` and again in the
else branch, usually inside try/except ImportError. The name is bound at
runtime there, so its TYPE_CHECKING import is no annotation-only import:
recording it flagged made every CALLS import map skip the name and lost the
calls and bases that bind through the runtime import (airflow mask_secret,
OpenLineageRedactor(SecretsMasker)). The imports of the elif/else branches
are collected (any depth, iteratively), every TYPE_CHECKING import whose
local name they bind is dropped with them, and a star import there drops the
whole block: those names keep the behaviour they had before #1277.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The airflow compat shape imports a name under `if TYPE_CHECKING:` and again
in the else branch, usually inside try/except ImportError: the name is bound
at runtime, so its TYPE_CHECKING import is not annotation-only and must stay
unrecorded, as the runtime import itself does. A star import in an elif or
else branch may bind any name. A pure annotation import stays flagged.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>