Skip to content

fix(plugins/bitwarden_item): prevent duplicate items from parallel runs - #389

Merged
NavidSassan merged 2 commits into
mainfrom
fix/bitwarden-lookup-duplicates
Sep 18, 2026
Merged

NavidSassan merged 2 commits into
mainfrom
fix/bitwarden-lookup-duplicates

Conversation

@NavidSassan

Copy link
Copy Markdown
Member

The bitwarden_item lookup created duplicates of Bitwarden items when it ran for several hosts in parallel, also of items that had existed for months, and the next run then aborted with "Found multiple Bitwarden items". Two causes: the worker processes did not coordinate, and bw serve briefly reports an empty vault right after a sync (bitwarden/clients#23283, filed with a minimal reproduction). While testing under Mitogen, it turned out that the bitwarden_item module does not run there at all; that is fixed in a second commit.

What is in here

Duplicates (443a6283)

  • The lookup and the module hold a mutex while they sync, search and create: flock on lfops_bitwarden_cache.json.mutex next to the cache file, waiting at most 300 s. Once a process holds it, it re-reads the cache, so it sees what the previous holder synced or created. Only one process per controller syncs, instead of one per host.
  • It is called a mutex, not a lock, to keep it apart from locking the vault (bw lock, status: locked).
  • An empty list/object/items is not taken as an empty vault: it is asked for again 5 times, 2 s apart, and then the run aborts with a pointer to #23283. get_item_by_id() retries its API fallback the same way, since bw serve answers 400 for an existing item in the same window.

Mitogen (e9ac6aee)

  • module_utils/bitwarden.py fell back to a no-op display only on ImportError. Under Mitogen the import of ansible.utils.display is served from the controller, and loading ansible.constants then raises AnsibleError ("Missing base YAML definition file"), so every module run failed with MODULE FAILURE, for example the grafana role's "store tokens to bitwarden" task. The fallback now catches any exception.

Points worth a close look

  • A vault that contains no items at all now aborts instead of getting its first item created. bw serve cannot be told apart from an empty vault in that window, and duplicate secrets are the worse outcome. The error message says to create one item by hand. This is under Changed in the CHANGELOG.
  • The mutex is held for the whole lookup or module run and released right after. flock belongs to the open file, not to the process, so holding it longer would make the next evaluation in the same worker, or the next module run in a Mitogen interpreter, wait for itself.
  • Not in here: checking the vault again right before creating an item, which would also catch partial lists. None were observed, only empty ones.

Testing

  • Unit tests: 49 pass (ansible-core 2.16, Python 3.11), including a test with 8 real processes that create the same missing item: once with the mutex, 8 times without it.
  • End-to-end against a private Bitwarden account with bw 2026.9.0, 8 hosts in parallel, both versions patched to force a full sync on every sync:
main this branch
New item, 5 rounds 3 to 13 items per name exactly 1 per name
Existing item, 5 rounds 1 item became 10, rounds 2 to 5 abort stays 1, all runs pass
Syncs per run 8 (and a 429 from the server) 1
Empty list from bw serve taken as "item missing" retried 3 times, then correct
  • Mitogen 0.3.53 (mitogen_linear): the lookup behaves the same; the module runs three times per host on 8 hosts without errors or mutex timeouts, where main fails every call.
  • No Molecule scenario covers the Bitwarden plugins; the code runs on the controller only.

Parallel Ansible workers each synced bw serve, found no matching item and
created one, even for items that had existed for months. Two causes:

- No coordination between processes: every worker synced, searched and
  created on its own, and the last one to write the cache file won.
- bw serve answers list/object/items with success and no items for a few
  seconds after a sync, and 400 for an existing item in the same window
  (bitwarden/clients#23283, reproduced with bw
  2026.8.0 and 2026.9.0).

The lookup and the module now hold a mutex (flock on a file next to the
cache) while they sync, search and create, and re-read the cache once they
hold it. It is not called a lock to keep it apart from locking the vault.
An empty item list is asked for again 5 times, 2s apart, and then aborts
instead of being taken as an empty vault. The API fallback of
get_item_by_id() retries the same way.
module_utils/bitwarden.py imports ansible.utils.display and falls back to
a no-op display on ImportError, for module runs on a managed node without
the controller's ansible package. Under Mitogen the import is served from
the controller and succeeds, but loading ansible.constants then raises
AnsibleError ("Missing base YAML definition file"), because the data files
are not served along. That escaped the except clause, so every run of the
bitwarden_item module failed with MODULE FAILURE, for example the grafana
role's "store tokens to bitwarden" task.

Catch any exception for the fallback. Verified with Mitogen 0.3.53 and
ansible-core 2.16 on Rocky Linux 9: 8 hosts calling the module three times
each run through, and the mutex from the previous commit is released
between the calls.
@NavidSassan NavidSassan added the bug Something isn't working label Sep 18, 2026
@NavidSassan
NavidSassan merged commit 240cd71 into main Sep 18, 2026
12 checks passed
@NavidSassan
NavidSassan deleted the fix/bitwarden-lookup-duplicates branch September 18, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant