andrijapanicsb opened a new pull request, #14527:
URL: https://github.com/apache/cloudstack/pull/14527
### Description
This PR fixes #14522.
#12409 added a cleanup to the async-job GC task that moves networks stuck in
Implementing to Shutdown, so that a
network left in Implementing after a crash or a lost job can be used again.
It treats a network as orphaned when it
is older than `job.expire.minutes` and no pending job of instance type
`Network` refers to it. A network that a VM
start or deployment is implementing has no such job (the work runs inside
the VM's job), so a cleanup run that lands
during the implementation (for an isolated network, while its virtual router
boots) shuts the network down under the
implementing thread, and the start fails with "Network is not yet fully
implemented". The start and deploy APIs retry
once, which usually hides it. Instance boot group starts (#13907, on main)
start each VM once and fail outright; HA
restarts also start once (not tested here). Details and a log trace are in
#14522.
The thread that implements a network
(`NetworkOrchestrator.implementNetwork`) holds the network's lock-table lock for
the whole time the network is in Implementing. So a network in Implementing
is orphaned only if that lock is free.
This PR:
1. In `AsyncJobManagerImpl.cleanupOrphanedNetworks()`, for each candidate
that passes the existing age check:
- try the network's lock once, without waiting (`acquireInLockTable(id,
0)`); if it is held, skip the network;
- otherwise use the row re-read under the lock: skip unless it is still
Implementing, then apply the existing
pending-job check and the existing transition to Shutdown;
- release the lock in `finally`.
A network that is really stuck (for example after a management server
crash, whose locks are released when the
server leaves the cluster or restarts) is still cleaned up as before.
2. **This also changes the lock-table class in framework/db,
`Merovingian2`.** In `acquire(key, timeInSeconds)`, a
timeout of 0 (or less) now makes exactly one attempt and returns at once,
without sleeping and without the
"Timed out on acquiring lock" WARN. Callers with a positive timeout run
the unchanged loop.
- Why it is needed: today the wait loop is `while (elapsed <
timeInSeconds * 1000) { try; sleep 5 s }`. For 0 the
body never runs, so the lock is never tried: `acquireInLockTable(id,
0)` always returns null, and a WARN with a
stack trace is logged. The cleanup could then never clean anything up,
including a network that is really stuck.
- Why not a 1 s timeout: on a held lock, `acquire(key, 1)` tries, sleeps
5 s, and can try and sleep once more,
because the loop's clock (`InaccurateClock`) only ticks every 10 s.
Each busy network would block the GC task
for 5-10 s and log the timeout WARN with a stack trace. That would
happen on every implementation of an old
network, and many networks implemented at once (a mass start after
maintenance) would stall the job GC for
minutes.
- No current caller passes 0. How this was checked on 4.20:
- `Merovingian2.acquire` is only called from `TransactionLegacy.lock`,
which is only called from
`GenericDaoBase.acquireInLockTable` and `lockInLockTable`.
- All 78 call sites of `acquireInLockTable`/`lockInLockTable` (`grep
-rnE "acquireInLockTable\(|lockInLockTable\("`
outside tests) were reviewed. The one-argument form uses the DAO's
`lock.timeout`, which defaults to 300 s and
is not set anywhere in the code base. The explicit timeouts are
literals (10, 30, 1200) or three settings:
`network.lock.timeout` (default 600), `storage.pool.max.waitseconds`
(3600) and `vm.job.lock.timeout` (1800).
- `GlobalLock` (MySQL `GET_LOCK` through `DbUtil.getGlobalLock`) is a
separate mechanism and is not affected.
- So 0 can only come from an administrator setting one of those three
to 0. Today that makes every acquisition
fail at once (for example, no network could be implemented). With
this change it means a single attempt.
No schema change, no new setting.
### Types of changes
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] Enhancement (improves an existing feature and functionality)
- [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
- [ ] build/CI
- [ ] test (unit or integration test code)
### Feature/Enhancement Scale or Bug Severity
#### Bug Severity
- [ ] BLOCKER
- [ ] Critical
- [x] Major
- [ ] Minor
- [ ] Trivial
### How Has This Been Tested?
Unit tests:
- `AsyncJobManagerImplTest`: lock held → no transition; lock free, still
Implementing, old enough → transition to
Shutdown on the re-read network, then the lock is released; lock free but
the network was implemented meanwhile →
no transition; a pending Network job → no transition; a young network →
the lock is not tried; the forced Shutdown
fallback runs under the lock.
- `Merovingian2AcquireTest` (lock table calls mocked, no database): a zero
timeout takes a free lock with a single
attempt, returns false at once on a lock held elsewhere, re-enters a lock
held by the same thread, and gives up
after one attempt when the insert loses a race; a positive timeout still
waits and retries (held on the first
try, free on the second: acquired after about 5 s). The framework/db
module excludes `com/cloud/utils/db/*Test*`
from surefire (those are database-backed tests, like `Merovingian2Test`),
so this test was run with
`-Dtest=Merovingian2AcquireTest`.
- Negative control: with the new tests on the old code (only
`cleanupOrphanedNetworks` made `protected` so the tests
compile), 9 of the 11 new tests fail. The two that pass are the
young-network and positive-timeout cases, whose
behaviour this PR does not change.
Tested before and after the patch on a single-host KVM test environment
(MySQL 8.0, isolated networks on
DefaultIsolatedNetworkOfferingWithSourceNatService, default
`job.expire.minutes`, `network.gc.wait` and
`network.gc.interval`). The test environment ran the main nightly
(24.0.0-SNAPSHOT.20261027), with the two changed
classes, built from main with the same change, swapped into the management
server jar for the "after" run; the
changed code is identical on 4.20, and the unit tests above ran on this
branch, which is based on 4.20. To make a new
test network "old" without changing `job.expire.minutes` (which would also
expunge the job history), its `created`
column was moved back by two days.
| Case | Before (original jar) | After (patched jar) |
|---|---|---|
| Deploy a VM into a new network older than `job.expire.minutes` | Cleanup
fired 8 s into the implementation (`Found orphaned network ... age: 2880
minutes`); the first start attempt failed with "Network is not yet fully
implemented"; the deploy's retry passed | No orphan line; `Network <id> in
Implementing state is locked (being implemented) ... skipping cleanup` on 5
cleanup runs; one start attempt, passed |
| Instance boot group start (no retry) on such a network | Cleanup fired 10
s in; **the boot group start failed** ("Failed to start a VM in boot group
...") | Skipped on 3 runs; the boot group start succeeded on its single attempt
|
| Stop the VM, let the network GC shut the network down, start the VM again
| Cleanup fired 4 s in; the first attempt failed, the retry passed | Skipped on
4 runs; one start attempt, passed |
| Network older than the threshold set to Implementing in the DB, no lock
held (really stuck) | Moved to Shutdown within 10 s | **Still moved to Shutdown
within 10 s**; no lock left behind |
With the patch, no `Timed out on acquiring lock` line was logged, and the
only orphan line was the one for the really
stuck network. A bytecode comparison of the swapped classes against the
nightly's showed that only
`AsyncJobManagerImpl.cleanupOrphanedNetworks`, the new
`cleanupNetworkIfOrphaned` and `Merovingian2.acquire` differ.
#### How did you try to break this feature and the system with this change?
- The stuck case above: a network in Implementing with no lock holder must
still be moved to Shutdown.
- The cleanup holds a network's lock only for the few milliseconds of the
re-read and transition, and only when the
lock was free; an implementation that starts in that window waits for the
lock as it would for any other holder.
Thank you @Pearl1594 for #12409. This keeps its cleanup as it is and only
narrows what it treats as orphaned.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]