| author | |
| committer | |
| log | 7802ff53b2f17ccccf6e2589b59f9513202f7e07 |
| tree | 26708b848f0fad2cfec866d0ad17e2fa92f5141b |
| parent | a217722937719101c12d3c2521a03b33abc32c8c |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
A browser timer can fire before its requested Instant deadline. One-shot armed flags then left waits pending forever without another wake, stranding durability retries and Live Share response timeouts. Rearm remaining waits on each pending poll and round host millisecond delays upward.
Force an early retry timer in the actual-wasm receipt probe and require both accepted edit and later barrier to stay pending through storage failures, then recover with one publication and retained dropped-owner ownership.
Validation: actual wasm probe passed with 3 synchronous host throws, 1 Promise rejection, forced early timer, 1 publication, responsive reads, and dropped-owner busy/reopen. Native all-feature notebook+snowbound check, wasm wgpu/live check, JavaScript 9/9, and formatting passed on the identical combined source tree.
Fixes #113
Assisted-by: gpt-6.1-sol2 files changed, 16 insertions(+), 11 deletions(-)
crates/notebook/src/task.rs+6-9| ... | @@ -151,7 +151,6 @@ mod web { | ... | @@ -151,7 +151,6 @@ mod web { |
| 151 | timeout: Duration, | 151 | timeout: Duration, |
| 152 | ) -> Result<T, std::sync::mpsc::RecvTimeoutError> { | 152 | ) -> Result<T, std::sync::mpsc::RecvTimeoutError> { |
| 153 | let deadline = web_time::Instant::now() + timeout; | 153 | let deadline = web_time::Instant::now() + timeout; |
| 154 | let mut armed = false; | ||
| 155 | let mut answer = std::pin::pin!(receive(receiver)); | 154 | let mut answer = std::pin::pin!(receive(receiver)); |
| 156 | std::future::poll_fn(|context| { | 155 | std::future::poll_fn(|context| { |
| 157 | if let Poll::Ready(answer) = answer.as_mut().poll(context) { | 156 | if let Poll::Ready(answer) = answer.as_mut().poll(context) { |
| ... | @@ -162,9 +161,10 @@ mod web { | ... | @@ -162,9 +161,10 @@ mod web { |
| 162 | if web_time::Instant::now() >= deadline { | 161 | if web_time::Instant::now() >= deadline { |
| 163 | return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Timeout)); | 162 | return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Timeout)); |
| 164 | } | 163 | } |
| 165 | if !std::mem::replace(&mut armed, true) { | 164 | after( |
| 166 | after(timeout, context.waker().clone()); | 165 | deadline.saturating_duration_since(web_time::Instant::now()), |
| 167 | } | 166 | context.waker().clone(), |
| 167 | ); | ||
| 168 | Poll::Pending | 168 | Poll::Pending |
| 169 | }) | 169 | }) |
| 170 | .await | 170 | .await |
| ... | @@ -227,12 +227,11 @@ mod web { | ... | @@ -227,12 +227,11 @@ mod web { |
| 227 | return waker.wake(); | 227 | return waker.wake(); |
| 228 | }; | 228 | }; |
| 229 | let wake = Closure::once_into_js(move || waker.wake()); | 229 | let wake = Closure::once_into_js(move || waker.wake()); |
| 230 | let _ = set_timeout.call2(&global, &wake, &(delay.as_secs_f64() * 1e3).into()); | 230 | let _ = set_timeout.call2(&global, &wake, &(delay.as_secs_f64() * 1e3).ceil().into()); |
| 231 | } | 231 | } |
| 232 | 232 | ||
| 233 | pub(crate) async fn wait(receiver: &Receiver<()>, timeout: Option<Duration>) { | 233 | pub(crate) async fn wait(receiver: &Receiver<()>, timeout: Option<Duration>) { |
| 234 | let deadline = timeout.map(|timeout| web_time::Instant::now() + timeout); | 234 | let deadline = timeout.map(|timeout| web_time::Instant::now() + timeout); |
| 235 | let mut armed = false; | ||
| 236 | std::future::poll_fn(|context| { | 235 | std::future::poll_fn(|context| { |
| 237 | let Ok(mut bell) = receiver.0.lock() else { | 236 | let Ok(mut bell) = receiver.0.lock() else { |
| 238 | return Poll::Ready(()); | 237 | return Poll::Ready(()); |
| ... | @@ -243,9 +242,7 @@ mod web { | ... | @@ -243,9 +242,7 @@ mod web { |
| 243 | return Poll::Ready(()); | 242 | return Poll::Ready(()); |
| 244 | } | 243 | } |
| 245 | bell.waiting = Some(context.waker().clone()); | 244 | bell.waiting = Some(context.waker().clone()); |
| 246 | if let Some(deadline) = deadline | 245 | if let Some(deadline) = deadline { |
| 247 | && !std::mem::replace(&mut armed, true) | ||
| 248 | { | ||
| 249 | after( | 246 | after( |
| 250 | deadline.saturating_duration_since(web_time::Instant::now()), | 247 | deadline.saturating_duration_since(web_time::Instant::now()), |
| 251 | context.waker().clone(), | 248 | context.waker().clone(), |
tools/web/receipt_probe.mjs+10-2| ... | @@ -7,7 +7,14 @@ import { setTimeout } from 'node:timers'; | ... | @@ -7,7 +7,14 @@ import { setTimeout } from 'node:timers'; |
| 7 | globalThis.crypto ??= webcrypto; | 7 | globalThis.crypto ??= webcrypto; |
| 8 | let failure = 0; | 8 | let failure = 0; |
| 9 | let phase = 'initializing wasm'; | 9 | let phase = 'initializing wasm'; |
| 10 | globalThis.setTimeout = (callback, delay, ...args) => setTimeout(callback, delay, ...args).unref(); | 10 | let early = true; |
| 11 | globalThis.setTimeout = (callback, delay, ...args) => { | ||
| 12 | if (early && failure === 2 && delay <= 1000) { | ||
| 13 | early = false; | ||
| 14 | delay = 0; | ||
| 15 | } | ||
| 16 | return setTimeout(callback, delay, ...args).unref(); | ||
| 17 | }; | ||
| 11 | const failures = [0, 0, 0]; | 18 | const failures = [0, 0, 0]; |
| 12 | globalThis.snowboundReceiptPhase = value => { phase = value; }; | 19 | globalThis.snowboundReceiptPhase = value => { phase = value; }; |
| 13 | globalThis.snowboundReceiptFailure = value => { failure = value; }; | 20 | globalThis.snowboundReceiptFailure = value => { failure = value; }; |
| ... | @@ -26,7 +33,8 @@ try { | ... | @@ -26,7 +33,8 @@ try { |
| 26 | await wasm.receipt_probe(); | 33 | await wasm.receipt_probe(); |
| 27 | assert.ok(failures[1] >= 2, 'The active and dropped owner each retry synchronous storage failure'); | 34 | assert.ok(failures[1] >= 2, 'The active and dropped owner each retry synchronous storage failure'); |
| 28 | assert.ok(failures[2] >= 1, 'An asynchronous storage rejection also keeps the receipt pending'); | 35 | assert.ok(failures[2] >= 1, 'An asynchronous storage rejection also keeps the receipt pending'); |
| 29 | console.log(`Actual wasm receipts passed: ${failures[1]} throws, ${failures[2]} rejections, one publication, responsive reads, retained ownership`); | 36 | assert.equal(early, false, 'A retry timer fired before its deadline'); |
| 37 | console.log(`Actual wasm receipts passed: ${failures[1]} throws, ${failures[2]} rejections, early timer, one publication, responsive reads, retained ownership`); | ||
| 30 | } finally { | 38 | } finally { |
| 31 | clearTimeout(timeout); | 39 | clearTimeout(timeout); |
| 32 | } | 40 | } |