Skip to content

fix(job): key singleton locks by job identity#2042

Open
adityathebe wants to merge 1 commit into
mainfrom
codex/keyed-singleton-locks
Open

fix(job): key singleton locks by job identity#2042
adityathebe wants to merge 1 commit into
mainfrom
codex/keyed-singleton-locks

Conversation

@adityathebe

@adityathebe adityathebe commented Jul 7, 2026

Copy link
Copy Markdown
Member

resolves: flanksource/config-db#2300

Config-db scraper jobs can be launched manually and by schedule through separate duty Job instances.

Singleton previously used a mutex stored on the Job struct, so equal logical jobs could overlap when represented by different objects.

Use an in-process lock registry keyed by Job.ID(), the single computed job identity. Resource-backed jobs use the Name::ResourceType/ResourceID format; jobs without a resource use Name. Replace the old stored alias field with Aliases, which is only used for property lookup and display naming.

GetProperty and GetPropertyInt keep their legacy lookup semantics: jobs.<name>.<property> wins over alias-specific values, and these accessors do not inherit the global jobs.<property> fallback.

Summary by CodeRabbit

  • New Features

    • Jobs can now have multiple aliases, and these aliases are exposed in job details.
    • Job identifiers are now shown consistently while preserving the same API field name.
  • Bug Fixes

    • Job property lookups now work with alias-based names and legacy key formats.
    • Singleton jobs now correctly avoid duplicate runs for the same resource while allowing different resources to run in parallel.
  • Tests

    • Added coverage for job identity, alias handling, property lookup, and singleton execution behavior.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Benchstat (RLS)

Base: 738c98a74d007edbaa31830bf036b3a08a994505
Head: 6fcbb9827ef67636bd7768b8b147dfbf31e083b7

📊 6 minor regression(s) (all within 5% threshold)

Benchmark Base Head Change p-value
RLS/Sample-15000/change_types/With_RLS-4 4.462m 4.678m +4.83% 0.004
RLS/Sample-15000/config_types/Without_RLS-4 4.374m 4.554m +4.11% 0.002
RLS/Sample-15000/config_classes/Without_RLS-4 3.524m 3.621m +2.75% 0.002
RLS/Sample-15000/analyzer_types/With_RLS-4 3.267m 3.341m +2.28% 0.026
RLS/Sample-15000/analysis_types/With_RLS-4 3.397m 3.453m +1.66% 0.015
RLS/Sample-15000/config_detail/With_RLS-4 119.7m 120.4m +0.55% 0.026
✅ 4 improvement(s)
Benchmark Base Head Change p-value
RLS/Sample-15000/catalog_changes/Without_RLS-4 4.620m 4.448m -3.72% 0.002
RLS/Sample-15000/config_summary/Without_RLS-4 94.46m 92.67m -1.89% 0.009
RLS/Sample-15000/config_changes/Without_RLS-4 4.472m 4.399m -1.64% 0.004
RLS/Sample-15000/catalog_changes/With_RLS-4 126.8m 125.5m -1.04% 0.002
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 9V74 80-Core Processor                
                                               │ bench-base.txt │           bench-head.txt           │
                                               │     sec/op     │    sec/op     vs base              │
RLS/Sample-15000/catalog_changes/Without_RLS-4      4.620m ± 1%   4.448m ±  2%  -3.72% (p=0.002 n=6)
RLS/Sample-15000/catalog_changes/With_RLS-4         126.8m ± 1%   125.5m ±  0%  -1.04% (p=0.002 n=6)
RLS/Sample-15000/config_changes/Without_RLS-4       4.472m ± 1%   4.399m ±  1%  -1.64% (p=0.004 n=6)
RLS/Sample-15000/config_changes/With_RLS-4          124.9m ± 2%   124.5m ±  2%       ~ (p=0.310 n=6)
RLS/Sample-15000/config_detail/Without_RLS-4        4.346m ± 6%   4.295m ±  2%       ~ (p=0.394 n=6)
RLS/Sample-15000/config_detail/With_RLS-4           119.7m ± 0%   120.4m ±  1%  +0.55% (p=0.026 n=6)
RLS/Sample-15000/config_names/Without_RLS-4         13.19m ± 8%   13.43m ±  7%       ~ (p=0.310 n=6)
RLS/Sample-15000/config_names/With_RLS-4            120.6m ± 1%   121.0m ±  1%       ~ (p=0.180 n=6)
RLS/Sample-15000/config_summary/Without_RLS-4       94.46m ± 2%   92.67m ±  2%  -1.89% (p=0.009 n=6)
RLS/Sample-15000/config_summary/With_RLS-4          612.4m ± 1%   605.9m ±  3%       ~ (p=0.093 n=6)
RLS/Sample-15000/configs/Without_RLS-4              7.408m ± 2%   7.328m ±  2%       ~ (p=0.180 n=6)
RLS/Sample-15000/configs/With_RLS-4                 119.9m ± 0%   119.9m ±  0%       ~ (p=0.818 n=6)
RLS/Sample-15000/analysis_types/Without_RLS-4       3.413m ± 1%   3.394m ±  1%       ~ (p=0.240 n=6)
RLS/Sample-15000/analysis_types/With_RLS-4          3.397m ± 1%   3.453m ±  3%  +1.66% (p=0.015 n=6)
RLS/Sample-15000/analyzer_types/Without_RLS-4       3.249m ± 1%   3.259m ±  2%       ~ (p=0.310 n=6)
RLS/Sample-15000/analyzer_types/With_RLS-4          3.267m ± 1%   3.341m ±  5%  +2.28% (p=0.026 n=6)
RLS/Sample-15000/change_types/Without_RLS-4         4.574m ± 3%   4.566m ±  9%       ~ (p=0.699 n=6)
RLS/Sample-15000/change_types/With_RLS-4            4.462m ± 2%   4.678m ± 10%  +4.83% (p=0.004 n=6)
RLS/Sample-15000/config_classes/Without_RLS-4       3.524m ± 1%   3.621m ±  6%  +2.75% (p=0.002 n=6)
RLS/Sample-15000/config_classes/With_RLS-4          120.6m ± 0%   120.6m ±  0%       ~ (p=0.589 n=6)
RLS/Sample-15000/config_types/Without_RLS-4         4.374m ± 2%   4.554m ± 14%  +4.11% (p=0.002 n=6)
RLS/Sample-15000/config_types/With_RLS-4            119.8m ± 2%   121.1m ±  1%       ~ (p=0.310 n=6)
geomean                                             18.53m        18.58m        +0.30%

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Benchstat (Other)

Base: 738c98a74d007edbaa31830bf036b3a08a994505
Head: 6fcbb9827ef67636bd7768b8b147dfbf31e083b7

📊 1 minor regression(s) (all within 5% threshold)

Benchmark Base Head Change p-value
ResourceSelectorConfigs/name_and_type-4 209.8µ 214.6µ +2.29% 0.041
✅ 1 improvement(s)
Benchmark Base Head Change p-value
ResourceSelectorQueryBuild/tags-4 16.27µ 16.09µ -1.08% 0.045
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 9V74 80-Core Processor                
                                                       │ bench-base.txt │           bench-head.txt            │
                                                       │     sec/op     │    sec/op      vs base              │
InsertionForRowsWithAliases/external_users.aliases-4       522.2µ ±  8%   523.4µ ±   2%       ~ (p=0.937 n=6)
InsertionForRowsWithAliases/config_items.external_id-4     1.038m ± 10%   1.030m ±  11%       ~ (p=1.000 n=6)
InsertionOfConfigsWithProperties-4                         3.378m ±  0%   3.363m ±   1%       ~ (p=0.394 n=6)
UpdateOfConfigsWithProperties-4                            7.119m ±  1%   7.109m ±   4%       ~ (p=0.699 n=6)
ResourceSelectorConfigs/name-4                             200.1µ ±  3%   196.5µ ±   4%       ~ (p=0.132 n=6)
ResourceSelectorConfigs/name_and_type-4                    209.8µ ±  3%   214.6µ ±   2%  +2.29% (p=0.041 n=6)
ResourceSelectorConfigs/tags-4                             59.33m ± 47%   32.45m ± 170%       ~ (p=0.589 n=6)
ResourceSelectorQueryBuild/name-4                          40.46µ ±  1%   41.64µ ±   4%       ~ (p=0.132 n=6)
ResourceSelectorQueryBuild/name_and_type-4                 59.40µ ±  2%   60.06µ ±   3%       ~ (p=0.589 n=6)
ResourceSelectorQueryBuild/tags-4                          16.27µ ±  1%   16.09µ ±   1%  -1.08% (p=0.045 n=6)
geomean                                                    513.3µ         484.3µ         -5.65%

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The Job struct's single ID field is replaced with a computed ID() method and an Aliases slice. A shared keyed singleton lock registry replaces per-job mutex locking. Property lookup and naming become alias-aware. The cron API exposes JobID and Aliases. New tests validate identity, property lookup, and singleton concurrency behavior.

Changes

Job identity, singleton locking, and property lookup refactor

Layer / File(s) Summary
Job identity computation
job/job.go
Removes the exported ID field and per-job mutex, adds Aliases []string, and introduces a computed ID() method along with SetAliases(); GetContext() reports the computed id and aliases.
Shared singleton lock registry
job/singleton.go, job/job.go
Adds singletonLocks, a keyed lock registry with TryLock/idempotent unlock via sync.Once; Run() uses singletonLocks.TryLock(j.ID()) for singleton skipping and j.ID() for trace span naming.
Alias-aware property lookup and naming
job/job.go
Property key lookup (getPropertyNames, GetProperty, GetPropertyInt) now builds keys from all distinct aliases via new aliases()/primaryAlias() helpers; init(), Label(), and GetResourcedName() use primaryAlias() instead of the removed ID.
Cron API exposure of JobID and Aliases
job/controllers.go
JobCronEntry.ID renamed to JobID (JSON key "id" unchanged) and a new Aliases field added; CronDetailsHandler populates both from j.ID() and j.Aliases.
Tests for identity, properties, and concurrency
job/job_test.go, tests/job_test.go
New tests validate ID() behavior, backward-compatible property lookup with legacy/alias keys, and singleton concurrency: same-ResourceID jobs skip concurrent runs while different-ResourceID jobs run concurrently and succeed.

Sequence Diagram(s)

sequenceDiagram
  participant JobA as Job (ResourceID X)
  participant JobB as Job (ResourceID X)
  participant Registry as singletonLocks

  JobA->>Registry: TryLock(ID())
  Registry-->>JobA: locked=true
  JobA->>JobA: execute (running)
  JobB->>Registry: TryLock(ID())
  Registry-->>JobB: locked=false (already running)
  JobB->>JobB: mark StatusSkipped
  JobA->>Registry: unlock()
  JobA->>JobA: mark StatusSuccess
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: singleton locks now use job identity.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/keyed-singleton-locks
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch codex/keyed-singleton-locks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@adityathebe adityathebe force-pushed the codex/keyed-singleton-locks branch from 049ac24 to 54edf9d Compare July 7, 2026 12:08
@adityathebe adityathebe marked this pull request as ready for review July 7, 2026 12:09
@adityathebe adityathebe marked this pull request as draft July 7, 2026 12:22
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Gavel results

Gavel exited with code .

View full results

@adityathebe adityathebe force-pushed the codex/keyed-singleton-locks branch 2 times, most recently from 3c6b0b6 to aea7ce6 Compare July 7, 2026 13:44
Manual config-db scraper runs create a fresh duty job while scheduled runs use another instance. The old singleton mutex lived on the Job struct, so jobs with the same logical scraper identity could overlap despite Singleton being enabled.

Use Job.ID() as the single computed in-process singleton identity, derived as Name::ResourceType/ResourceID when a resource is present. Replace the old stored alias field with Aliases, which is only used for property lookup and display names.

Keep GetProperty and GetPropertyInt backward compatible: jobs.<name>.<property> continues to win over alias-specific values, and these accessors do not inherit the global jobs.<property> fallback.

Addresses flanksource/config-db#2300
@adityathebe adityathebe force-pushed the codex/keyed-singleton-locks branch from aea7ce6 to 6fcbb98 Compare July 7, 2026 13:49
@adityathebe adityathebe marked this pull request as ready for review July 7, 2026 13:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@job/job.go`:
- Around line 435-438: The deferred semaphore release log in the job semaphore
handling uses Infof with a prebuilt message string, which can misinterpret
percent signs in j.ID() as format tokens. Update the deferred closure around the
semaphore Release in job/job.go to log the message as data instead of a format
string by using the logger with a literal format and the assembled msg variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35a4e93c-d801-4f21-ba59-8c442ef21636

📥 Commits

Reviewing files that changed from the base of the PR and between 738c98a and 6fcbb98.

📒 Files selected for processing (5)
  • job/controllers.go
  • job/job.go
  • job/job_test.go
  • job/singleton.go
  • tests/job_test.go

Comment thread job/job.go
@adityathebe adityathebe requested a review from moshloop July 7, 2026 14:00
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.

Job singleton locking is scoped to Job struct instances instead of logical job identity

1 participant