Skip to content

tdfs4ds.scheduling.parallel_plan

tdfs4ds.scheduling.parallel_plan

Parallel-execution planner for tdfs4ds feature-engineering processes (v0.3.1.147).

Given a seed list of processes / datasets / features, this module resolves each seed to its producing process(es), expands to the upstream process closure (every process the seeds depend on — the self-contained "build set", which doubles as a migration unit), and groups the closure into dependency waves: a topological layering of the process DAG where every process in a wave is mutually independent and can run in parallel, and waves run in order.

The heavy lifting reuses the persisted lineage graph (:func:tdfs4ds.lineage.lineage_store.load_lineage_digraph) and the process / feature / dataset catalogs. The graph algorithms (_build_process_dag / _compute_waves / _upstream_closure) are pure functions over a networkx.DiGraph so they can be unit-tested without a database.

Nothing here mutates the store — see :mod:tdfs4ds.scheduling.unify for the opt-in manager-unification mutators and :mod:tdfs4ds.scheduling.script for turning a plan into an executable script.

_ENTITY_SENTINEL_DB module-attribute

_ENTITY_SENTINEL_DB = '__ENTITY__'

_REGISTRATION_KEY module-attribute

_REGISTRATION_KEY = (_ENTITY_SENTINEL_DB, '__FEATURE_REGISTRATION__')

logger_safe

logger_safe(level, message, *args, **kwargs)

Wrapper around the global logger that only emits logs when tdfs4ds.DISPLAY_LOGS is True. level is a string like "info", "error", etc.

_process_catalog_df

_process_catalog_df(all_domains: bool = True)

The whole process catalog as a pandas frame (fail-open to empty).

_feature_catalog_df

_feature_catalog_df(all_domains: bool = True)

_dataset_catalog_df

_dataset_catalog_df()

_clean

_clean(v) -> Optional[str]

None/NaN/blank -> None, else the stripped string (LEFT JOIN nulls).

_nodeid_for_view

_nodeid_for_view(view_name: str) -> Optional[str]

Canonical 'DB.OBJECT' lineage node id for a catalog VIEW_NAME.

_build_nodeid_to_pid

_build_nodeid_to_pid(proc_df) -> Dict[str, str]

Map each process's lineage node id ('DB.OBJECT') -> PROCESS_ID.

_process_dependency_edges

_process_dependency_edges(G, nodeid_to_pid: Dict[str, str]) -> Set[Tuple[str, str]]

Contract the object lineage graph G onto process nodes.

G edges point parent -> child = downstream -> upstream (a process depends on its children). For each process p we take its transitive upstream objects (networkx.descendants(G, node_p)) and, for every one that is also a process q, emit an edge (q, p) meaning "q is upstream, run q before p". Transitive edges are harmless for the topological layering computed later.

_build_process_dag

_build_process_dag(edges, all_pids)

Build the process DiGraph: nodes = all_pids, edges = upstream -> dependent.

_upstream_closure

_upstream_closure(PG, seed_pids)

Seed processes + every process they depend on (ancestors in PG).

_compute_waves

_compute_waves(PG)

Topological generations of PG -> (waves, cycle).

waves is a list of lists; each inner list is one parallel wave (a topological generation, in_degree-0 first). cycle is None on a DAG, else a list of edges forming a cycle (waves is then []).

_resolve_seeds

_resolve_seeds(seeds, proc_df, ds_df, nodeid_to_pid)

Resolve a mixed list of process / dataset / feature names to producing PROCESS_IDs.

Returns (resolved, unresolved, seed_pids) where resolved maps each seed string to the list of process ids it resolved to. First matching category wins per seed: PROCESS_ID -> process view -> dataset -> feature.

_dataset_process_ids

_dataset_process_ids(database: str, name: str) -> List[str]

Producing process ids of a dataset, from its view DDL FEATURE_VERSION refs.

_feature_process_ids

_feature_process_ids(feature_name: str, proc_df) -> List[str]

Process ids whose registered FEATURE_NAMES contains feature_name.

_filter_content_key

_filter_content_key(database: Optional[str], view: Optional[str]) -> Optional[str]

Stable content hash of a FilterManager (col_names + time flag + rows).

_time_content_key

_time_content_key(database: Optional[str], view: Optional[str]) -> Optional[str]

Stable content hash of a TimeManager (timeline rows + data type).

_hash_key

_hash_key(obj) -> str

_entity_conflict_key

_entity_conflict_key(data_domain: Optional[str], entity_id: Optional[str]) -> Optional[Tuple[str, str]]

Synthetic conflict key for a process whose target FS_T_* table can't yet be resolved from the feature catalog (features not registered).

Physical tables are named FS_T_{domain}_{entity}{part}, so two processes on the same (DATA_DOMAIN, ENTITY_ID) may MERGE into the same table. This coarse key serialises them conservatively (it cannot distinguish data types, which the catalog would — see _pid_to_tables). Returns None when neither domain nor entity is known (nothing to key on).

_format_conflict_key

_format_conflict_key(db: str, tb: str) -> str

Render a conflict key for display: real table as 'db.table', synthetic as 'entity:...'.

_pid_to_tables

_pid_to_tables(proc_df, feat_df, refresh_tables=None) -> Dict[str, Set[Tuple[str, str]]]

Map each PROCESS_ID -> set of (FEATURE_DATABASE, FEATURE_TABLE) it writes.

Conservative: joins the process's FEATURE_NAMES to the feature catalog on (FEATURE_NAME, DATA_DOMAIN). Over-inclusion only makes the serialize guard more cautious (safe). Fail-open to empty.

Fresh-build fallback (issue 1): when a process resolves to zero exact tables (its features are not yet registered — the common case on a first parallel build), it is keyed instead on a synthetic (DATA_DOMAIN, ENTITY_ID) conflict key (:func:_entity_conflict_key) so it still serialises against other same-domain-same-entity processes. The fallback fires only when the exact set is empty, so once the catalog is populated the exact table drives the decision and same-entity/different-data-type processes still parallelise. Accepted rare gap: a registered process (exact table) and an unregistered one (synthetic key) that write the same physical table are not matched.

refresh_tables (0.3.3.30): {PROCESS_ID: (DATABASE, TABLE)} of the admin FS_R_* table a process with track_entity_refresh writes after its values. Its MERGE takes a table lock too, so two tracked processes of one (domain, entity, primary index, placement tag) are serialised even when their storage tables differ. Added after the fallback above, never in place of it. None / {} (no tracked process) leaves the result unchanged.

_serialize_groups

_serialize_groups(wave: List[str], pid_tables: Dict[str, Set[Tuple[str, str]]])

Union-find pids in a wave that share a target table -> conflict groups.

analyze_parallel_plan

analyze_parallel_plan(seeds=None, all_domains: bool = True, time_managers=None) -> Dict[str, Any]

Compute a parallel-execution plan for feature-engineering processes.

Parameters

seeds : list[str] | None Mixed list of process (id or view name) / dataset / feature names. The plan is the upstream closure of the seeds' producing processes (everything needed to (re)build them). None = the whole store. all_domains : bool Read every data domain (default). False restricts to the active tdfs4ds.DATA_DOMAIN. time_managers : ignored placeholder Reserved for supplying explicit TimeManagers to the content-equivalence check when the catalog has no TIME_* association yet.

Returns

dict seeds, resolved_seeds, unresolved_seeds, closure, processes (per-pid metadata incl. filter/time manager + wave index + lineage_unknown), waves (ordered list of parallel batches), wave_groups (per-wave grouping by (filter, time) manager), serialize_together (write-conflict groups), unify_candidates (equivalent-but-distinct managers), edges (process dependency edges within the closure), cycle (None or a cycle), plus counts.

_unify_candidates

_unify_candidates(closure, meta) -> Dict[str, List[Dict[str, Any]]]

Group distinct filter/time manager objects (in the closure) by content.