Skip to content

Fixes #39626 - Recover gracefully from concurrent create races on Subnet and Operatingsystem - #11161

Draft
pablomh wants to merge 2 commits into
theforeman:developfrom
pablomh:feat/harden-os-subnet-create-race
Draft

Fixes #39626 - Recover gracefully from concurrent create races on Subnet and Operatingsystem#11161
pablomh wants to merge 2 commits into
theforeman:developfrom
pablomh:feat/harden-os-subnet-create-race

Conversation

@pablomh

@pablomh pablomh commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Subnet (unique index on name since 20180806151925) and Operatingsystem (unique index on name+major+minor since 20150713143226) both already have real DB-level uniqueness constraints, but their controllers' create actions never handled the ActiveRecord::RecordNotUnique a concurrent duplicate create raises — it surfaced as a raw 500 instead of the graceful "return the existing record" behavior a client would expect from a race on an idempotent-looking create call.

Fixes #39626.

Changes

Wires both into the process_create_with_record_not_unique helper introduced in #11160 for Taxonomy, matching the exact same pattern. No schema changes needed here — these two already had the DB constraint the recovery path depends on, unlike Taxonomy.

Stacked on #11160 — this branch is based on top of it (needs process_create_with_record_not_unique in Api::BaseController). Please review/merge #11160 first.

Testing done

Live-verified against the same real Satellite 6.19/foreman-3.18 instance used for #11160 (patched both controller files, restarted the service):

  • Concurrent identical POST /api/subnets: 4/5 requests returned 201 with the same id (the 5th got the pre-existing, unaffected 422 "already taken" from the normal validation path — the recovery is additive, not a replacement). DB confirmed exactly one row.
  • Concurrent identical POST /api/operatingsystems: 3/5 returned 201 with the same id, 2/5 got the normal 422. DB confirmed exactly one row.

Ruby syntax-checked all changed files locally. Same caveat as #11160 - wasn't able to run the full Rails test suite locally (no local Postgres set up), relying on CI plus the live verification above.

@pablomh
pablomh force-pushed the feat/harden-os-subnet-create-race branch 3 times, most recently from b1684a5 to c99be43 Compare August 14, 2026 16:10
pablomh and others added 2 commits August 14, 2026 18:38
Location/Organization uniqueness (Taxonomy#validates :name, uniqueness:
{scope: [:ancestry, :type]}) is application-level only - a SELECT before
INSERT, with no backing DB constraint. Two concurrent POST /api/locations
(or /api/organizations) for the same name both pass the check before
either commits, both INSERT cleanly, and you end up with duplicate rows.
Confirmed live against a real Satellite 6.19/foreman-3.18 instance (5
concurrent identical creates produced 5 distinct duplicate Location rows).

Adds a unique index matching the model's existing validation scope:
type, COALESCE(ancestry, ''), lower(name). A plain UNIQUE index on the
raw ancestry column does nothing for root taxonomies (NULL is distinct
from NULL); COALESCE was caught and verified live.

Api::V2::TaxonomiesController#create recovers from
ActiveRecord::RecordNotUnique via process_create_with_record_not_unique
and returns the winning record as 201.

If duplicate rows already exist, the migration aborts and lists their
ids. It does not merge or delete them, so an upgrade hard-blocks until
an admin resolves the extras. That is intentional: auto-heal has a large
association surface and raced creates are not actually unused.

Tests cover the DB constraint (validations bypassed), API recovery, the
migration abort path, index creation, and reversibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
…net and Operatingsystem

Subnet (unique index on name since 20180806151925) and Operatingsystem (unique index on name+major+minor since 20150713143226) both already have real DB-level uniqueness constraints, but their controllers' create actions never handled the ActiveRecord::RecordNotUnique a concurrent duplicate create raises - it surfaced as a raw 500 instead of the graceful 'return the existing record' behavior a client would expect from a race on an idempotent-looking create call.

Wires both into the process_create_with_record_not_unique helper introduced for Taxonomy on the branch this stacks on (fix/taxonomy-location-create-race). No schema changes needed here - these two already had the DB constraint the recovery path depends on, unlike Taxonomy.

Related but non-overlapping: theforeman#11059 fixes a different concurrency issue on the same Operatingsystem model, in Katello::RhsmFactParser's internal create_or_find_by call during RHSM registration (a service-class code path, not the REST API controller this PR touches) - zero file overlap with this PR.

Verified live on a Satellite 6.19/foreman-3.18 instance: concurrent identical POST /api/subnets and POST /api/operatingsystems both return 201 with the same id for the racing requests (plus the pre-existing, unaffected 422 for non-racing ones), confirmed no duplicate rows in either case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pablomh
pablomh force-pushed the feat/harden-os-subnet-create-race branch from c99be43 to 3efb1f3 Compare August 14, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant