| 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 | 151 | timeout: Duration, |
| 152 | 152 | ) -> Result<T, std::sync::mpsc::RecvTimeoutError> { |
| 153 | 153 | let deadline = web_time::Instant::now() + timeout; |
| 154 | let mut armed = false; | |
| 155 | 154 | let mut answer = std::pin::pin!(receive(receiver)); |
| 156 | 155 | std::future::poll_fn(|context| { |
| 157 | 156 | if let Poll::Ready(answer) = answer.as_mut().poll(context) { |
| ... | ... | @@ -162,9 +161,10 @@ mod web { |
| 162 | 161 | if web_time::Instant::now() >= deadline { |
| 163 | 162 | return Poll::Ready(Err(std::sync::mpsc::RecvTimeoutError::Timeout)); |
| 164 | 163 | } |
| 165 | if !std::mem::replace(&mut armed, true) { | |
| 166 | after(timeout, context.waker().clone()); | |
| 167 | } | |
| 164 | after( | |
| 165 | deadline.saturating_duration_since(web_time::Instant::now()), | |
| 166 | context.waker().clone(), | |
| 167 | ); | |
| 168 | 168 | Poll::Pending |
| 169 | 169 | }) |
| 170 | 170 | .await |
| ... | ... | @@ -227,12 +227,11 @@ mod web { |
| 227 | 227 | return waker.wake(); |
| 228 | 228 | }; |
| 229 | 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 | 233 | pub(crate) async fn wait(receiver: &Receiver<()>, timeout: Option<Duration>) { |
| 234 | 234 | let deadline = timeout.map(|timeout| web_time::Instant::now() + timeout); |
| 235 | let mut armed = false; | |
| 236 | 235 | std::future::poll_fn(|context| { |
| 237 | 236 | let Ok(mut bell) = receiver.0.lock() else { |
| 238 | 237 | return Poll::Ready(()); |
| ... | ... | @@ -243,9 +242,7 @@ mod web { |
| 243 | 242 | return Poll::Ready(()); |
| 244 | 243 | } |
| 245 | 244 | bell.waiting = Some(context.waker().clone()); |
| 246 | if let Some(deadline) = deadline | |
| 247 | && !std::mem::replace(&mut armed, true) | |
| 248 | { | |
| 245 | if let Some(deadline) = deadline { | |
| 249 | 246 | after( |
| 250 | 247 | deadline.saturating_duration_since(web_time::Instant::now()), |
| 251 | 248 | context.waker().clone(), |
tools/web/receipt_probe.mjs+10-2| ... | ... | @@ -7,7 +7,14 @@ import { setTimeout } from 'node:timers'; |
| 7 | 7 | globalThis.crypto ??= webcrypto; |
| 8 | 8 | let failure = 0; |
| 9 | 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 | 18 | const failures = [0, 0, 0]; |
| 12 | 19 | globalThis.snowboundReceiptPhase = value => { phase = value; }; |
| 13 | 20 | globalThis.snowboundReceiptFailure = value => { failure = value; }; |
| ... | ... | @@ -26,7 +33,8 @@ try { |
| 26 | 33 | await wasm.receipt_probe(); |
| 27 | 34 | assert.ok(failures[1] >= 2, 'The active and dropped owner each retry synchronous storage failure'); |
| 28 | 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 | 38 | } finally { |
| 31 | 39 | clearTimeout(timeout); |
| 32 | 40 | } |