Skip to content

fix(ingestion): stop silent jira sync hang via lightweight substream parent - #1283

Merged
mitasovr merged 2 commits into
constructorfabric:mainfrom
mitasovr:claude/youthful-rubin-ddefef
Jun 11, 2026
Merged

fix(ingestion): stop silent jira sync hang via lightweight substream parent#1283
mitasovr merged 2 commits into
constructorfabric:mainfrom
mitasovr:claude/youthful-rubin-ddefef

Conversation

@mitasovr

@mitasovr mitasovr commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

jira syncs on large instances hang silently: the Airbyte job stays running with 0 records committed for hours, Argo poll times out (workflow Failed), and the orphaned replication pod keeps spinning. On the virtuozzo tenant this meant bronze_jira never received a single completed sync.

Root cause

The low-code CDK auto-caches a SubstreamPartitionRouter parent's HTTP responses in a SQLite requests-cache so child streams can reuse them. The parent of jira_issue_history / jira_comments / jira_worklogs was jira_issue, which requests fields=*all + expand=names. On an instance with ~1000 fields (44 system + 959 custom, mostly (migrated) duplicates), each /search/jql response is ~2 MB. The cache ballooned to ~226 MB after ~108 responses and the read stalled before emitting anything — diagnosed with an isolated read --debug run of the bare source (no destination/orchestrator), which reproduced the prod hang exactly.

Fix

Mirrors the official source-jira pattern (minimal-fields parent for partition enumeration, *all only on the terminal emitter):

  • New lightweight stream jira_issue_keys: same /search/jql + JQL window, but fields=updated only (id/key arrive top-level for free, no expand).
  • jira_issue_history, jira_comments, jira_worklogs repointed to it as their parent (#/streams/4#/streams/13).
  • jira_issue stays the full-record emitter but is no longer a cached substream parent.

Since reconcile (ADR-0015) auto-selects every discovered stream, jira_issue_keys lands as a real (tiny: key + updated per issue) bronze table, so it:

  • carries the standard identity stamp tenant_id / source_id / unique_key ({tenant}-{source}-{key}),
  • hoists the updated cursor field to the top level (the search response nests it as fields.updated; jira_issue does the same hoist),
  • is added to the promote_bronze_to_rmt list (jira__bronze_promoted.sql) so RMT dedup caps its growth. The macro skips missing tables, so ordering vs. first sync is safe.

Verification (isolated read runs against a live large instance)

before after
parent SQLite cache 226 MB, frozen at 108 responses ~4 KB
parent enumeration stuck, 0 emitted completes (9994 keys)
jira_issue_history never starts 4210 records in a 2-day window, per-partition state advances
jira_issue_keys standalone 1579 records, unique_key=virtuozzo-jira-main-VSTOR-…, exit 0

No schema changes to existing bronze tables: children emit byte-identical records (id_readable comes from record['key'] in both old and new parent); dbt staging/silver untouched apart from the added promotion line.

Note: confluence has the same architecture (wiki_pages parents 3 substreams) and will get the same treatment in a follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a lightweight Jira issue-key enumeration stream to improve cursor progress and incremental reads.
  • Refactor
    • Substream partitioning for issue history, comments, and worklogs now uses the lightweight parent to reduce read stalls and cache growth.
  • Maintenance
    • Bronze promotion updated to include the new issue-keys stream to help deduplicate and cap table growth.
  • Documentation
    • Added guidance and troubleshooting on silent stalls, cursor hoisting, and validation rules for connector authors.

Also included

docs(skills): the /connector skill (cypilot/.core/skills/connector/) now encodes these lessons — forbidden heavy substream parents + the lightweight key-parent / inline-parent patterns, identity stamp & RMT promotion required on every top-level stream (reconcile auto-selects all), nested-cursor hoist rule, silent-stall diagnosis for read, and "never trust Airbyte job status — verify recordsCommitted / bronze freshness" ops guidance.

…parent

jira syncs on large instances hung silently: the job stayed "running" with
0 records committed for hours while Argo poll timed out. Root cause: the
low-code CDK auto-caches a SubstreamPartitionRouter parent's HTTP responses
in a SQLite requests-cache, and the parent was jira_issue with
fields=*all + expand=names. On an instance with ~1000 fields (44 system +
959 custom) each /search/jql response is ~2 MB, so the cache ballooned to
~226 MB after ~108 responses and the read stalled before emitting anything.

Fix: add a lightweight jira_issue_keys stream (fields=updated only; id/key
arrive top-level for free) and repoint jira_issue_history, jira_comments
and jira_worklogs to it as their parent. jira_issue stays the full-record
emitter but is no longer a cached substream parent. Verified with isolated
`read --debug` runs: parent cache 226 MB -> ~4 KB, parent enumeration
completes (9994 keys), children emit steadily (4210 history records in one
2-day window) with per-partition state advancing.

Since reconcile (ADR-0015) auto-selects every discovered stream,
jira_issue_keys lands as a real bronze table: it carries the standard
tenant_id/source_id/unique_key stamp ({tenant}-{source}-{key}), hoists the
`updated` cursor field to the top level like jira_issue does, and is added
to the promote_bronze_to_rmt list so RMT dedup caps its growth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a lightweight jira_issue_keys parent stream that emits only cursor/identity fields and hoists updated, re-points jira_issue_history, jira_comments, and jira_worklogs to use it for partitioning, promotes the new bronze table in dbt, and updates connector runbooks/workflows with cache and cursor-hoisting guidance.

Changes

Jira Issue Keys Parent Stream and Substream Re-parenting

Layer / File(s) Summary
New jira_issue_keys parent stream definition
src/ingestion/connectors/task-tracking/jira/connector.yaml
A new DeclarativeStream named jira_issue_keys is added with a DatetimeBasedCursor on updated, a JQL requester returning only fields.updated, transformations that hoist updated, stamp tenant/source/unique_key/id_readable/jira_id and collected_at, and an inline minimal schema.
Substream partition router re-parenting
src/ingestion/connectors/task-tracking/jira/connector.yaml
jira_issue_history, jira_comments, and jira_worklogs have their SubstreamPartitionRouter parent stream reference changed from #/streams/4 to #/streams/13, altering the upstream source for per-issue partition keys.
dbt promotion for new jira_issue_keys stream
src/ingestion/connectors/task-tracking/jira/dbt/jira__bronze_promoted.sql
bronze_jira.jira_issue_keys is added to promote_bronze_to_rmt(..., order_by='unique_key') in the promoted model.
Connector runbook and workflow docs
cypilot/.core/skills/connector/*
Added guidance and validation rules warning against heavy SubstreamPartitionRouter parents, advising cursor hoisting for DatetimeBasedCursor, adding stall-diagnosis steps, and requiring identity stamping and bronze promotion lines in manifests.

Sequence Diagram

sequenceDiagram
  participant JiraAPI as Jira API
  participant IssueKeys as jira_issue_keys
  participant Substreams as jira_issue_history/comments/worklogs
  JiraAPI->>IssueKeys: JQL query (fields: updated)
  IssueKeys->>IssueKeys: Hoist `updated`, stamp identity, emit `unique_key`
  IssueKeys->>Substreams: Emit partitions (id_readable, unique_key)
  Substreams->>JiraAPI: Fetch per-issue payloads using partitions
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 I nibble keys light and neat,
hoist a timestamp, tidy the beat,
three old trails now follow my song,
small and steady—fast and strong. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main fix: introducing a lightweight substream parent to resolve silent Jira sync hangs caused by SQLite cache ballooning.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Capture the operational and authoring lessons from the jira silent-hang
fix so the next connector doesn't repeat them:

- create.md: forbid heavy (fields=*all) SubstreamPartitionRouter parents
  (CDK auto-caches parent responses in SQLite; 226 MB cache -> permanent
  silent stall), document the lightweight key-parent pattern
  (jira_issue_keys) and the inline-parent alternative (_scrum_boards);
  require the identity stamp + RMT promotion on every top-level stream
  since reconcile (ADR-0015) auto-selects all discovered streams; require
  hoisting nested cursor fields (fields.updated) to the top level. Two new
  runtime-landmine rows + a cache-size acceptance criterion.
- validate.md: checklist items for the above (no heavy parents, top-level
  cursor field, stamped + promoted helper streams).
- test.md: stall diagnosis for silent reads — SQLite cache growth check,
  CPU-time vs frozen counter, CLOSE_WAIT-after-idle false positive.
- SKILL.md: never trust Airbyte job status — verify recordsCommitted and
  bronze freshness (green-but-empty and forever-running failure modes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cypilot/.core/skills/connector/SKILL.md (1)

57-61: ⚡ Quick win

Consider adding a brief example for checking aggregatedStats.recordsCommitted.

The guidance correctly identifies the real health signals (aggregatedStats.recordsCommitted and bronze freshness), but operators may not know how to retrieve aggregatedStats.recordsCommitted from the jobs API. Consider adding a one-line example or cross-referencing the Airbyte API endpoint (e.g., GET /api/v1/jobs/get.attempts[].stats.recordsCommitted) to make the guidance immediately actionable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cypilot/.core/skills/connector/SKILL.md` around lines 57 - 61, Add a one-line
example showing how to read aggregatedStats.recordsCommitted from the Airbyte
jobs API: reference the GET /api/v1/jobs/get endpoint and indicate the path
attempts[].stats.recordsCommitted (or jobs[].attempts[].stats.recordsCommitted /
.aggregatedStats.recordsCommitted as applicable) so operators can immediately
locate the metric; include this short example sentence near the paragraph that
recommends checking aggregatedStats.recordsCommitted and bronze freshness.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cypilot/.core/skills/connector/SKILL.md`:
- Around line 57-61: Add a one-line example showing how to read
aggregatedStats.recordsCommitted from the Airbyte jobs API: reference the GET
/api/v1/jobs/get endpoint and indicate the path
attempts[].stats.recordsCommitted (or jobs[].attempts[].stats.recordsCommitted /
.aggregatedStats.recordsCommitted as applicable) so operators can immediately
locate the metric; include this short example sentence near the paragraph that
recommends checking aggregatedStats.recordsCommitted and bronze freshness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 771fea94-3d49-4296-b465-561a0b1778a5

📥 Commits

Reviewing files that changed from the base of the PR and between d5388b0 and 1cd7878.

📒 Files selected for processing (4)
  • cypilot/.core/skills/connector/SKILL.md
  • cypilot/.core/skills/connector/workflows/create.md
  • cypilot/.core/skills/connector/workflows/test.md
  • cypilot/.core/skills/connector/workflows/validate.md
✅ Files skipped from review due to trivial changes (2)
  • cypilot/.core/skills/connector/workflows/validate.md
  • cypilot/.core/skills/connector/workflows/create.md

@mitasovr
mitasovr merged commit 311780d into constructorfabric:main Jun 11, 2026
10 of 12 checks passed
mitasovr added a commit that referenced this pull request Jun 12, 2026
…#1308 (#1310)

Reconcile republishes a nocode declarative manifest only on descriptor
version drift (descriptor version vs the version stored on the Airbyte
definition; equal -> noop per ADR-0015). #1308 changed connector.yaml
(default_concurrency 1 -> 4) without bumping descriptor versions, so
reconcile on the deployed 0.1.59 correctly no-op'd and the deadlock fix
never reached Airbyte: the active jira manifest (v3) and confluence
manifest (v2) still carry default_concurrency: 1. (#1283 only got
published because an unrelated image-ref bump had already moved jira's
descriptor to 1.2.0.)

- jira:       1.2.0 -> 1.2.1
- confluence: 1.1.0 -> 1.1.1

Also encode the lesson in the /connector skill (deploy prerequisites +
validate descriptor checklist): bump descriptor.yaml version in the same
PR as any connector.yaml change, or the manifest silently never ships.

Co-authored-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mitasovr added a commit that referenced this pull request Jun 15, 2026
…list (#1316)

Replaces the manually-maintained jira_project_keys secret field with
runtime project discovery. Supersedes #941 (idea and per-project JQL by
mozhaev-dev, cyberfabric/cyber-insight#616), reworked against the current
manifest:

- New inline-only parent `jira_project_discovery` under `definitions`
  (never visible to discover, so reconcile cannot select it as a bronze
  table): GET /rest/api/3/project/search?expand=insight enumerates every
  project the API token can see.
- BOTH jira_issue and jira_issue_keys (the lightweight parent of
  jira_issue_history/comments/worklogs from #1283) are partitioned per
  project with JQL `project = "<KEY>"`. #941 predates jira_issue_keys and
  would have silently broken all three substreams by removing the config
  their JQL still referenced.
- Incremental discovery gate: client-side cursor on the hoisted
  insight.lastIssueUpdateTime — first sync emits all projects, later syncs
  only projects whose issues changed. Verified live: first read enumerated
  211 projects (25 with fresh issues, 2478 records — the 4-project
  allowlist would have missed 9 of them), resume read touched only 3/211
  partitions. Projects without issues default to epoch and stay filtered
  until their first issue.
- Timezone tail fix done safely: lookback_window PT1H -> PT14H on the
  issue scans. #941 instead pushed end_datetime 14h into the future, which
  advances the cursor past the actual query time and permanently skips
  records updated in between.
- jira_project_keys removed from spec, descriptor required_fields, secret
  example and README.
- descriptor 1.2.1 -> 2.0.0 (major): per-project partitioning resets
  incremental state; reconcile dispatches a full refresh on major bumps,
  which is the intended migration.

Known costs, documented in-manifest: the first sync after rollout is a
full re-sync of every visible project since jira_start_date; the
client-side gate compares strictly against the cursor (lookback does not
widen it on resume), so a lagging insight aggregate delays that project
until its next update.

validate (CDK runtime): manifest valid. validate-strict: 14 pre-existing
errors on main (jira is the known whole-object-$ref anti-template);
unchanged by this diff.

Co-authored-by: Roman Mitasov <Roman.Mitasov@constructor.tech>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants