Bug: two programs sharing one database can both start a scan at the same moment
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 56/100
Research direction
Read backend/data_access/network_data/scan_store.py and DatabaseEngine.unit_of_work() to determine whether one connection can hold a MySQL named lock through commit; then inspect backend/services/scan/scan_session.py and ScanStoreLike. Implement the described atomic start flow and update FakeScanStore in tests/services/scan/test_scan_session.py. Run the named-lock and concurrency cases in tests/services/scan/test_scan_session_db.py; done means simultaneous opens on one database start exactly one scan and the README explains the single-database rule.
Written by the indexing model from the issue text.
Description
Only one scan may run at a time (CM-15). ScanSessionController.open() enforces this in two steps: it asks the database whether a scan is running (find_running()), and if none is, it saves the new scan as running (start()). The lock around those two steps, self.lock, is a Python threading.Lock, so it only stops threads inside one program. If two programs use the same database (two server processes, or .\dev scan on two machines pointed at one MySQL), both can run find_running() before either has saved its scan. Both see "nothing running", both save a running scan, and both scans run. This breaks CM-15 and leaves two running rows. find_running() then returns only the newest, so the other scan can't be seen or stopped from the status screen until recover_stuck_scans() runs. Found while testing on two machines (see "Not this bug" below).
How to reproduce
- Point two copies of the code at the same database (the same
DB_HOST,DB_PORTandDB_NAMEin both.envfiles). - Start a scan from both within the same second:
.\dev scan 192.168.1.0/24 --user scanner - Both start.
SELECT scan_id, status, started_at FROM scan WHERE status = 'running';shows two rows. - The window is small: a scan started a few seconds after the other is correctly refused, so the race needs near-simultaneous starts. The integration test below hits it on purpose with a barrier.
Fix: take the lock in MySQL, which every program shares
- In
backend/data_access/network_data/scan_store.py, addstart_if_idle(scan) -> tuple[Scan | None, Scan | None]. It returns(new scan, None)when it started one and(None, the running scan)when it was refused. It runs, on one connection:SELECT GET_LOCK('network_scanner.scan_open', 5). Returns 1 when we hold the lock. A result of 0 (another program held it for 5 s) or NULL (error) means refuse with "Another scan is starting. Try again in a moment." and save nothing.- In one unit of work:
find_running(). If a scan is running, return it. Otherwise insert the new scan the same waystart()does. - Commit.
SELECT RELEASE_LOCK('network_scanner.scan_open'), in afinally, so it is released even when step 2 fails.
- The lock must be released after the commit. Released before it, the second program could check before the first program's row is visible and start anyway.
GET_LOCKbelongs to one connection, so the lock, the check, the insert and the release must all use the same connection. IfDatabaseEngine.unit_of_work()can't hold one connection across the commit, add a smallDatabaseEngine.named_lock(name, timeout_s)context manager that does.- MySQL frees the lock by itself if the connection drops, so a crashed program can't block scans forever.
- In
backend/services/scan/scan_session.py,open()callsscan_store.start_if_idle(...)instead offind_running()thenstart(). It keepsself.lock(cheap, and it covers_retry_pending_closes()and the in-memory progress) and builds the same_still_running_message(running)refusal from the returned scan. - Add
start_if_idleto theScanStoreLikeProtocol, and toFakeScanStoreintests/services/scan/test_scan_session.py, where it does the same check and insert under onethreading.Lock. - Keep
start()for the tests and tools that need to create a scan directly.
Why not something else
- A UNIQUE index so the database refuses a second running row. MySQL 5.7 can do this with a generated column (
running_guard= 1 when running, NULL otherwise, with a UNIQUE index). It's stronger, but it needs a schema change and a migration on every database, and it turns a refusal into a duplicate-key error we would have to translate. It's worth considering later;GET_LOCKfixes the bug without touching the schema. SELECT ... FOR UPDATEon the running rows. When no scan is running there is no row to lock, so it doesn't stop the race this bug is about.
Not this bug
Two machines that each use their own MAMP database (DB_HOST=127.0.0.1, the default) can both run a scan. Each records its scan in its own database, so neither can know about the other. That's expected: one installation means one database. Testing across machines needs DB_HOST on the second machine set to the first machine's address, with MySQL there allowing remote connections.
Tests
- Unit (
test_scan_session.py):open()usesstart_if_idle, and a refused start returns the running scan's message, as before. - Integration (
test_scan_session_db.py): two threads, each with its ownDatabaseEngineand controller (separate connections, like two programs), wait on athreading.Barrier(2)and callopen()together. Exactly one starts; the other is refused naming the first. Afterwards there is exactly onerunningrow. Repeat 20 times to show it isn't luck, and clean up intearDownwithrecover_stuck_scans(). - Integration: while one connection holds
GET_LOCK('network_scanner.scan_open', 0),open()from another engine waits up to 5 s, then refuses with "Another scan is starting" and saves nothing. - Every existing
test_scan_session*andtest_scan_managertest still passes.
Done when
- Two programs on one database calling
open()at the same moment start exactly one scan, every time. - The refusal reads the same as today: "A scan started by X at HH:MM is still running."
- The README's "Running a scan (#44)" section says one database means one running scan, however many programs or machines use it.
Milestone: M4. Related: #45, #48, CM-15.
- Dominant language
- Python
- Stars
- 1
- Forks
- 1
- Avg merge
- 8h 51m
- Merged PRs (30d)
- 39
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Bearkat-Professionals/Software-Engineering-Project
-
bug frontend help wanted High Priority
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Bearkat-Professionals/Software-Engineering-Project#98 ·
Maintainers usually reply within 1 day
-
SUBTASK #86: Password hashing (BB-5)Possibly taken @Edgeless6 claimed this 4 days ago. Openbackend Medium Priority subtask
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Bearkat-Professionals/Software-Engineering-Project#87 · 1 assignee ·
Maintainers usually reply within 1 day
-
Enhancement: Regression tests for Validate Input crash fixes (SH-1)Possibly taken @yoshi301 claimed this 3 days ago. Openbackend Low Priority
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
Bearkat-Professionals/Software-Engineering-Project#59 · 1 assignee ·
Maintainers usually reply within 1 day
-
SUBTASK #86: Manage user accounts (admin)Possibly taken @Edgeless6 claimed this 4 days ago. Openbackend Medium Priority subtask
Difficulty 4/5 Over a week Newbie friendliness 20/100
Bearkat-Professionals/Software-Engineering-Project#92 · 1 assignee ·
Maintainers usually reply within 1 day
-
SUBTASK #86: Login screen and signed-in layoutPossibly taken @Solo-Her0 claimed this 4 days ago. Openfrontend Medium Priority subtask
Difficulty 4/5 3-5 days Newbie friendliness 28/100
Bearkat-Professionals/Software-Engineering-Project#91 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in Bearkat-Professionals/Software-Engineering-Project
Similar issues
-
docs size:S
Difficulty 1/5 Under an hour Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
strawberry-graphql/strawberry#4794 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dataforgoodfr/cetolog#21 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
btclib-org/btclib#2932 ·
Maintainers usually reply within 1 day