↑ ↓ 移动 · Enter 打开 · Esc 关闭点按一个结果

架构
skill · design 类 · opus

jobs-architect

设计定时和后台任务:技术选型、跨实例协调和执行保证。

skill · designopus写入 35-jobs.md仅限 docs/use-cases/UC-*/**
§ 它写出的文件

35-jobs.md,逐块说明

一份真实的分文件:bank-app 中的 UC-002-prune-expired-idempotency-keys,为节选。把鼠标移到某一块上或点击它,可以看到 skill 在这里固定了什么。文件的各个章节也是标签:点击即可跳转。

bank-app/docs/use-cases/UC-002-prune-expired-idempotency-keys/35-jobs.md

UC-002 · Jobs

Partial of docs/use-cases/UC-002-prune-expired-idempotency-keys/. Owner: jobs-architect. Contains no code. Form references in .claude/skills/jobs-architect/templates/. Rules applied, never reproduced: @.claude/rules/scheduling.md.
FieldValue
Mother spec00-caso-de-uso.md
Why this partial exists00-caso-de-uso.md § Trigger is a schedule — every 1 hour
Scheduling packageinfrastructure.scheduling (blueprint packages.map) — subpackage idempotency, as 00-caso-de-uso.md § Components named it
Divergences from other partialsnone

Survey: pom.xml (Boot parent 4.1.1) carries no scheduler and no lock library; src/main has no @Scheduled, no @EnableScheduling; no earlier 35-jobs.md; UC-001 20-persistencia.md records no Claim strategy. First job of the project — technology and wiring are NEW here and reused by every later job. No Form B outbox in the project, so no relay and no outbox prune.

No question asked. Each interview axis was settled by the specs: replicas — more than one (00-caso-de-uso.md § Concurrency, the user's requirement); kind — polling pass (hourly, not calendar-anchored); missed run — the next pass is the catch-up; volume — bounded batches, a rerun from zero costs nothing; retry — the next pass; operator control — not requested.

## 1 · Jobs

JobKindCallsTriggerCadence propertyOn/off propertyCoordinationMissed runPer pass
PruneExpiredIdempotencyKeysJobpolling passPruneExpiredIdempotencyKeysUseCase.prune(PruneExpiredIdempotencyKeysCommand)fixedDelay, with initialDelayapp.jobs.prune-idempotency-keys.interval: PT1H, app.jobs.prune-idempotency-keys.initial-delay: PT5M — read only by the @Scheduled placeholdersapp.jobs.prune-idempotency-keys.enabled — default true, false in the test profileevery instance runs the pass; the delete tolerates the overlap (§ 3)skipped — the next pass deletes every row expired by thenbatches of batch-size rows, up to max-batches batches; a failing batch ends the pass, earlier batches stay committed

fixedDelay, not fixedRate and not cron: the work is not calendar-anchored ("every 1 hour", not "at minute 0"), and fixed delay never overlaps itself on one instance (@.claude/rules/scheduling.md § Triggers). Replicas started at different moments stagger their passes naturally; a cron would fire every replica at the same instant against the same rows. No zone — no cron.

initialDelay PT5M: without it every replica prunes at boot, so a rolling deploy runs the pass once per pod within seconds. Harmless (§ 3), but pointless; five minutes keeps it out of startup.

The trigger builds PruneExpiredIdempotencyKeysCommand(batchSize, maxBatches) from its properties record (§ 8), calls the use case through JobRunRecorder, and logs the returned count — INFO when greater than zero, DEBUG when zero (@.claude/rules/scheduling.md § Observability). No other dependency on business code. Shape: templates/ScheduledJob.java.example.

## 2 · Technology

DecisionValueWhy
Scheduler@Scheduled (Spring Framework)One polling pass; no runtime-created schedule, no catch-up need, no per-execution retry, no operator UI. references/tool-decision-matrix.md § 2 row "many instances, work partitioned by row claim": @Scheduled ✅, every other tool overkill or redundant, and it carries zero infrastructure
Coordination toolnone — the delete partitions the work (§ 3)No lock mechanism exists in the project; references/tool-decision-matrix.md § 3 locks an idempotent prune only "when a lock mechanism already exists". The user asked for "safe on more than one replica", not "runs once"
Inherited?no — first job of this projectno scheduler in pom.xml, no earlier 35-jobs.md

Rejected, one line each: ShedLock — one table and two dependencies to stop an idempotent hourly delete from running on a second replica, when the delete itself can skip what another replica holds; adopt it when a job that must run once arrives. Quartz — eleven tables for one static job. db-scheduler — a persistent scheduler for a pass with no catch-up need. Spring Batch — one bounded loop, nothing to restart from. JobRunr — no operator-UI requirement; LGPL not asked. Kubernetes CronJob — no deployment platform recorded, and the job would need a CLI entry the use case does not have.

## 3 · Coordination

FactValueSource
Replicas in productionmore than one — exact count not needed: the mechanism below is correct for any N00-caso-de-uso.md § Concurrency (user's requirement); no earlier 35-jobs.md or Claim strategy
JobMechanismBounds
PruneExpiredIdempotencyKeysJobOverlap-tolerant delete, no lock around the trigger. Every replica runs the pass; each batch deletes only rows no concurrent transaction holds, so two replicas at the same instant split the rows instead of waiting on or failing each other. Required from persistence in § 6none — no lock to bound

Why this meets 10-dominio.md § 3's port contract: two callers never fail each other (no row lock wait, no deadlock — a held row is skipped, not waited on) and never delete a row with expires_at at or after the cutoff (the condition is in every statement). A row one replica skips is either deleted by the replica holding it, or by the next pass.

Cost of N replicas: N short passes per hour, each finding little after the first. Accepted.

## 4 · Execution guarantees

GuaranteeValue
Body idempotentYes — a deleted row cannot be deleted again; a rerun deletes only what expired since
Per-pass boundbatch-size rows per statement (value: 20-persistencia.md), at most max-batches statements per pass. The use case stops early when a statement deletes fewer than batch-size
One failing itemNo per-item processing — the unit is a batch. A failing statement ends the pass; its exception escapes to the scheduler's error handler through JobRunRecorder (recorded as outcome=failure); committed batches stay deleted; the next pass retries
Missed runSkipped. The window is expires_at, persisted state — never "now minus the interval" — so the next pass catches up by construction
Pass longer than the intervalImpossible to overlap on one instance (fixedDelay); bounded by max-batches × statement time — at the § 8 values, far under one hour

## 5 · Observability

MetricJobAlarm it exists for
jobs.execution (timer, tags job=prune-idempotency-keys, outcome)PruneExpiredIdempotencyKeysJoboutcome=failure count above zero for 3 consecutive hours
jobs.last_success.seconds (gauge, tag job=prune-idempotency-keys)PruneExpiredIdempotencyKeysJobolder than 3 h — three missed or failed passes; the table is growing unpruned

The deleted count is in the INFO log line, not a metric: nothing alarms on it, and a counter per pass adds a series nobody reads (@.claude/rules/observability.md § Metrics). Scheduled runs are observed through SchedulingConfig's registrar hook, so each pass carries a trace.

The timer and the gauge come from JobRunRecorder, NEW in this case and shared by every later job (templates/ScheduledJob.java.example).

## 6 · Schema requirements

Read by persistence-architect in its first pass. Requirements, never DDL.

RequirementWhyAddressee
IdempotencyKeyPort.deleteExpired(cutoff, limit) — bounded delete of idempotency_keys rows with expires_at strictly before cutoff, at most limit rows per call, own transaction, skipping rows a concurrent transaction holds instead of waiting on them§ 3: more than one replica runs the pass with no lock; the statement is the coordinationpersistence-architect — the statement, the batch bound's value, and whether ix_idempotency_keys_expires_at serves it are its own
No scheduling tool table§ 2 — @Scheduled alone—

## 7 · Declared dependencies

none — @Scheduled is in spring-context, already on the classpath through spring-boot-starter. Micrometer (MeterRegistry, ObservationRegistry) is already present through the tracing bridge UC-001 declared.

## 8 · Configuration

Properties bound in PruneIdempotencyKeysJobProperties (record, @ConfigurationProperties app.jobs.prune-idempotency-keys, validated at startup: both counts ≥ 1) — except interval and initial-delay, read only by the @Scheduled placeholders and never also bound (@.claude/rules/scheduling.md § Triggers).

PropertyValueOwner of the value
app.jobs.prune-idempotency-keys.enabledtrue; false in src/test/resources/application-test.ymlthis partial
app.jobs.prune-idempotency-keys.intervalPT1Hthis partial — 00-caso-de-uso.md § Trigger
app.jobs.prune-idempotency-keys.initial-delayPT5Mthis partial
app.jobs.prune-idempotency-keys.batch-size—20-persistencia.md — what one statement can hold
app.jobs.prune-idempotency-keys.max-batches100this partial — one pass stays well inside the hour; a backlog larger than batch-size × 100 drains over successive passes
spring.task.scheduling.pool.size1 — written explicitlythis partial — one job in the project; the next job raises it (@.claude/rules/scheduling.md § Triggers: pool size is a decision)
spring.task.scheduling.thread-name-prefixjob-this partial
spring.task.scheduling.shutdown.await-termination / await-termination-periodtrue / PT30Sthis partial — a pass in flight at shutdown finishes its batch

src/test/resources/application-test.yml does not exist yet (the test profile is active in BankAppApplicationTests, ForwardedHeadersIT, CreateCustomerIdempotencyIT): NEW, holding the one enabled: false line.

Full shape in templates/application-jobs.yml.example.

## 9 · Deferred

none

## Design patterns

none — one job, one trigger calling one inbound port; no shared claim-process-mark skeleton across jobs, no symptom on disk (no job exists yet). JobRunRecorder is the exemplar's shared helper, not a catalog pattern.

## Impact on approved use cases

none — UC-001 has no job; the port change is 10-dominio.md's row.

## Implementation order

  1. SchedulingConfig (@EnableScheduling + observation registry) and JobRunRecorder in
  2. PruneIdempotencyKeysJobProperties and PruneExpiredIdempotencyKeysJob in
  3. Properties in application.yml; src/test/resources/application-test.yml with
  4. Tests — 40-testes.md
在 GitHub 上打开完整文件 →
§ 行为

它读什么、写什么、拒绝什么

skill 自己的契约,取自 SKILL.md,保留原文。

Class

design — the territory is skill_classes.design in @.claude/schemas/extensions.json, and ArchHook.java guard enforces it. Writes inside the use case folder and nothing else.

Reads

docs/use-cases/UC-NNN-<slug>/00-use-case.md (mandatory), 10-domain.md (when the trigger is a schedule), 25-messaging.md (when present), every partial's Deferred block, earlier cases' 35-jobs.md and 20-persistence.md § 1, pom.xml, the active blueprint's packages.map, @.claude/rules/scheduling.md, @.claude/rules/architecture-ddd.md, @.claude/rules/observability.md, and references/tool-decision-matrix.md.

Writes

docs/use-cases/UC-NNN-<slug>/35-jobs.md. Nothing else.

Owns

the scheduling technology of the project, each job's trigger and cadence — the relay's poll interval included — coordination across instances and the replica count it rests on, overlap and missed-run policy, on/off properties, job metrics, and the outbox prune job.

Does not decide

the relay's form or its broker side (25-messaging.md), any table, column, claim query, retention window or statement (20-persistence.md), the use case boundary (00-use-case.md), or the tests (40-tests.md). Does not write Java, pom.xml, or application.yml — the executor does, from this partial. Doesn't touch .claude/rules/**.

来源:SKILL.md § Contract(nerviz e4cae27)。

§ guard 与 hook

guard 如何处理它

design 是这个 skill 的类。ArchHook 在 skill 被调用时开启一个阶段,并按该类的领地检查每次写入。右侧文字是 hook 自己的输出。

design 类的 write_allow

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**:迁移和类归执行器
  • docker-compose.yml:另一个类的文件
  • 已批准的用例:其 spec 已冻结

拒绝以退出码 2 结束,消息回到模型。

PreToolUse  Skill(jobs-architect)
  audit call                       exit 0
  guard call                       exit 0
# a skill of another class replaces the phase
# write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md
§ 编排

它在轨道上的位置

每张卡片是 /new-feature 的一步;箭头带着一步交给下一步的分文件。虚线的步骤仅在用例需要时运行。

/new-feature-implement 之后谁读这份分文件

批准之后,/new-feature-implement 让执行器构建代码。spec 按引用整合,与分文件不一致时以 spec 为准;分文件按章节读取,用于细节。

35-jobs.mddocs/use-cases/UC-NNN-…/
按章节读取,用于细节
java-spring-boot-developeragent · executor
写出代码
src/**把一个用例设计成规格,不写代码
← 返回链路