dongjoon-hyun commented on PR #58312:
URL: https://github.com/apache/spark/pull/58312#issuecomment-5457930929
Thanks for tracking this down. The direction looks right to me: putting the
whole of `release()` under the `active` lock and re-checking each eviction
candidate under the same lock is consistent with what `Lease.commit` already
does, and cleaning up the stale listing entry in `makeRoom()` is a nice
addition. I ran the new test locally and confirmed it reproduces the reported
failure on the pre-fix code:
```
- SPARK-58985: release with delete is atomic with openStore *** FAILED ***
(2 seconds, 567 ms)
java.lang.IllegalStateException: Disk usage tracker went negative (now =
-2, delta = -2)
```
Three comments.
### 1. The new test no longer exercises the interleaving it describes
With the fix in place, the latch handshake cannot work: `release()` holds
the `active` lock while it is blocked inside the mocked `sizeOf()`, so the main
thread's `openStore()` blocks on that same lock and never reaches
`openStoreDone.countDown()` in the `finally`. The release thread only proceeds
when `openStoreDone.await(2, TimeUnit.SECONDS)` **times out**.
The runtimes show this — 2.588 s with the fix, 2.567 s without; the 2
seconds are pure timeout wait.
As a result `release()` always completes first and deletes the store, so
`openStore()` always returns `None`, and this block plus its comment are dead
code after the fix:
```scala
// If openStore() handed out a path, the subsequent release in
FsHistoryProvider must not
// deduct the size again.
opened.foreach { _ =>
manager.release("app1", None, delete = true)
}
```
It is still a valid regression test, but it would read much better if it
asserted the post-fix behavior explicitly (`release()` wins the lock, therefore
`openStore()` returns `None`). Running `openStore()` on its own thread and
counting the latch down from there would also let both suites finish without
burning ~2.5 s each on a timeout.
### 2. `Lease.commit()` vs `release(delete = true)` leaves the same crash
reachable
`commit()` does `tmpPath.renameTo(dst)` and `updateUsage(newSize, committed
= true)` outside the lock, and only registers the app in `active` in a later
`synchronized` block. In that window the store is on disk but not in `active` —
exactly the shape this PR fixes for `openStore()`:
```
commit: rename tmp -> dst; committed += newSize
release: (lock) not active -> deducts sizeOf(dst), deleteStore(dst)
commit: active(app) = newSize // registers a store that is
already gone
release: (lock) deducts oldSize again -> committed goes negative
```
I verified this with a scratch test that blocks inside `commit()` between
the rename and the `active` insertion (hooking `Clock.getTimeMillis()`, which
`updateApplicationStoreInfo` calls in that window). It fails identically on
this branch and on `master`:
```
- commit vs release(delete = true) *** FAILED ***
java.lang.IllegalStateException: Disk usage tracker went negative (now =
-2, delta = -2)
at ...HistoryServerDiskManager.release(HistoryServerDiskManager.scala:195)
```
<details>
<summary>scratch test</summary>
```scala
test("commit vs release(delete = true)") {
val conf2 = new SparkConf().set(MAX_LOCAL_DISK_USAGE, MAX_USAGE)
val armed = new java.util.concurrent.atomic.AtomicBoolean(false)
val inCommit = new CountDownLatch(1)
val releaseDone = new CountDownLatch(1)
val clock = new ManualClock() {
override def getTimeMillis(): Long = {
if (armed.get()) {
inCommit.countDown()
releaseDone.await(10, TimeUnit.SECONDS)
}
super.getTimeMillis()
}
}
val manager = spy[HistoryServerDiskManager](
new HistoryServerDiskManager(conf2, testDir, store, clock))
doAnswer(AdditionalAnswers.returnsFirstArg[Long]()).when(manager)
.approximateSize(anyLong(), anyBoolean())
val leaseA = manager.lease(2)
doReturn(2L).when(manager).sizeOf(meq(leaseA.tmpPath))
val dst = leaseA.commit("app1", None)
doReturn(2L).when(manager).sizeOf(meq(dst))
manager.release("app1", None)
assert(manager.committed() === 2)
val leaseB = manager.lease(2)
doReturn(2L).when(manager).sizeOf(meq(leaseB.tmpPath))
armed.set(true)
val commitThread = new Thread(() => leaseB.commit("app1", None))
commitThread.start()
inCommit.await(10, TimeUnit.SECONDS)
// commit() has renamed the store into place and added its size, but has
not yet
// registered the app in the active map.
manager.release("app1", None, delete = true)
releaseDone.countDown()
commitThread.join(TimeUnit.SECONDS.toMillis(10))
assert(manager.committed() === 0)
// The provider that opened the store eventually releases it.
manager.release("app1", None, delete = true)
assert(manager.committed() === 0)
}
```
</details>
This is pre-existing rather than something this PR introduces, and the call
sites are real: `createDiskStore()` serving a UI request can run against
`cleanLogs()` calling `release(delete = true)` for the same app. `makeRoom()`
vs `commit()` has the same gap — a store being committed is not in `active`, so
it can still pass the new re-check and be deleted, and the `info.size` deducted
there is a stale snapshot value.
Given the PR title says "double-counting store size on concurrent release
and makeRoom", readers will likely assume the whole class of bug is closed.
Could you either note the remaining window in the description, or close it here
as well by reserving the `active` entry before the rename?
### 3. Minor
- In `makeRoom()`, `freed` is a `ListBuffer[Long]` but only `freed.size` and
`freed.sum` are used; two counters (`var freedBytes = 0L`, `var freedCount =
0`) would be simpler.
- `openStore()` still calls `updateApplicationStoreInfo` outside the lock,
so a `release()` that deletes the store in between resurrects a listing entry
for a path that no longer exists. The accounting stays correct and the new
stale-entry cleanup in `makeRoom()` eventually removes it, so this only skews
the `needed` computation during an eviction scan — not a blocker, just noting
it.
- The wider lock now covers `deleteStore()` (a recursive delete of a
potentially large RocksDB directory) in both `release()` and `makeRoom()`, so
UI-driven `openStore()` calls can block behind it. The trade-off is already
called out in the description; just flagging that `makeRoom()` inherits it too.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]