Skip to content

Commit 5d37508

Browse files
committed
Merge branch 'dev' into polish/vol1-final-page-balance
2 parents 605aecf + 93b8792 commit 5d37508

49 files changed

Lines changed: 1115 additions & 211 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -741,19 +741,23 @@ Thanks goes to these wonderful people who have contributed to making this resour
741741
</tr>
742742
<tr>
743743
<td align="center" valign="top" width="14.29%"><a href="https://github.com/Fabio-RibeiroB"><img src="https://avatars.githubusercontent.com/u/74654489?v=4?v=4?s=50" width="50px;" alt="Fábio"/><br /><sub><b>Fábio</b></sub></a><br />🧑‍💻 ✍️</td>
744+
<td align="center" valign="top" width="14.29%"><a href="https://github.com/aadityansha06"><img src="https://avatars.githubusercontent.com/u/96714228?v=4?v=4?s=50" width="50px;" alt="Aadityansha "/><br /><sub><b>Aadityansha </b></sub></a><br />🪲 🧑‍💻</td>
745+
<td align="center" valign="top" width="14.29%"><a href="https://github.com/euwyngoh"><img src="https://avatars.githubusercontent.com/u/211522196?v=4?v=4?s=50" width="50px;" alt="euwyngoh"/><br /><sub><b>euwyngoh</b></sub></a><br />🧑‍💻 ✍️</td>
744746
<td align="center" valign="top" width="14.29%"><a href="https://github.com/AmirAlasady"><img src="https://avatars.githubusercontent.com/AmirAlasady?v=4?s=50" width="50px;" alt="Amir Alasady"/><br /><sub><b>Amir Alasady</b></sub></a><br />🪲</td>
745747
<td align="center" valign="top" width="14.29%"><a href="https://github.com/jettythek"><img src="https://avatars.githubusercontent.com/jettythek?v=4?s=50" width="50px;" alt="jettythek"/><br /><sub><b>jettythek</b></sub></a><br />🧑‍💻</td>
746748
<td align="center" valign="top" width="14.29%"><a href="https://github.com/wz1114841863"><img src="https://avatars.githubusercontent.com/wz1114841863?v=4?s=50" width="50px;" alt="wzz"/><br /><sub><b>wzz</b></sub></a><br />🪲</td>
747749
<td align="center" valign="top" width="14.29%"><a href="https://github.com/keo-dara"><img src="https://avatars.githubusercontent.com/u/175544368?v=4?v=4?s=50" width="50px;" alt="keo-dara"/><br /><sub><b>keo-dara</b></sub></a><br />🪲</td>
748-
<td align="center" valign="top" width="14.29%"><a href="https://github.com/Kobra299"><img src="https://avatars.githubusercontent.com/u/4283156?v=4?v=4?s=50" width="50px;" alt="Wayne Norman"/><br /><sub><b>Wayne Norman</b></sub></a><br />🪲</td>
749-
<td align="center" valign="top" width="14.29%"><a href="https://github.com/lalalostcode"><img src="https://avatars.githubusercontent.com/u/149884766?v=4?v=4?s=50" width="50px;" alt="Ilham Rafiqin"/><br /><sub><b>Ilham Rafiqin</b></sub></a><br />🪲</td>
750750
</tr>
751751
<tr>
752+
<td align="center" valign="top" width="14.29%"><a href="https://github.com/Kobra299"><img src="https://avatars.githubusercontent.com/u/4283156?v=4?v=4?s=50" width="50px;" alt="Wayne Norman"/><br /><sub><b>Wayne Norman</b></sub></a><br />🪲</td>
753+
<td align="center" valign="top" width="14.29%"><a href="https://github.com/lalalostcode"><img src="https://avatars.githubusercontent.com/u/149884766?v=4?v=4?s=50" width="50px;" alt="Ilham Rafiqin"/><br /><sub><b>Ilham Rafiqin</b></sub></a><br />🪲</td>
752754
<td align="center" valign="top" width="14.29%"><a href="https://github.com/oscarf189"><img src="https://avatars.githubusercontent.com/u/28113740?v=4?v=4?s=50" width="50px;" alt="Oscar Flores"/><br /><sub><b>Oscar Flores</b></sub></a><br />✍️</td>
753755
<td align="center" valign="top" width="14.29%"><a href="https://github.com/sotoblanco"><img src="https://avatars.githubusercontent.com/u/46135649?v=4?v=4?s=50" width="50px;" alt="Pastor Soto"/><br /><sub><b>Pastor Soto</b></sub></a><br />✍️</td>
754756
<td align="center" valign="top" width="14.29%"><a href="https://github.com/salmanmkc"><img src="https://avatars.githubusercontent.com/u/32169182?v=4?v=4?s=50" width="50px;" alt="Salman Chishti"/><br /><sub><b>Salman Chishti</b></sub></a><br />🧑‍💻</td>
755757
<td align="center" valign="top" width="14.29%"><a href="https://github.com/adityamulik"><img src="https://avatars.githubusercontent.com/u/10626835?v=4?v=4?s=50" width="50px;" alt="Aditya Mulik"/><br /><sub><b>Aditya Mulik</b></sub></a><br />✍️</td>
756758
<td align="center" valign="top" width="14.29%"><a href="https://github.com/yarikoptic"><img src="https://avatars.githubusercontent.com/u/39889?v=4?v=4?s=50" width="50px;" alt="Yaroslav Halchenko"/><br /><sub><b>Yaroslav Halchenko</b></sub></a><br />🧑‍💻</td>
759+
</tr>
760+
<tr>
757761
<td align="center" valign="top" width="14.29%"><a href="https://github.com/RinZ27"><img src="https://avatars.githubusercontent.com/u/222222878?v=4?v=4?s=50" width="50px;" alt="Rin"/><br /><sub><b>Rin</b></sub></a><br />🧑‍💻</td>
758762
</tr>
759763
</tbody>
@@ -777,8 +781,11 @@ Thanks goes to these wonderful people who have contributed to making this resour
777781
<td align="center" valign="top" width="14.29%"><a href="https://github.com/Shashank-Tripathi-07"><img src="https://avatars.githubusercontent.com/u/178375647?v=4?v=4?s=50" width="50px;" alt="Rocky"/><br /><sub><b>Rocky</b></sub></a><br />🪲 🧑‍💻 🎨 ✍️</td>
778782
<td align="center" valign="top" width="14.29%"><a href="https://github.com/farhan523"><img src="https://avatars.githubusercontent.com/u/62025759?v=4?v=4?s=50" width="50px;" alt="Farhan Asghar"/><br /><sub><b>Farhan Asghar</b></sub></a><br />🪲 🧑‍💻 🎨 ✍️</td>
779783
<td align="center" valign="top" width="14.29%"><a href="https://github.com/aadityansha06"><img src="https://avatars.githubusercontent.com/u/96714228?v=4?v=4?s=50" width="50px;" alt="Aadityansha "/><br /><sub><b>Aadityansha </b></sub></a><br />🪲 🧑‍💻 🧪</td>
784+
<td align="center" valign="top" width="14.29%"><a href="https://github.com/ShivtejG236"><img src="https://avatars.githubusercontent.com/u/91597404?v=4?v=4?s=50" width="50px;" alt="Shivtej Gaikwad"/><br /><sub><b>Shivtej Gaikwad</b></sub></a><br />🪲 🧑‍💻 🧪</td>
780785
<td align="center" valign="top" width="14.29%"><a href="https://github.com/asgalon"><img src="https://avatars.githubusercontent.com/u/45242704?v=4?v=4?s=50" width="50px;" alt="Peter Koellner"/><br /><sub><b>Peter Koellner</b></sub></a><br />🪲 ✍️</td>
781786
<td align="center" valign="top" width="14.29%"><a href="https://github.com/hzeljko"><img src="https://avatars.githubusercontent.com/hzeljko?v=4?s=50" width="50px;" alt="Zeljko Hrcek"/><br /><sub><b>Zeljko Hrcek</b></sub></a><br />🧑‍💻</td>
787+
</tr>
788+
<tr>
782789
<td align="center" valign="top" width="14.29%"><a href="https://github.com/nyxst4ck"><img src="https://avatars.githubusercontent.com/u/289980115?v=4?v=4?s=50" width="50px;" alt="nyxst4ck"/><br /><sub><b>nyxst4ck</b></sub></a><br />✍️</td>
783790
</tr>
784791
</tbody>

labs/tests/test_wasm_persistence.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ class body, was silently rewritten by the Python compiler to
3939

4040
import functools
4141
import http.server
42+
import json
4243
import shutil
4344
import socketserver
4445
import threading
4546
from pathlib import Path
4647

4748
import pytest
4849

50+
4951
STATE_PY = (
5052
Path(__file__).resolve().parents[2] / "mlsysim" / "mlsysim" / "labs" / "state.py"
5153
)
@@ -205,3 +207,146 @@ def test_design_ledger_save_async_persists_in_real_indexeddb(served_dir):
205207
f"from #1985 / PR #1988. A mocked test cannot catch this; only a "
206208
f"real Pyodide + IndexedDB check like this one can."
207209
)
210+
211+
212+
def test_load_async_corrupt_record_sets_last_load_error(served_dir):
213+
"""A stored record exists but is corrupt JSON -- json.loads() raising
214+
is a Python-side failure independent of the JS resolve/reject shape,
215+
so last_load_error must be populated regardless of #1988's status."""
216+
from playwright.sync_api import sync_playwright
217+
218+
_, port = served_dir
219+
220+
with sync_playwright() as p:
221+
browser = p.chromium.launch()
222+
context = browser.new_context()
223+
try:
224+
page = context.new_page()
225+
init_errors: list[str] = []
226+
page.on(
227+
"pageerror",
228+
lambda exc, errors=init_errors: errors.append(str(exc)),
229+
)
230+
231+
page.goto(f"http://127.0.0.1:{port}/index.html")
232+
page.wait_for_function(
233+
"window.__ready === true || window.__initError", timeout=30_000
234+
)
235+
init_error = page.evaluate("window.__initError || null")
236+
assert not init_error, f"Pyodide init failed: {init_error}"
237+
assert not init_errors, f"Uncaught page errors during init: {init_errors}"
238+
239+
page.evaluate(
240+
"""
241+
() => new Promise((resolve) => {
242+
const req = indexedDB.deleteDatabase("mlsys_ledger_db");
243+
req.onsuccess = req.onerror = req.onblocked = () => resolve();
244+
})
245+
"""
246+
)
247+
248+
# Seed a corrupt record directly at the storage layer.
249+
page.evaluate(
250+
"""
251+
() => new Promise((resolve, reject) => {
252+
const req = indexedDB.open("mlsys_ledger_db", 1);
253+
req.onupgradeneeded = (e) => {
254+
const db = e.target.result;
255+
if (!db.objectStoreNames.contains("ledger")) {
256+
db.createObjectStore("ledger");
257+
}
258+
};
259+
req.onsuccess = (e) => {
260+
const db = e.target.result;
261+
const tx = db.transaction("ledger", "readwrite");
262+
tx.objectStore("ledger").put("{not valid json", "mlsys_design_ledger");
263+
tx.oncomplete = () => { db.close(); resolve(); };
264+
tx.onerror = () => { db.close(); reject(tx.error); };
265+
};
266+
req.onerror = () => reject(req.error);
267+
})
268+
"""
269+
)
270+
271+
result = page.evaluate(
272+
"""
273+
async () => {
274+
const pyodide = window.__pyodide;
275+
return await pyodide.runPythonAsync(`
276+
import json
277+
ledger = DesignLedger()
278+
await ledger.load_async()
279+
json.dumps({"error": ledger.last_load_error})
280+
`);
281+
}
282+
"""
283+
)
284+
page.close()
285+
finally:
286+
context.close()
287+
browser.close()
288+
289+
parsed = json.loads(result)
290+
assert parsed["error"] is not None, (
291+
"load_async() must surface a corrupt-JSON read failure via "
292+
"last_load_error instead of silently returning a blank LedgerState()."
293+
)
294+
295+
296+
def test_load_async_synchronous_indexeddb_open_throw_sets_last_load_error(served_dir):
297+
"""indexedDB.open() throwing synchronously must reject the Promise
298+
(per the Promise constructor spec) and propagate to last_load_error --
299+
true today even against the pre-#1988 resolve(null)-style onerror
300+
handlers, since this never reaches those handlers at all."""
301+
from playwright.sync_api import sync_playwright
302+
303+
_, port = served_dir
304+
305+
with sync_playwright() as p:
306+
browser = p.chromium.launch()
307+
context = browser.new_context()
308+
try:
309+
page = context.new_page()
310+
init_errors: list[str] = []
311+
page.on(
312+
"pageerror",
313+
lambda exc, errors=init_errors: errors.append(str(exc)),
314+
)
315+
316+
page.goto(f"http://127.0.0.1:{port}/index.html")
317+
page.wait_for_function(
318+
"window.__ready === true || window.__initError", timeout=30_000
319+
)
320+
init_error = page.evaluate("window.__initError || null")
321+
assert not init_error, f"Pyodide init failed: {init_error}"
322+
assert not init_errors, f"Uncaught page errors during init: {init_errors}"
323+
324+
page.evaluate(
325+
"""() => {
326+
window.indexedDB.open = () => {
327+
throw new Error('Simulated synchronous IndexedDB failure');
328+
};
329+
}"""
330+
)
331+
332+
result = page.evaluate(
333+
"""
334+
async () => {
335+
const pyodide = window.__pyodide;
336+
return await pyodide.runPythonAsync(`
337+
import json
338+
ledger = DesignLedger()
339+
await ledger.load_async()
340+
json.dumps({"error": ledger.last_load_error})
341+
`);
342+
}
343+
"""
344+
)
345+
page.close()
346+
finally:
347+
context.close()
348+
browser.close()
349+
350+
parsed = json.loads(result)
351+
assert parsed["error"] is not None
352+
assert "Simulated synchronous IndexedDB failure" in parsed["error"]

mlsysim/.all-contributorsrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@
8888
"contributions": [
8989
"doc"
9090
]
91+
},
92+
{
93+
"login": "ShivtejG236",
94+
"name": "Shivtej Gaikwad",
95+
"avatar_url": "https://avatars.githubusercontent.com/u/91597404?v=4",
96+
"profile": "https://github.com/ShivtejG236",
97+
"contributions": [
98+
"bug",
99+
"code",
100+
"test"
101+
]
91102
}
92103
]
93104
}

mlsysim/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,11 @@ Thanks to these wonderful people for helping improve MLSys·im!
267267
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Shashank-Tripathi-07"><img src="https://avatars.githubusercontent.com/u/178375647?v=4?v=4?s=80" width="80px;" alt="Rocky"/><br /><sub><b>Rocky</b></sub></a><br />🪲 🧑‍💻 🎨 ✍️</td>
268268
<td align="center" valign="top" width="14.28%"><a href="https://github.com/farhan523"><img src="https://avatars.githubusercontent.com/u/62025759?v=4?v=4?s=80" width="80px;" alt="Farhan Asghar"/><br /><sub><b>Farhan Asghar</b></sub></a><br />🪲 🧑‍💻 🎨 ✍️</td>
269269
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aadityansha06"><img src="https://avatars.githubusercontent.com/u/96714228?v=4?v=4?s=80" width="80px;" alt="Aadityansha "/><br /><sub><b>Aadityansha </b></sub></a><br />🪲 🧑‍💻 🧪</td>
270+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ShivtejG236"><img src="https://avatars.githubusercontent.com/u/91597404?v=4?v=4?s=80" width="80px;" alt="Shivtej Gaikwad"/><br /><sub><b>Shivtej Gaikwad</b></sub></a><br />🪲 🧑‍💻 🧪</td>
270271
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asgalon"><img src="https://avatars.githubusercontent.com/u/45242704?v=4?v=4?s=80" width="80px;" alt="Peter Koellner"/><br /><sub><b>Peter Koellner</b></sub></a><br />🪲 ✍️</td>
271272
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hzeljko"><img src="https://avatars.githubusercontent.com/hzeljko?v=4?s=80" width="80px;" alt="Zeljko Hrcek"/><br /><sub><b>Zeljko Hrcek</b></sub></a><br />🧑‍💻</td>
273+
</tr>
274+
<tr>
272275
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nyxst4ck"><img src="https://avatars.githubusercontent.com/u/289980115?v=4?v=4?s=80" width="80px;" alt="nyxst4ck"/><br /><sub><b>nyxst4ck</b></sub></a><br />✍️</td>
273276
</tr>
274277
</tbody>

mlsysim/mlsysim/labs/state.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(self):
3131
self.file_path = self.config_dir / "ledger.json"
3232

3333
self._state = LedgerState()
34+
self._last_load_error: Optional[str] = None
3435

3536
# WASM save tasks remain tracked until flush() observes them. Keeping
3637
# completed tasks lets a later flush() re-raise persistence failures.
@@ -49,6 +50,11 @@ def last_save_error(self) -> Optional[str]:
4950
"""
5051
return self._last_save_error
5152

53+
@property
54+
def last_load_error(self) -> Optional[str]:
55+
"""Error message from the most recent failed load."""
56+
return self._last_load_error
57+
5258
@property
5359
def save_pending(self) -> bool:
5460
"""True while at least one WASM background save is still running."""
@@ -75,6 +81,7 @@ def _parse_history(self, data: dict) -> dict:
7581

7682
def load(self) -> LedgerState:
7783
"""Loads the ledger from the best available persistent storage."""
84+
self._last_load_error = None
7885

7986
# WASM loading is asynchronous, so synchronous load()
8087
# simply returns the current in-memory state.
@@ -88,10 +95,10 @@ def load(self) -> LedgerState:
8895
data = json.load(f)
8996

9097
data["history"] = self._parse_history(data)
91-
9298
self._state = LedgerState(**data)
9399

94-
except Exception:
100+
except Exception as e:
101+
self._last_load_error = f"{type(e).__name__}: {e}"
95102
self._state = LedgerState()
96103

97104
return self._state
@@ -100,6 +107,7 @@ async def load_async(self) -> LedgerState:
100107
"""
101108
Async load for WASM environments using IndexedDB.
102109
"""
110+
self._last_load_error = None
103111

104112
if not self.is_wasm:
105113
return self.load()
@@ -187,14 +195,12 @@ async def load_async(self) -> LedgerState:
187195

188196
if raw:
189197
data = json.loads(raw)
190-
191198
data["history"] = self._parse_history(data)
192-
193199
self._state = LedgerState(**data)
194200

195201
except Exception as e:
202+
self._last_load_error = f"{type(e).__name__}: {e}"
196203
print(f"Failed to load from IndexedDB: {e}")
197-
198204
self._state = LedgerState()
199205

200206
return self._state

0 commit comments

Comments
 (0)