Repository navigation
Refactor(engine): Engine God-Class Refactor - Extract 4 Business Components + Stage 6 Strategy Tables + Value Objects - #176
Merged
Conversation
added 12 commits
July 21, 2026 12:00
Move search/ls/cat/head/tail/grep/export/resolve_connector_uri out of the
Engine god-class into a dedicated ReadService component (engine-redesign
§4.6 stage 3). Engine delegates to self.reads; the 8 public read methods
become thin forwards. Method bodies are unchanged; only self.<dep>
resolution targets moved.
- New engine/components/reads/{__init__,text_views,read_service}.py:
ReadService + pure helpers (_density_view/_locator_matches) + read-path
constants. Locator pair (open_path/match_connector) is a ReadService
public method connecting directly to ConnectorFactory+ObjectRepository,
no reverse reference to Engine (D1: direct connect, not delayed-lambda).
- engine.py: ~1504 -> 887 lines. Assemble self.reads; 8 read methods thin
delegates; drop _read_full/_warn_if_huge_export/_has_registered_search_scope
/_locator_matches/_density_view/_CODE_SYMBOL/_open_path + read-path
constants + now-orphan imports; drop dead-code _now().
- Tests: patch target eng._open_path -> eng.reads.open_path (4 sites:
test_cat_locator x2, test_engine_tail, test_grep_not_found).
- New tests/test_engine_read_service.py: 16 cases, constructs ReadService
without an Engine/Milvus, covers text_views pure helpers + the locator
pair + search short-circuits + resolve_connector_uri.
Verified: 433 passed, 9 skipped; ruff format --check + ruff check clean.
Move ingest_upload/files_manifest/files_upload out of the Engine god-class into a dedicated UploadService component (engine-redesign §4.7 stage 5). Engine delegates to self.upload; the 3 public upload methods become thin forwards. Method bodies are unchanged; only self.<dep> resolution targets moved. Reuses the already-public IngestOrchestrator entrypoints (register_or_get_connector / open_sync_job / drain_job). - New engine/components/upload.py: UploadService + module-level _norm_rel / _validate_upload_member (zip-slip guard). DI is precise: (cfg, infra, objects, ingest) - no unused factory/artifacts (UploadService touches infra.artifact_cache.files_root + meta, not the cache service). - engine.py: 887 -> 635 lines. Assemble self.upload; 3 upload methods thin delegates; drop _staging_root/_staging_connector + module-level _norm_rel/_validate_upload_member + now-orphan FileStateStore import. - BundleValidator / StagingLocator class extraction + shared Template Method are follow-up (plan §2.5) - this round is pure relocation. - New tests/test_engine_upload_service.py: 9 cases, constructs UploadService without an Engine/meta, covers _norm_rel / _validate_upload_member (symlink/hardlink/absolute/traversal) + the staging locator (_staging_root sha1 sub / _staging_connector stable URI). Verified: 442 passed, 9 skipped; ruff format --check + ruff check clean.
…workers Move run_worker_once / run_worker_forever / _reclaim_stale_jobs / _resolve_concurrency out of the Engine god-class into a dedicated WorkerScheduler component (engine-redesign §4.5 stage 4c). Engine delegates to self.worker; run_worker_forever becomes a thin forward. Method bodies are unchanged; only self.<dep> resolution targets moved. Single-direction dep on IngestOrchestrator (run_job + finalize_job), no cycle. - New engine/worker.py (Engine-internal collaborator, not components/): WorkerScheduler + _JOB_STALE_AFTER_S / _WORKER_CONNECT_TIMEOUT_S. DI: (cfg, infra, factory, objects, ingest). run_worker_once connects directly to factory.build_plugin (was Engine._build_plugin). - engine.py: 635 -> 511 lines. Assemble self.worker; run_worker_forever thin delegate; drop _claim_queued_job (direct objects.claim_queued_job) / run_worker_once / _resolve_concurrency / _reclaim_stale_jobs + the two worker constants + now-orphan ConnectorStateStore import. remove_connector imports _JOB_STALE_AFTER_S from .worker (still uses it for the heartbeat stale check). cancel_job stays (thin delegate to ingest.cancel_job, D4). - Two-layer try/except invariant preserved verbatim: the OUTER `except Exception: jid = None` in run_forever._loop keeps a single failed job from killing the worker coroutine (sqlite single-worker would wedge all ingest); the INNER except in run_worker_once is business-level. - New tests/test_engine_worker.py: 5 cases, _resolve_concurrency (auto/int/invalid/None) + the queue-empty short-circuit. Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
Wire the pre-existing engine/manage.py ConnectorManager draft. Engine delegates to self.connector_manager; probe/estimate/inspect/remove_connector become thin forwards. bind_remover now wires the add-failure rollback directly to connector_manager.remove_connector (no longer Engine.remove_connector, plan §4.1). - engine/manage.py (was untracked draft): drop _WORKER_CONNECT_TIMEOUT_S (unused); _JOB_STALE_AFTER_S now imported from .worker (single source, D2); fix probe/estimate/inspect/remove to call self._resolve_target (returns the tuple) instead of self._factory.resolve_target (returns TargetResolution, not unpackable) - the draft had 5 sites calling factory.resolve_target directly and crashing on unpack. - engine.py: 511 -> 247 lines. Assemble self.connector_manager; 4 methods thin delegates; drop 6 now-dead thin delegates (_resolve_target/_build_plugin/ _match_connector/_resolve_readonly_config/_read_text/_read_bytes - moved to manage.py); drop now-orphan imports (asyncio/json/os/uuid/datetime). bind_remover moved after the connector_manager assembly (it needs connector_manager.remove). _write_object_row stays (test_engine_orphan_gc calls it directly). - Tests: test_engine_forwards_to_ingest updated - bind_remover now wires connector_manager.remove_connector (design change, plan §4.1). _await_worker_drained extraction + stale comment cleanup left as follow-up (manage.py remove_connector keeps the inline while-loop). Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
Pull the inline "wait for worker to leave running" while-loop out of ConnectorManager.remove_connector into a dedicated _await_worker_drained(cid) method (plan §4.8). Behavior unchanged - the loop body (heartbeat check + stale reclaim via cancel_pending_running_tasks_for_job + cancel_running_job) moves verbatim, now callable + testable on its own. Comments updated to current method names (finalize_job/run_job; was _finalize_job/_run_job/_index_object). Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
Replace ReadService.grep's three-branch if/elif (pushdown -> BM25 -> linear) with a GrepStrategy chain (engine-redesign §4.6 stage 6). Each strategy's match(ctx) returns a final list (terminal - chain stops) or None (continue): Pushdown is terminal on hit; BM25 appends and continues; Linear appends and is terminal. Behavior identical to the original inline dispatch. - New engine/components/reads/grep_strategies.py: GrepContext dataclass + GrepStrategy Protocol + PushdownGrep/BM25Grep/LinearGrep + GrepChain. Branch bodies migrated verbatim. - read_service.py: import the chain; __init__ assembles self._grep_chain ([PushdownGrep(), BM25Grep(infra.milvus, ns), LinearGrep(objects)]); grep now builds a GrepContext + runs the chain. Drop now-unused re / _GREP_LINEAR_SCAN_MAX imports (moved to grep_strategies). - test_engine_read_service.py: _build_reads fake infra now carries a milvus stub (BM25Grep construction needs it; the tests don't run grep). cat CatRouter is left for a follow-up round (8 strategies, larger). Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
Replace ReadService.cat's 6-branch if/elif (meta/locator-lines/locator-record/
structured-stream/artifact-text/image-description/plain-text + density tail)
with a CatRouter first-match strategy table (engine-redesign §4.6 stage 6, cat
half). Each strategy applies(ctx)+read(ctx)->Any|None: None means "not
applicable / fall through", a non-None value is terminal. Behavior identical
to the original inline dispatch.
- New engine/components/reads/cat_strategies.py: CatContext dataclass +
CatStrategy Protocol + 7 strategies (Meta/LocatorLines/LocatorRecord/
StructuredStream/ArtifactText/ImageDescription/PlainText) + CatRouter +
_apply_density helper. Branch bodies migrated verbatim.
- D3 fallthrough (image+vlm try/except) becomes ImageDescriptionStrategy
returning None on provider error -> chain falls to PlainTextStrategy.
- density only applies to the artifact-text / plain-text paths (original
D1/D2/E), so _apply_density lives in those two strategies; meta/locator/
structured/image return before density (matching the original early returns).
- read_service.py: import the router; __init__ assembles self._cat_router;
cat now builds a CatContext + runs the router. Drop now-unused imports
(CONVERT_EXTS/_density_view/_locator_matches/_BARE_CAT_MAX_BYTES moved to
cat_strategies; head/tail/_read_full still use CONVERT_EXTS).
Stage 6 complete (grep chain + cat router both done).
Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
…rain tail Pull the tar-probe/zip-slip guard and the staging-path/connector-registration logic out of UploadService into standalone classes (engine-redesign §3.3/§4.7 follow-up), plus a shared _drain_after_upload tail (partial Template Method). - BundleValidator.validate(data, require_meta=False) -> (members, meta): wraps the tar probe + _validate_upload_member + (manifest-diff) .mfs-meta.json parse. require_meta=False validates every member (tar snapshot); require_meta=True checks .mfs-meta.json is a file + parses it (manifest-diff). Behavior identical to the two inline probe blocks. - StagingLocator(infra, ns, ingest): root(client_id, root) + connector(client_id, root) - the staging path sha1-sub + the file://<client_id><root> registration. - UploadService._drain_after_upload(cid, connector_uri, job_id, full, process): shared tail (get_connector_config + drain_job) that both ingest_upload and files_upload ended with. - ingest_upload / files_upload now call BundleValidator + self._locator + _drain_after_upload; the staging-step bodies (extractall vs renames/bytes/ deletions) stay inline - they differ enough that a shared template would risk behavior drift (left as a future follow-up). - test_engine_upload_service: staging-locator tests now call up._locator.root / up._locator.connector (the methods moved off UploadService). Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
…r value objects
Pull the retry/circuit-breaker logic out of IngestOrchestrator into pure
value objects (engine-redesign §4.4 follow-up).
- New engine/policies.py:
- ErrorClassifier.classify(e) -> 'auth'/'quota'/'retryable' (was
IngestOrchestrator.classify_error staticmethod).
- BackoffPolicy(initial_ms, max_ms).delay_ms(attempt) = min(initial*2**attempt, max)
(was the inline min(...) in process_with_retry).
- CircuitBreaker(threshold, initial).record_success() resets;
record_failure() increments and returns True once tripped (was the
consec_fail int + `if consec_fail >= threshold` in run_job_loop).
- ingest.py: import the three; __init__ assembles self._backoff;
process_with_retry uses ErrorClassifier.classify + self._backoff.delay_ms;
run_job_loop builds a CircuitBreaker(threshold, consec_fail) and calls
record_success/record_failure. run_job_loop signature is unchanged
(threshold, consec_fail) so the e2e tests that call it directly still work.
- test_ingest_orchestrator _Cfg fake now carries object_task.backoff_*.
Verified: 447 passed, 9 skipped; ruff format --check + ruff check clean.
…line Drop "Thin delegate / Kept on Engine" migration narrative, stage refs (stage 3/5/stage-4c), the D1 decision ref, stale _index_object references, and the design-doc section refs (§x.x) that littered engine.py and pipeline.py docstrings/inline comments. Tighten a few over-long scenario comments (cancelled_jobs, success-callback, retry/cancel) while keeping the non-obvious WHY. Aligns with the concise style of pipeline_supervisor.py. Zero behavior change: verified token-level that only COMMENT tokens and docstring STRINGs differ; NAME/OP/NUMBER tokens are identical.
…ents Drop the remaining "Extracted verbatim / Engine delegates / thin Engine delegates" migration narrative and §x.x / stage-N design-doc refs from the component docstrings: policies, worker, job_watcher, artifact_cache, object_repository, connector_factory, reads/__init__, reads/read_service. Two stale-claim fixes along the way: connector_factory no longer claims the factory is invoked "only through thin Engine delegates" (ReadService calls it directly), and artifact_cache's LocalArtifactCache is described as living on InfraStack (not Engine). ingest.py verified already clean. Zero behavior change: token-level check confirms only COMMENT and docstring STRING tokens differ; NAME/OP/NUMBER identical across all 8 files.
…ine files Final round of the engine comment cleanup. Drop "Extracted from ..." migration narrative and §x.x / stage-N / commit-hash refs from the remaining module docstrings: manage (stale _index_object -> mid-index), cat_strategies, grep_strategies, upload (also drops the c10db97 commit hash + stage-5/6 narrative), text_views. job_lane/ and producers/ are intentionally untouched per the task scope. Zero behavior change: token-level check confirms only COMMENT and docstring STRING tokens differ across all 5 files. CI gate ruff format --check src/ tests/ passes (187 files); full engine test sweep 194 passed / 9 skipped.
CodyTonn
force-pushed
the
rebuild_engine_v1
branch
from
July 22, 2026 03:37
95b1dbd to
6a99655
Compare
build_plugin() returns a BuiltPlugin dataclass, but probe() and estimate() still unpacked its result as a tuple (`plugin, _ = self._factory.build_plugin(...)`), raising "cannot unpack non-iterable BuiltPlugin object" on /v1/connectors/estimate (500). Use the existing _build_plugin helper, which returns the (plugin, ctx) tuple both call sites expect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zc277584121
reviewed
Jul 22, 2026
The decorator was dropped during extraction, so Engine._is_secret_key("api_key")
bound "api_key" to cls and raised TypeError: missing 1 required positional
argument: 'key'. Restore @classmethod (matching the _redact_config sibling) and
add a facade compat test that calls the credential delegates on the class itself
to catch the regression.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
resolve #166
Background
server/python/src/mfs_server/engine/engine.pywas a 1504-line god-class bearing 9 responsibilities (infra assembly, pipeline singleton, connector factory, ingest write orchestration, worker scheduling, upload protocol, artifact cache, read-path retrieval, connector management). Prior PRs extractedInfraStack/PipelineSupervisor/IngestOrchestrator/ConnectorFactory/ObjectRepository/ArtifactCacheService. This PR extracts the remaining 4 business components, completes the stage-6 strategy tables, and pulls out value objects, reducingEngineto a pure Facade.Overview
Engineshrinks from 1504 to 227 lines (-85%), now a pure Facade (thin delegates + lifecycle +_write_object_rowtest delegate). 9 commits, 18 files, +2283/-1378.Architecture
Enginecomposes 10 collaborating components (6 business + 2 infra + repository + process singleton); dependencies are unidirectional and acyclic.Design patterns:
open_path/match_connector) are public methods connecting directly toConnectorFactory+ObjectRepository, with no back-reference to Engine (decision D1: direct-connect + public method, not delayed-lambda)CatRouterfirst-match strategy table (7 strategies: Meta/LocatorLines/LocatorRecord/StructuredStream/ArtifactText/ImageDescription/PlainText); the D3 fallthrough (image+vlm failure) is expressed asread() -> Noneso the chain continuesGrepStrategychain (Pushdown->BM25->Linear),match() -> list|None(None=continue, list=terminal); Pushdown is terminal on hit, BM25 appends and continues, Linear appends and is terminalBundleValidator/StagingLocatorare standalone classes;_drain_after_uploadis a shared tail (partial Template Method)engine/policies.pyvalue objectsBehavior Equivalence
Zero external behavior change: the 20 call sites in
api/app.pyandserver/__main__.pykeep their signatures + theinfraattribute.Each component's method bodies are migrated verbatim; only
self.<dep>resolution targets change (e.g.self.infra.milvus->self._infra.milvus,self.objects->self._objects). Key invariants are preserved explicitly:except Exception: jid = None(coroutine-immortal, sqlite single-worker semantics) must stay inrun_forever._loop; the inner except (business-level: drain the queue) stays inrun_worker_once_apply_densityhelper; meta/locator/structured/image strategies return before density (matching the original early returns)_await_worker_drainedTests
test_engine_read_service.py(16 cases: text_views pure helpers + locators + search short-circuit + resolve_connector_uri)test_engine_upload_service.py(9 cases: _norm_rel + _validate_upload_member zip-slip + staging locator)test_engine_worker.py(5 cases: _resolve_concurrency + queue-empty short-circuit)test_cat_locator/test_grep_not_found/ e2e testseng._open_path->eng.reads.open_path, the D1 direct-connect strategy);test_engine_forwards_to_ingest's bind_remover assertion changed toconnector_manager.remove_connector(design change)ruff format --check+ruff checkclean (CI gate)Files
engine/components/reads/{__init__,text_views,read_service}.pyengine/components/reads/cat_strategies.pyengine/components/reads/grep_strategies.pyengine/components/upload.pyengine/worker.pyengine/manage.pyengine/policies.pyengine/engine.pyengine/ingest.pyRisk & Rollback
Verification