New to Kestra?
Use blueprints to kickstart your first workflows.
Detect DB2 tables stuck in check-pending state and rejecting reads, then clear them with SET INTEGRITY and prove they answer queries again.
A DB2 table in check-pending state is an outage, not a slow query. After a LOAD or after
a constraint is added with SET INTEGRITY ... OFF, SYSCAT.TABLES.STATUS goes to 'C' and from
that moment the table rejects every SELECT and every INSERT, UPDATE or DELETE with
SQL0668N reason code 1. The application reading it is down. Nothing times out, nothing degrades,
the table simply refuses. The fix is one statement, SET INTEGRITY FOR <table> IMMEDIATE CHECKED,
and the table stays broken until somebody runs it.
This blueprint reads SYSCAT.TABLES.STATUS for a schema, orders the pending tables parents first
using SYSCAT.REFERENCES, then behind a dry run and a Pause runs SET INTEGRITY one table at a
time. It reads STATUS again and fails the run if any target is still not 'N', then counts rows
through each cleared table so the proof is a working query and not only a catalog flag.
This blueprint was created by zkasuran.
| Verdict | Rule | Fix |
|---|---|---|
ACTION |
At least one table in the schema has STATUS = 'C' |
SET INTEGRITY ... IMMEDIATE CHECKED, parents before children |
BLOCKED |
Divert mode is on but a target has no exception table. Alternatively the only tables found are inoperative (STATUS = 'X') |
Create the exception tables. An inoperative table has to be dropped then recreated. The guard reports and stops |
OK |
Every table in the schema reads STATUS = 'N' |
None |
SET INTEGRITY runs only when auto_remediate is true, dry_run is
false, the verdict is ACTION and a human approves the Pause. An unanswered resume leaves
approved null, which the gate reads as a refusal.SYSCAT.REFERENCES and the loop runs lowest depth
first with concurrencyLimit: 1.max_tables caps one run. SET INTEGRITY reads every row of the table to validate it, so a
wide run is a long scan. Truncation takes the lowest depth first, which means parents, so the
children are still checkable on the next run.fail mode the statement fails and the
table stays pending, which the re-verify step turns into a failed run. In
divert_to_exception_tables mode each bad row is moved to that table's exception table and the
final step counts those rows so they are reported.STATUS = 'X' (inoperative) is reported and never touched. SET INTEGRITY cannot
fix it.detect (io.kestra.plugin.jdbc.db2.Query, read-only) selects every table in the schema whose
STATUS is not 'N'. One recursive query over SYSCAT.REFERENCES gives each pending table its
dependency depth and the pending parents that must be cleared first. It also reports whether an
exception table exists for each target.assess (io.kestra.plugin.core.output.OutputValues) splits check-pending from inoperative,
sorts parents first, applies max_tables, builds one SET INTEGRITY statement per target and
the row count query used later as proof.summary logs the report. route (io.kestra.plugin.core.flow.If) stops with a log when every
table reads 'N'.alert posts to Slack and planned prints the exact statements a remediation run
would execute, so a dry run is a readable plan.remediate_gate opens only on ACTION with auto_remediate true and dry_run false.
hold (io.kestra.plugin.core.flow.Pause) waits for approval.remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs the statements in
order, one table per iteration.reverify reads SYSCAT.TABLES.STATUS again. verify_result fails the run when a target is
still not 'N', naming it and why it can stay stuck.prove_reads counts rows through every cleared table, which only works if the table really
accepts reads again, then remediated logs the counts. In divert mode the exception tables
are counted in the same query.errors posts a Slack alert when the flow fails, for example when the database is unreachable.STATUS back to
'N' and a real row count through the table.LOAD can leave a table unreadable, where the next working day starts with
an application that cannot query a table nobody changed.The detection needs the catalog. The fix needs a dependency order. The order needs a loop that
cannot run in parallel. All of it needs an approval, because the fix is a full table scan.
Kestra holds all four in one flow: the If gates on auto_remediate plus dry_run, the Pause
holds for a human, the Loop with concurrencyLimit: 1 keeps parents before children. A re-verify that fails
the run means a partial recovery can never be reported as a success.
On Kestra 2.0.5 OSS against DB2 Community Edition 12.1.5 (icr.io/db2_community/db2), database
testdb, with a local HTTP sink standing in for Slack. Check-pending was created for real with
SET INTEGRITY FOR <table> OFF and confirmed with the DB2 CLI: SYSCAT.TABLES.STATUS read 'C'
while SELECT COUNT(*) on the same table returned
SQL0668N Operation not allowed for reason code "1", SQLSTATE 57007. After the fix the same
SELECT returned 5 rows. A second table in the schema stayed at STATUS = 'N' and readable the
whole time.
| Run | Inputs | Result |
|---|---|---|
| 1 | one table pending, defaults | Verdict ACTION. DB2INST1.ORDERS reported at depth 0, alert sent, no change |
| 2 | auto_remediate: true, dry_run: false, pause declined |
No change. Schema still reporting 1 check-pending table |
| 3 | same, pause approved | SET INTEGRITY ran. STATUS 'C' to 'N', 5 rows counted through the table. CLI agreed |
| 4 | healthy schema, defaults | Verdict OK. 0 tables reported |
| 5 | parent and child both pending, approved | Ordered PARENT_T depth 0 then CHILD_T depth 1, both cleared, 2 rows each |
| 6 | max_tables: 1 with 2 pending |
Only PARENT_T in scope, child left for the next run |
| 7 | a row breaking a CHECK constraint, fail mode, approved |
Statement failed, table stayed 'C', the run FAILED at the re-verify |
| 8 | divert_to_exception_tables with no exception table, armed |
Verdict BLOCKED. Nothing ran |
| 9 | divert_to_exception_tables with the exception table in place, approved |
2 violating rows moved to BADROWS_EXCEPTIONS, base table back to 'N' with 2 readable rows, diverted count reported |
| 10 | unreachable port | detect failed, the errors block posted to Slack, the execution is FAILED |
Ordering is not a theory. Checking the child first by hand returned
SQL3608N Cannot check a dependent table ... while the parent table ... is in the Set Integrity Pending state, SQLSTATE 428A8, which is what run 5 avoids by sorting on dependency depth.
SYSCAT.TABLES and SYSCAT.REFERENCES, plus CONTROL or the
LOAD authority needed to run SET INTEGRITY on the target tables for remediation. The instance
owner has all of these.divert_to_exception_tables, an exception table per target. Build it with
CREATE TABLE <base>_EXCEPTIONS LIKE <base> then
ALTER TABLE <base>_EXCEPTIONS ADD COLUMN TS TIMESTAMP ADD COLUMN MSG CLOB(32K). A hand written
table with the same column types is not enough: DB2 rejected one with
SQL3604N ... Reason code: "2", while the LIKE form was accepted and returned SQL3602W with
the bad rows moved.docker run -d --name db2 --privileged -p 50000:50000 -e LICENSE=accept -e DB2INST1_PASSWORD=... -e DBNAME=testdb icr.io/db2_community/db2:latest;SET INTEGRITY FOR <table> OFF to put it into check-pending state.DB2_PASSWORD: the database password, used as the default for the jdbc_password input.SLACK_WEBHOOK_URL: used by alert and the errors block.SECRET_.| Input | Default | Purpose |
|---|---|---|
jdbc_url |
jdbc:db2://localhost:50000/testdb |
Database to read |
jdbc_username |
db2inst1 |
Database user |
jdbc_password |
{{ secret('DB2_PASSWORD') }} |
Database password |
table_schema |
DB2INST1 |
Schema whose tables are scanned |
violation_handling |
fail |
fail stops on a bad row. divert_to_exception_tables moves bad rows to exception tables |
exception_table_suffix |
_EXCEPTIONS |
Names the exception table in divert mode |
max_tables |
10 |
Tables checked per run, lowest dependency depth first |
auto_remediate |
false |
Allow SET INTEGRITY to run |
dry_run |
true |
Must be false for anything to change |
DB2_PASSWORD and SLACK_WEBHOOK_URL secrets.jdbc_url, jdbc_username and table_schema, then run it with the
defaults. The log names any table that is refusing reads.planned log to see the exact statements and the order they would run in.auto_remediate: true and dry_run: false, then approve the
pause. The last log carries the row count through each recovered table.every_15_minutes trigger, which always runs in safe mode.IMMEDIATE CHECKED for IMMEDIATE UNCHECKED in a controlled window when the data is known
good and the full scan is not affordable. That skips validation, so it belongs behind its own gate.FULL ACCESS handling for tables that come out of check-pending into no-access state after a
partial check.