@1aboveio/skills 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/runtime/skills/distribution/generated/recipes.json +29 -19
- package/skills/backend/pyspark/LICENSE +3 -0
- package/skills/backend/pyspark/SKILL.md +116 -0
- package/skills/backend/pyspark/assets/templates/etl.py +696 -0
- package/skills/backend/pyspark/assets/templates/utils/__init__.py +1 -0
- package/skills/backend/pyspark/assets/templates/utils/hudi_metadata.py +14 -0
- package/skills/backend/pyspark/references/diagnosis-and-profiling.md +145 -0
- package/skills/backend/pyspark/references/etl-contract.md +107 -0
- package/skills/backend/pyspark/references/parity-testing.md +83 -0
- package/skills/backend/pyspark/references/production-validation.md +166 -0
- package/skills/backend/pyspark/references/transformation-design.md +162 -0
- package/skills/backend/pyspark/references/velocity-feature-calculation.md +193 -0
- package/skills/backend/pyspark/scripts/spark_eventlog_summary.py +223 -0
- package/skills/engineering/engineering-runtime/coherence/workflow.json +14 -14
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
- package/skills/engineering/resolve-issues/generated/workflow-repair-policy.json +11 -11
- package/skills/engineering/resolve-issues/scripts/run-state.mjs +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Safe default for repositories without a scoped Hudi metadata index.
|
|
2
|
+
|
|
3
|
+
Replace this helper with repository-owned read/write metadata configuration when
|
|
4
|
+
the target cluster maintains a Hudi metadata table. Returning no options keeps
|
|
5
|
+
the ETL template from enabling an index that writers may not maintain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def hudi_metadata_write_options(table: str) -> Dict[str, str]:
|
|
12
|
+
"""Return no table-specific Hudi metadata overrides by default."""
|
|
13
|
+
del table
|
|
14
|
+
return {}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Diagnosis And Profiling
|
|
2
|
+
|
|
3
|
+
Use this reference to decide whether a Spark job is fast enough, identify the
|
|
4
|
+
largest performance gap, and determine whether a fix is ready to ship. Focus on
|
|
5
|
+
the required evidence and outcome; choose environment-specific commands and
|
|
6
|
+
APIs from the available runtime.
|
|
7
|
+
|
|
8
|
+
## Performance Target
|
|
9
|
+
|
|
10
|
+
Use this default target unless the repository defines a stricter one:
|
|
11
|
+
|
|
12
|
+
- A medium batch job with fewer than 10 million input records should complete
|
|
13
|
+
within 5 minutes under properly isolated profiling conditions.
|
|
14
|
+
- Measure one complete scheduled job, not a selected fast stage.
|
|
15
|
+
- Report compute/validation, publication, and end-to-end wall time separately.
|
|
16
|
+
- A validate-only canary must meet the 5-minute compute/validation target, but it
|
|
17
|
+
does not prove publication or end-to-end write performance.
|
|
18
|
+
- Define a separate explicit target for larger datasets, streaming jobs, or jobs
|
|
19
|
+
whose required behavior is inherently outside this workload class.
|
|
20
|
+
|
|
21
|
+
Treat the target as failed until comparable evidence proves it passed.
|
|
22
|
+
|
|
23
|
+
## Require An Isolated, Comparable Profile
|
|
24
|
+
|
|
25
|
+
Accept a baseline or candidate profile only when all of these are fixed and
|
|
26
|
+
recorded:
|
|
27
|
+
|
|
28
|
+
- exact code hash and Spark, Python, Java, and table-format runtimes;
|
|
29
|
+
- immutable source snapshot and input-row denominator;
|
|
30
|
+
- equivalent job mode, history scope, validation, and write mode;
|
|
31
|
+
- stable executor/resource configuration and YARN queue;
|
|
32
|
+
- no competing Spark or warehouse workload that can materially consume the
|
|
33
|
+
same CPU, memory, network, disk, metastore, or source systems;
|
|
34
|
+
- readable driver logs, executor/container logs, and retained Spark event log;
|
|
35
|
+
- explicit statement of whether scheduler/queue wait and publication are
|
|
36
|
+
included in the reported time.
|
|
37
|
+
|
|
38
|
+
If isolation or scope equivalence cannot be established, mark the comparison
|
|
39
|
+
inconclusive instead of attributing the difference to code.
|
|
40
|
+
|
|
41
|
+
## Improve Logic, Not Parameters
|
|
42
|
+
|
|
43
|
+
Apply these rules to every performance change:
|
|
44
|
+
|
|
45
|
+
- Reduce physical work at the same input scope, resource profile, and required
|
|
46
|
+
behavior.
|
|
47
|
+
- Prefer fewer scans, pairs, rows, passes, shuffles, sorts, Python crossings,
|
|
48
|
+
state copies, and writes.
|
|
49
|
+
- Fix unbounded joins, repeated computation, poor state shape, missing pruning,
|
|
50
|
+
hot-key algorithms, and write amplification in the job logic or data layout.
|
|
51
|
+
- Do not claim a performance fix from changing shuffle partitions, executor
|
|
52
|
+
count/cores/memory, timeouts, broadcast thresholds or hints, AQE switches,
|
|
53
|
+
repartition counts, cache/persistence, or retry settings.
|
|
54
|
+
- Do not meet the target by dropping history, rows, columns, validations,
|
|
55
|
+
formulas, or publication work required by the job.
|
|
56
|
+
- Classify a proven cluster, storage, or configuration defect as an operational
|
|
57
|
+
issue. Correct and measure it separately; do not present it as a logic
|
|
58
|
+
optimization.
|
|
59
|
+
|
|
60
|
+
Accept a candidate only when its measured gain comes from a better computation
|
|
61
|
+
shape rather than more resources or relaxed work.
|
|
62
|
+
|
|
63
|
+
## Use The Evidence Source That Answers The Question
|
|
64
|
+
|
|
65
|
+
| Evidence source | Use it to determine |
|
|
66
|
+
|---|---|
|
|
67
|
+
| YARN ResourceManager, application report, and container status | Queue/allocation delay, application attempts, executor/container loss, resource contention, and final lifecycle state |
|
|
68
|
+
| Live Spark UI | Current jobs, stages, SQL executions, executors, task skew, shuffle, spill, GC, storage, and active bottlenecks |
|
|
69
|
+
| Spark History Server | The same Spark execution evidence after completion when retained event logs are available |
|
|
70
|
+
| Retained Spark event log and `scripts/spark_eventlog_summary.py` | Durable stage-attempt/task metrics, final adaptive plan-pattern counts, output evidence, and baseline/candidate comparison |
|
|
71
|
+
| Spark History Server REST API | Programmatic job, stage, task-summary, executor, SQL-plan, and environment evidence |
|
|
72
|
+
| YARN driver and executor/container logs | OOM, fetch failure, retries, executor loss, Python/JVM exceptions, writer behavior, and commit boundaries |
|
|
73
|
+
| Logical/physical plans and SQL runtime statistics | Scans, joins, exchanges, sorts, windows, Python nodes, estimated/runtime cardinality, and optimizer decisions |
|
|
74
|
+
| Source/table statistics, file/partition metadata, and table-format timeline | Input size, file shape, partition coverage, stale statistics, write amplification, and commit evidence |
|
|
75
|
+
| Cluster metrics such as Prometheus, Grafana, JMX, or node telemetry | CPU, memory, GC, disk, network, and infrastructure saturation outside the Spark plan |
|
|
76
|
+
| Code and test inspection | Unbounded joins, repeated actions, unnecessary scans, collect/explode growth, UDF boundaries, incorrect history scope, and missing plan-shape protection |
|
|
77
|
+
|
|
78
|
+
Use code inference to form a hypothesis when runtime evidence is incomplete.
|
|
79
|
+
Do not present inference alone as measured performance proof.
|
|
80
|
+
|
|
81
|
+
## Identify The Dominant Gap
|
|
82
|
+
|
|
83
|
+
Name the dominant gap before changing code. Classify it as one or more of:
|
|
84
|
+
|
|
85
|
+
- queue, allocation, startup, or dependency delay;
|
|
86
|
+
- source listing, scan, partition pruning, or small-file overhead;
|
|
87
|
+
- intermediate cardinality explosion from join, explode, or repeated grouping;
|
|
88
|
+
- shuffle, sort, exchange, or network transfer;
|
|
89
|
+
- hot-key or partition skew and straggler tasks;
|
|
90
|
+
- Python serialization, UDF, or whole-group memory overhead;
|
|
91
|
+
- JVM/Python memory pressure, GC, spill, or executor loss;
|
|
92
|
+
- driver collection, planning, or result-size pressure;
|
|
93
|
+
- repeated computation across Spark actions;
|
|
94
|
+
- table writer, file layout, commit, or downstream storage latency.
|
|
95
|
+
|
|
96
|
+
Require an evidence chain:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
performance target gap
|
|
100
|
+
-> application/job/stage/task evidence
|
|
101
|
+
-> physical-plan or runtime operator
|
|
102
|
+
-> responsible transformation or infrastructure constraint
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Fix the earliest responsible cause in that chain. Do not tune unrelated Spark
|
|
106
|
+
settings merely because the slow work becomes visible at a later action.
|
|
107
|
+
|
|
108
|
+
## Resolution Workflow
|
|
109
|
+
|
|
110
|
+
1. **Measure performance:** capture an isolated baseline and quantify the gap to
|
|
111
|
+
the 5-minute target using aggregate-only evidence.
|
|
112
|
+
2. **Identify the gap:** locate the dominant job/stage/task and connect it to a
|
|
113
|
+
plan operator, code shape, data skew, writer, or infrastructure constraint.
|
|
114
|
+
3. **Make the fix:** reduce the responsible logical or physical work without
|
|
115
|
+
tuning parameters, adding resources, or weakening output identity, ordering,
|
|
116
|
+
boundaries, arithmetic, history, validation, or write behavior.
|
|
117
|
+
4. **Validate locally:** prove required behavior, bidirectional parity, boundary
|
|
118
|
+
cases, and the intended physical-plan change with focused tests.
|
|
119
|
+
5. **Canary and profile in the cluster:** run the production entry point in
|
|
120
|
+
validate-only mode on the comparable isolated snapshot; retain logs, prove
|
|
121
|
+
zero writes, and compare the same metrics with the baseline.
|
|
122
|
+
6. **Ship:** open and review the PR, pass CI, enqueue and merge, deploy the merged
|
|
123
|
+
commit, then run authorized production validation and reconciliation.
|
|
124
|
+
|
|
125
|
+
Repeat the loop when the target is still missed or the dominant gap moves.
|
|
126
|
+
|
|
127
|
+
## Minimum Evidence
|
|
128
|
+
|
|
129
|
+
- **Scope:** target, input-row denominator, snapshot/date range, and
|
|
130
|
+
validate-only or write-enabled mode.
|
|
131
|
+
- **Run identity:** baseline/candidate application IDs and code hashes, plus
|
|
132
|
+
confirmation of the same runtime, resources, and isolation conditions.
|
|
133
|
+
- **Performance:** wall time, dominant stage, maximum task, cardinality,
|
|
134
|
+
shuffle/spill, logic change, and pass/fail against the 5-minute target.
|
|
135
|
+
- **Correctness:** local parity/plan results and canary blocker/output status.
|
|
136
|
+
- **Delivery:** deployed hash, publication/reconciliation result when applicable,
|
|
137
|
+
and remaining limitations.
|
|
138
|
+
|
|
139
|
+
Keep evidence aggregate-only. Never print credentials or entity-level rows.
|
|
140
|
+
|
|
141
|
+
## Primary Spark References
|
|
142
|
+
|
|
143
|
+
- [Monitoring and Instrumentation](https://spark.apache.org/docs/latest/monitoring.html)
|
|
144
|
+
- [Running Spark on YARN](https://spark.apache.org/docs/latest/running-on-yarn.html)
|
|
145
|
+
- [Spark SQL Performance Tuning](https://spark.apache.org/docs/latest/sql-performance-tuning.html)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# ETL Contract
|
|
2
|
+
|
|
3
|
+
Read this reference during Step 1 before creating or changing a Spark ETL job.
|
|
4
|
+
Use [the complete ETL module](../assets/templates/etl.py) when the repository
|
|
5
|
+
does not already provide an equivalent base module. Prefer and extend an
|
|
6
|
+
established repository module instead of introducing a parallel framework.
|
|
7
|
+
|
|
8
|
+
The bundled `etl.py` is derived from a complete production implementation. It
|
|
9
|
+
retains every source adapter, session helper, and writer, with template-only
|
|
10
|
+
fixes for empty repartition settings, recursive CLI flags, logging, and a safe
|
|
11
|
+
Hudi metadata default.
|
|
12
|
+
|
|
13
|
+
## Included ETL Modules
|
|
14
|
+
|
|
15
|
+
| Module | Included behavior |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Spark session helpers | Native Spark and Spark Connect creation, active-runtime detection, nested leases, and ownership-safe shutdown |
|
|
18
|
+
| `Etl` | Date-scoped extraction, full/per-day execution, schema completion, partition preparation, Spark/Hudi publication, and CLI entry point |
|
|
19
|
+
| `MongoDbEtl` | MongoDB connector extraction with optional declared schema |
|
|
20
|
+
| `JdbcEtl` | Predicate-scoped JDBC extraction, fetch sizing, and partitioned reads when date bounds are available |
|
|
21
|
+
| `MySqlEtl` | MySQL JDBC driver specialization |
|
|
22
|
+
| `MsSqlEtl` | SQL Server quoting, URL-property handling, SSL defaults, and single-partition extraction for redirect-sensitive servers |
|
|
23
|
+
| `JsonEtl` | Recursive JSON extraction with optional schema |
|
|
24
|
+
| `CsvEtl` | Recursive/header-aware CSV extraction with optional schema |
|
|
25
|
+
|
|
26
|
+
## Declare The Contract Before Transformation Logic
|
|
27
|
+
|
|
28
|
+
| Surface | Required declarations | `etl.py` mapping |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| Input parameters | Names, defaults, validation, secrets by reference, and source scope | constructor and `run_from_cli`; `start_date`, `end_date`, `url`, `bulk` |
|
|
31
|
+
| Runtime | Exact Python and PySpark versions | deployment/runtime files outside the class; verify before session creation |
|
|
32
|
+
| Concurrency | Overlap policy, idempotency, lock ownership, and retry/restart behavior | `concurrency_mode`, `zookeeper`, Hudi lock key/options, scheduler policy |
|
|
33
|
+
| Output table | Destination and `spark_table` versus `hudi_table` | `dst_db`, `dst_tbl`, `path`, `table_type` |
|
|
34
|
+
| Output identity | Complete record key and schema | `id`, transformed DataFrame schema |
|
|
35
|
+
| Output layout | Partition columns and Hudi conflict order | `par_cols`, `ts` precombine field |
|
|
36
|
+
| Write behavior | Append, overwrite, upsert, bulk insert, or insert overwrite | `hudi_mode`, `hudi_mode_override`, `load_hudi`, `load_spark` |
|
|
37
|
+
| Spark runtime | Native Spark versus Spark Connect and session owner | `create_spark_session`, `managed_spark_session`, `spark_remote` |
|
|
38
|
+
| Processing scope | Full versus incremental/per-day | `bulk`, `filter_by`, `offset`, `start_date`, `end_date`, `process` |
|
|
39
|
+
| Incremental state | Watermark, lookback/checkpoint, restart boundary, and late-data behavior | `filter_by`, `offset`, subclass/checkpoint implementation |
|
|
40
|
+
|
|
41
|
+
Do not design the transformation until every applicable surface has an explicit
|
|
42
|
+
value. Reject defaults that can silently change data scope, key semantics,
|
|
43
|
+
concurrency, or publication behavior.
|
|
44
|
+
|
|
45
|
+
## Adapt Repository-Specific Dependencies
|
|
46
|
+
|
|
47
|
+
The complete module depends on its source warehouse. Resolve each dependency
|
|
48
|
+
before reuse:
|
|
49
|
+
|
|
50
|
+
- The bundled [`utils/hudi_metadata.py`](../assets/templates/utils/hudi_metadata.py)
|
|
51
|
+
returns no metadata overrides. Replace it only when the target repository
|
|
52
|
+
maintains a scoped Hudi metadata index on both reads and writes.
|
|
53
|
+
- Pin compatible PySpark, pandas, python-dateutil, Typer, and typing-extensions
|
|
54
|
+
versions.
|
|
55
|
+
- Provide the Hudi Spark bundle and Hive sync configuration expected by the
|
|
56
|
+
target cluster.
|
|
57
|
+
- Provide MongoDB, MySQL, and SQL Server connector/driver artifacts only for the
|
|
58
|
+
adapters the job uses.
|
|
59
|
+
- Replace Zookeeper lock endpoints, base path, port, and lock ownership with the
|
|
60
|
+
target repository's concurrency mechanism.
|
|
61
|
+
- Review Hudi key generator, complex-key encoding, metadata, schema reconcile,
|
|
62
|
+
initial-load fallback, and write operation against the deployed Hudi version.
|
|
63
|
+
- Route connection strings and credentials through the repository's secret
|
|
64
|
+
mechanism. Do not place secret values in class attributes, CLI examples, or
|
|
65
|
+
logs.
|
|
66
|
+
|
|
67
|
+
Remove unused adapters only after confirming no job imports them. Keep the
|
|
68
|
+
bundled asset as the reference and adapt a copy inside the target repository.
|
|
69
|
+
|
|
70
|
+
## Close Contract Gaps Before Use
|
|
71
|
+
|
|
72
|
+
The bundled module implements `extract -> transform -> load`. Before adopting
|
|
73
|
+
it as a job base, require the target repository to add or confirm:
|
|
74
|
+
|
|
75
|
+
- an aggregate validation boundary before every `load` path;
|
|
76
|
+
- an in-job validate-only guard that logs publication was skipped;
|
|
77
|
+
- complete output-identity and required-column checks;
|
|
78
|
+
- explicit overlapping-run and idempotency behavior;
|
|
79
|
+
- exact source snapshot/restart semantics for incremental runs;
|
|
80
|
+
- focused tests for native/Connect, full/incremental, and selected adapters;
|
|
81
|
+
- independent post-write reconciliation.
|
|
82
|
+
|
|
83
|
+
Do not hide these gaps in wrapper scripts. Validation and no-write controls
|
|
84
|
+
belong before the writer in the job process.
|
|
85
|
+
|
|
86
|
+
## Contract Rules
|
|
87
|
+
|
|
88
|
+
- Pin validation and publication to the same immutable source boundary.
|
|
89
|
+
- Treat `ts` as Hudi conflict-resolution/precombine order, not physical row
|
|
90
|
+
sort order.
|
|
91
|
+
- Declare partition columns independently from record keys.
|
|
92
|
+
- Reject Hudi output without a complete record key and precombine field.
|
|
93
|
+
- Reject incremental mode without a filter/watermark and exact restart boundary.
|
|
94
|
+
- Reject optimistic concurrency without a working lock provider.
|
|
95
|
+
- Do not rely on scheduler timing as the single-writer guarantee.
|
|
96
|
+
- Do not present `repartition`, JDBC partition count, fetch size, or writer
|
|
97
|
+
option changes as logic optimization; classify required configuration fixes
|
|
98
|
+
as separate operational work.
|
|
99
|
+
|
|
100
|
+
## Minimum Review Evidence
|
|
101
|
+
|
|
102
|
+
- Contract fields and runtime dependencies are explicit.
|
|
103
|
+
- The selected adapter is tested against its real connector/runtime.
|
|
104
|
+
- Writer options match table type, key, order, partitions, concurrency, and
|
|
105
|
+
write mode.
|
|
106
|
+
- Validation and validate-only controls precede every writer.
|
|
107
|
+
- Full/incremental and native/Connect paths used by the job are covered.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Parity Testing
|
|
2
|
+
|
|
3
|
+
Use this playbook for semantics-preserving Spark rewrites.
|
|
4
|
+
|
|
5
|
+
## Keep The Old Path As A Bounded Oracle
|
|
6
|
+
|
|
7
|
+
Retain the previous implementation only in tests while parity is being proven.
|
|
8
|
+
Use small fixtures that are dense enough to expose the old complexity but
|
|
9
|
+
bounded enough to run reliably.
|
|
10
|
+
|
|
11
|
+
## Compare Full Rows In Both Directions
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
expected = old_build(source).select(*OUTPUT_COLUMNS)
|
|
15
|
+
actual = new_build(source).select(*OUTPUT_COLUMNS)
|
|
16
|
+
|
|
17
|
+
assert expected.exceptAll(actual).count() == 0
|
|
18
|
+
assert actual.exceptAll(expected).count() == 0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Counts alone cannot detect wrong values, swapped identities, or duplicate rows.
|
|
22
|
+
Use `exceptAll`, not `subtract`, when duplicate multiplicity matters.
|
|
23
|
+
|
|
24
|
+
## Add Literal Boundary Assertions
|
|
25
|
+
|
|
26
|
+
Parity can be self-confirming if oracle and candidate share the same mistake.
|
|
27
|
+
Assert critical rules directly:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
by_id = {row.event_id: row for row in actual.collect()}
|
|
31
|
+
assert by_id["at-24h"].count_24h == 2 # inclusive lower boundary
|
|
32
|
+
assert by_id["after-24h"].count_24h == 1 # one unit outside
|
|
33
|
+
assert by_id["same-time-b"].count_1h == 2 # stable tie ordering
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Keep collected fixtures synthetic and non-sensitive.
|
|
37
|
+
|
|
38
|
+
## Minimum Fixture Matrix
|
|
39
|
+
|
|
40
|
+
Cover:
|
|
41
|
+
|
|
42
|
+
- empty and single-event groups;
|
|
43
|
+
- exact lower/upper boundaries and one unit beyond;
|
|
44
|
+
- same-timestamp events with every tie-breaker;
|
|
45
|
+
- null key, event time, amount, FX, and counterparty;
|
|
46
|
+
- both directions and relevant statuses;
|
|
47
|
+
- repeated and distinct counterparties;
|
|
48
|
+
- state open, partial close, full close, eviction, and re-entry;
|
|
49
|
+
- floating-point add/remove residue;
|
|
50
|
+
- one dense hot key;
|
|
51
|
+
- duplicate and missing output identities.
|
|
52
|
+
|
|
53
|
+
## Test The Physical Shape
|
|
54
|
+
|
|
55
|
+
Assert the expensive relation is absent without snapshotting the whole plan:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
plan = actual._jdf.queryExecution().executedPlan().toString().lower()
|
|
59
|
+
assert not ("join" in plan and "event_time" in plan and "interval" in plan)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Prefer a targeted helper that counts known pair-join nodes. Also assert the
|
|
63
|
+
expected grouped-sweep or built-in operator is present when that guards the
|
|
64
|
+
optimization.
|
|
65
|
+
|
|
66
|
+
## Mutation-Prove Important Tests
|
|
67
|
+
|
|
68
|
+
Temporarily introduce representative faults and confirm tests fail:
|
|
69
|
+
|
|
70
|
+
- change `< cutoff` to `<= cutoff`;
|
|
71
|
+
- remove a tie-breaker;
|
|
72
|
+
- use ordinary rolling addition/subtraction;
|
|
73
|
+
- reintroduce the pair join;
|
|
74
|
+
- omit a downstream-required column.
|
|
75
|
+
|
|
76
|
+
Record red/green evidence for high-risk money, date, and identity semantics.
|
|
77
|
+
|
|
78
|
+
## Match Test Cost To Ownership
|
|
79
|
+
|
|
80
|
+
Behavioral Spark tests belong on files that own formulas, source scope,
|
|
81
|
+
history, or state transitions. Unrelated upstream changes may use a focused
|
|
82
|
+
required-column check instead of rerunning an expensive full-history suite.
|
|
83
|
+
Do not demote behavioral coverage for a semantic change.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Production Validation
|
|
2
|
+
|
|
3
|
+
Use this playbook before production-scale profiling, canaries, backfills, or
|
|
4
|
+
writes.
|
|
5
|
+
|
|
6
|
+
## Fail-Closed Admission
|
|
7
|
+
|
|
8
|
+
Do not launch until all applicable facts are recorded and valid:
|
|
9
|
+
|
|
10
|
+
- exact commit/module hash;
|
|
11
|
+
- production Spark, Python, Java, and table-format runtimes;
|
|
12
|
+
- source snapshot or date boundary;
|
|
13
|
+
- scheduler pause/ownership state;
|
|
14
|
+
- zero competing warehouse work;
|
|
15
|
+
- explicit validate-only or write-enabled mode;
|
|
16
|
+
- durable writable log and event-log destination;
|
|
17
|
+
- restart/recovery plan for writes.
|
|
18
|
+
|
|
19
|
+
Classify existing processes and working-tree changes before touching them. Do
|
|
20
|
+
not kill or overwrite another operator's work.
|
|
21
|
+
|
|
22
|
+
## Run A Validate-Only Canary Before The Writer
|
|
23
|
+
|
|
24
|
+
Implement the guard in the Spark job, not only in a wrapper. Keep each stage
|
|
25
|
+
literal:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import json
|
|
29
|
+
|
|
30
|
+
from pyspark.sql import functions as F
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_candidate(candidate):
|
|
34
|
+
# first() is a Spark action: it executes the lazy transform plan.
|
|
35
|
+
metrics = candidate.agg(
|
|
36
|
+
F.count("*").alias("row_count"),
|
|
37
|
+
F.countDistinct("event_id").alias("distinct_event_count"),
|
|
38
|
+
F.sum(
|
|
39
|
+
F.when(F.col("event_id").isNull(), 1).otherwise(0)
|
|
40
|
+
).alias("null_event_id_count"),
|
|
41
|
+
F.sum(
|
|
42
|
+
F.when(F.col("event_count_1h") < 0, 1).otherwise(0)
|
|
43
|
+
).alias("invalid_event_count_1h"),
|
|
44
|
+
F.count("amount_usd_1h").alias("non_null_amount_usd_1h_count"),
|
|
45
|
+
).first().asDict()
|
|
46
|
+
|
|
47
|
+
blockers = []
|
|
48
|
+
if metrics["row_count"] != metrics["distinct_event_count"]:
|
|
49
|
+
blockers.append("event_id is not unique")
|
|
50
|
+
if metrics["null_event_id_count"]:
|
|
51
|
+
blockers.append("event_id contains nulls")
|
|
52
|
+
if metrics["invalid_event_count_1h"]:
|
|
53
|
+
blockers.append("event_count_1h contains negative values")
|
|
54
|
+
if blockers:
|
|
55
|
+
raise RuntimeError(f"validation blocked: {blockers}")
|
|
56
|
+
return metrics
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(validate_only, source_snapshot):
|
|
60
|
+
# Bind every action to the same immutable source version or date boundary.
|
|
61
|
+
source = extract(source_snapshot)
|
|
62
|
+
candidate = transform(source) # build a lazy DataFrame plan
|
|
63
|
+
|
|
64
|
+
metrics = validate_candidate(candidate) # execute validation aggregates
|
|
65
|
+
print(json.dumps(metrics, sort_keys=True))
|
|
66
|
+
|
|
67
|
+
if validate_only: # canary stops before any writer call
|
|
68
|
+
print("validate-only: skipping publication")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
publish(candidate) # execute the writer from the same source
|
|
72
|
+
reconcile_published_scope(metrics) # independently read back aggregates
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Validation and publication are separate Spark actions, so Spark may recompute
|
|
76
|
+
the candidate for publication. Pin every source read to the same immutable
|
|
77
|
+
snapshot, table version, or exact date boundary. If the source cannot be pinned,
|
|
78
|
+
the validation result does not prove what the writer will publish; stop and
|
|
79
|
+
redesign the run.
|
|
80
|
+
|
|
81
|
+
Adapt identity and blocker checks to the table's required behavior. Reference
|
|
82
|
+
every derived output in at least one validation aggregate; otherwise Catalyst
|
|
83
|
+
may prune an unreferenced calculation and the canary will not exercise the full
|
|
84
|
+
candidate. Validation must use aggregate evidence and fail before publication.
|
|
85
|
+
The canary exercises the production-shaped extract, transform, and validation
|
|
86
|
+
path without calling the writer.
|
|
87
|
+
|
|
88
|
+
## Avoid Explicit Cache And Persistence
|
|
89
|
+
|
|
90
|
+
Do not add `.cache()` or `.persist()` as a performance fix. They add storage,
|
|
91
|
+
serialization, eviction, and cleanup concerns without improving the logical
|
|
92
|
+
work. Prefer a better transformation shape and a pinned source snapshot.
|
|
93
|
+
|
|
94
|
+
Treat existing persistence as separate operational behavior: do not expand it
|
|
95
|
+
while optimizing the job, and remove it when repository tests and same-snapshot
|
|
96
|
+
evidence show it is unnecessary.
|
|
97
|
+
|
|
98
|
+
## Launch Through The Production Entry Point
|
|
99
|
+
|
|
100
|
+
Use the same `spark-submit` path and driver/executor runtimes as production:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
export PYSPARK_DRIVER_PYTHON="$DRIVER_PYTHON"
|
|
104
|
+
export PYSPARK_PYTHON="$EXECUTOR_PYTHON"
|
|
105
|
+
|
|
106
|
+
spark-submit \
|
|
107
|
+
--name "$CANARY_NAME" \
|
|
108
|
+
--conf spark.pyspark.driver.python="$DRIVER_PYTHON" \
|
|
109
|
+
--conf spark.pyspark.python="$EXECUTOR_PYTHON" \
|
|
110
|
+
--conf spark.eventLog.enabled=true \
|
|
111
|
+
job.py --validate-only
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Do not replace deployed scheduler files with an unmerged staged module. Stage
|
|
115
|
+
an isolated artifact or deploy merged code through the normal path.
|
|
116
|
+
|
|
117
|
+
## Prove The Canary Did Not Write
|
|
118
|
+
|
|
119
|
+
Require all three forms of evidence:
|
|
120
|
+
|
|
121
|
+
1. Validation completed with zero blockers.
|
|
122
|
+
2. The terminal log says publication was skipped.
|
|
123
|
+
3. The event log reports zero Spark output records and bytes.
|
|
124
|
+
|
|
125
|
+
Parse retained logs with upstream failures visible:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
set -o pipefail
|
|
129
|
+
hdfs dfs -cat "$EVENT_LOG" | \
|
|
130
|
+
python {baseDir}/scripts/spark_eventlog_summary.py > canary-summary.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Record application ID, shell/event-log wall, stage/task metrics, plan nodes,
|
|
134
|
+
shuffle, spill, output, code hash, and snapshot denominator.
|
|
135
|
+
|
|
136
|
+
## Run Write Verification Separately
|
|
137
|
+
|
|
138
|
+
Run write-enabled verification only after merge, deployment, successful
|
|
139
|
+
validate-only evidence, and explicit authorization. Re-check admission just
|
|
140
|
+
before launch.
|
|
141
|
+
|
|
142
|
+
Verify row count, distinct identity count, date/partition coverage, commit
|
|
143
|
+
success, schema, consumer compatibility, validation blockers, write mode,
|
|
144
|
+
single-writer constraints, and downstream readability.
|
|
145
|
+
|
|
146
|
+
Report compute/validation, publication, and total time separately. Never
|
|
147
|
+
present validate-only time as end-to-end write performance.
|
|
148
|
+
|
|
149
|
+
If a write run is cancelled, prove from logs/source that publication was not
|
|
150
|
+
reached. YARN `KILLED` alone does not prove no commit occurred.
|
|
151
|
+
|
|
152
|
+
## Keep Evidence Aggregate-Only
|
|
153
|
+
|
|
154
|
+
Safe evidence includes counts, null rates, distinct counts, min/max dates,
|
|
155
|
+
hashes, stage metrics, plan-node counts, and commit IDs. Do not print
|
|
156
|
+
credentials, entity rows, account identifiers, or per-entity values.
|
|
157
|
+
|
|
158
|
+
## Restore Ownership
|
|
159
|
+
|
|
160
|
+
After the run:
|
|
161
|
+
|
|
162
|
+
1. Confirm application and wrapper processes are terminal.
|
|
163
|
+
2. Remove agent-owned watchers and temporary processes.
|
|
164
|
+
3. Restore scheduler pause state or ownership.
|
|
165
|
+
4. Separate unrelated downstream failures from the optimized job.
|
|
166
|
+
5. Publish evidence with limitations and unmeasured surfaces.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Transformation Design
|
|
2
|
+
|
|
3
|
+
Use this playbook when implementing or restructuring Spark transformations.
|
|
4
|
+
The snippets illustrate shapes; adapt names, schemas, and state to the job's
|
|
5
|
+
required behavior.
|
|
6
|
+
|
|
7
|
+
## Define Required Behavior Before Operators
|
|
8
|
+
|
|
9
|
+
Record output identity/cardinality, event-time and timezone, deterministic
|
|
10
|
+
ordering, time boundaries, null/tombstone behavior, arithmetic, and required
|
|
11
|
+
history. Performance work preserves this behavior unless the user explicitly
|
|
12
|
+
approves a change.
|
|
13
|
+
|
|
14
|
+
## Choose The Shallowest Sufficient Mechanism
|
|
15
|
+
|
|
16
|
+
Prefer, in order:
|
|
17
|
+
|
|
18
|
+
1. Built-in column expressions and aggregations.
|
|
19
|
+
2. Spark windows when state and partition size are bounded.
|
|
20
|
+
3. Exact temporal bucketing with overlap correction.
|
|
21
|
+
4. One sorted grouped sweep for stateful transitions.
|
|
22
|
+
5. Python/pandas UDFs only when they remove substantially more distributed work
|
|
23
|
+
than they add.
|
|
24
|
+
|
|
25
|
+
Measure rather than assuming a UDF, Arrow, or built-in rewrite is faster.
|
|
26
|
+
|
|
27
|
+
## Keep One DataFrame Boundary
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
def build_features(events: DataFrame, rules: DataFrame) -> DataFrame:
|
|
31
|
+
normalized = normalize_events(events, rules)
|
|
32
|
+
return add_stateful_features(normalized)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Put complex state transitions in pure functions with explicit input and output
|
|
36
|
+
schemas so boundary semantics can be tested without a cluster.
|
|
37
|
+
|
|
38
|
+
## Use Built-In Windows For Independent Aggregates
|
|
39
|
+
|
|
40
|
+
When the feature is a standard rolling aggregate and all same-time peers should
|
|
41
|
+
share the same frame, keep execution inside Catalyst:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pyspark.sql import Window, functions as F
|
|
45
|
+
|
|
46
|
+
one_hour = (
|
|
47
|
+
Window.partitionBy("entity_id")
|
|
48
|
+
.orderBy(F.col("event_time").cast("long"))
|
|
49
|
+
.rangeBetween(-3600, 0)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
features = (
|
|
53
|
+
events.withColumn("event_count_1h", F.count(F.lit(1)).over(one_hour))
|
|
54
|
+
.withColumn("amount_usd_1h", F.sum("amount_usd").over(one_hour))
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Check peer semantics carefully: a time-only range frame includes every row with
|
|
59
|
+
the same timestamp. It is wrong when same-time visibility depends on stable
|
|
60
|
+
secondary keys.
|
|
61
|
+
|
|
62
|
+
## Keep Bounded State Native With Higher-Order Aggregation
|
|
63
|
+
|
|
64
|
+
For bounded per-key arrays and group-level state, `functions.aggregate` keeps
|
|
65
|
+
the fold in Catalyst and does not call Python per group:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
ordered = events.groupBy("entity_id").agg(
|
|
69
|
+
F.sort_array(
|
|
70
|
+
F.collect_list(
|
|
71
|
+
F.struct(
|
|
72
|
+
"event_time",
|
|
73
|
+
"event_id",
|
|
74
|
+
F.col("amount_usd").cast("double").alias("amount_usd"),
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
).alias("events")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
initial = F.struct(
|
|
81
|
+
F.lit(0.0).alias("balance"),
|
|
82
|
+
F.lit(0).alias("event_count"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
state = ordered.select(
|
|
86
|
+
"entity_id",
|
|
87
|
+
F.aggregate(
|
|
88
|
+
"events",
|
|
89
|
+
initial,
|
|
90
|
+
lambda acc, event: F.struct(
|
|
91
|
+
(acc["balance"] + event["amount_usd"]).alias("balance"),
|
|
92
|
+
(acc["event_count"] + 1).alias("event_count"),
|
|
93
|
+
),
|
|
94
|
+
).alias("state"),
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This example emits final group state. Carrying one output per event inside the
|
|
99
|
+
accumulator grows an array and can become expensive, so use it only when group
|
|
100
|
+
size and output state are bounded.
|
|
101
|
+
|
|
102
|
+
## Choose The Stateful Execution API Deliberately
|
|
103
|
+
|
|
104
|
+
Arbitrary per-row FIFO and temporal eviction have no general Catalyst-native
|
|
105
|
+
PySpark batch operator. Choose based on the actual state shape:
|
|
106
|
+
|
|
107
|
+
| State shape | Preferred API | Main constraint |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| Independent rolling aggregates | `Window` | Same-time peer semantics must fit the frame |
|
|
110
|
+
| Bounded array fold or final group state | `functions.aggregate` | Collected array and accumulator must remain bounded |
|
|
111
|
+
| Bounded batch group needing Python | [`applyInPandas`](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.GroupedData.applyInPandas.html) | Full shuffle; each group is loaded into memory |
|
|
112
|
+
| Stateful streaming group | [`applyInPandasWithState`](https://spark.apache.org/docs/latest/api/python/reference/pyspark.sql/api/pyspark.sql.GroupedData.applyInPandasWithState.html) | Streaming API, not a batch replacement |
|
|
113
|
+
| Large or unbounded sequential batch state | Redesign with exact bucketing, Scala `mapGroups`, or a stateful engine | More implementation and operational complexity |
|
|
114
|
+
|
|
115
|
+
Keep the transition function pure regardless of the execution API. If a Python
|
|
116
|
+
grouped path replaces a much worse relation, benchmark it against the same
|
|
117
|
+
snapshot and report Python serialization and maximum-group memory explicitly.
|
|
118
|
+
|
|
119
|
+
## Preserve Exact Output Columns And Row Identity
|
|
120
|
+
|
|
121
|
+
Follow these rules:
|
|
122
|
+
|
|
123
|
+
1. Declare the complete output identity before writing the transformation.
|
|
124
|
+
2. Carry every identity column through grouping, sorting, UDF, and explode
|
|
125
|
+
boundaries.
|
|
126
|
+
3. Require one grouped result per identity before joining it to the base rows.
|
|
127
|
+
4. Join on the complete identity. Do not substitute customer, timestamp, or
|
|
128
|
+
another non-unique lookup key.
|
|
129
|
+
5. Declare the required output columns and their order. Never use `select("*")`
|
|
130
|
+
for the final candidate.
|
|
131
|
+
6. Drop helper, duplicate, and intermediate columns at the final projection.
|
|
132
|
+
7. Fail tests and production validation on null, missing, or duplicate
|
|
133
|
+
identities and on unexpected output columns.
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
IDENTITY_COLUMNS = ["event_id"]
|
|
137
|
+
BASE_OUTPUT_COLUMNS = [
|
|
138
|
+
"event_id",
|
|
139
|
+
"entity_id",
|
|
140
|
+
"event_time",
|
|
141
|
+
"amount_usd",
|
|
142
|
+
]
|
|
143
|
+
FEATURE_COLUMNS = ["event_count_1h", "amount_usd_1h"]
|
|
144
|
+
OUTPUT_COLUMNS = BASE_OUTPUT_COLUMNS + FEATURE_COLUMNS
|
|
145
|
+
|
|
146
|
+
feature_projection = features_by_event.select(
|
|
147
|
+
*IDENTITY_COLUMNS,
|
|
148
|
+
*FEATURE_COLUMNS,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
candidate = (
|
|
152
|
+
events.select(*BASE_OUTPUT_COLUMNS)
|
|
153
|
+
.join(feature_projection, on=IDENTITY_COLUMNS, how="left")
|
|
154
|
+
.select(*OUTPUT_COLUMNS)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
assert candidate.columns == OUTPUT_COLUMNS
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For composite identity, list every component, for example
|
|
161
|
+
`IDENTITY_COLUMNS = ["transaction_id", "transaction_leg_id"]`. Add the
|
|
162
|
+
bidirectional identity and row-parity tests from [Parity Testing](parity-testing.md).
|