Skip to content

Preferences: report whether a flush actually persisted - #7852

Open
MRZ07 wants to merge 6 commits into
libgdx:masterfrom
MRZ07:preferences-save-result
Open

MRZ07 wants to merge 6 commits into
libgdx:masterfrom
MRZ07:preferences-save-result

Conversation

@MRZ07

@MRZ07 MRZ07 commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Adds an additive callback overload to Preferences.flush(PreferencesSaveCallback) for asynchronous persistence with a reported result. Existing flush() behavior remains unchanged.

API

preferences.flush(new PreferencesSaveCallback() {
    public void onSuccess () { ... }
    public void onFailure (PreferencesSaveResult result, Throwable cause) { ... }
});

The callback is invoked once after persistence completes. Existing custom Preferences implementations remain source-compatible through the default interface implementation; platform backends override it to perform asynchronous work.

Backend behavior

  • Android: runs SharedPreferences.Editor.commit() on a daemon worker thread, preserving the synchronous result without blocking the application thread.
  • LWJGL3 / LWJGL / Headless: runs the shared NIO write path on a daemon worker thread. AccessDeniedException is classified as ACCESS_DENIED; other write failures are IO_ERROR.
  • iOS (RoboVM and MetalANGLE): runs the existing dictionary write on a GCD background queue. RoboVM exposes only a boolean result, so failures are IO_ERROR.
  • GWT: defers the write to the next event-loop turn because JavaScript is single-threaded. Local Storage quota failures are classified as DISK_FULL using the browser error identifier QuotaExceededError.

PreferencesSaveResult.from(Throwable) is deliberately conservative and GWT-safe: SecurityException maps to ACCESS_DENIED, IOException maps to IO_ERROR, and other failures map to UNKNOWN. It does not parse localized OS error messages. Desktop backends do not claim DISK_FULL because Java NIO provides no dedicated ENOSPC exception type.

Verification

  • Full builds passed for gdx, Android, GWT, Headless, LWJGL, LWJGL3, RoboVM, and RoboVM MetalANGLE.
  • Full :gdx:test suite passed.
  • PreferencesSaveResultTest covers conservative type classification and cause-chain handling.
  • MetalANGLE generated sources were regenerated and validated.
  • Android, iOS, and browser runtime behavior still need device/browser validation.

@MRZ07
MRZ07 force-pushed the preferences-save-result branch from 9e0a4b8 to 4d85f12 Compare August 22, 2026 23:16
@obigu

obigu commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Even if I agree we lack consistency across backends and potential improvements (on iOS for example), I don't think going synchronous on disk write locking the main thread is the way to go. On mobile Android with SharedPreferences and iOS with UserDefaults recommend (or even force) async for performance reasons and I'd gess this applies to the rest of platforms.

Ideally persisting should occur in a background thread and we would have a callback that allows users to "block" or display a "Saving...." progress element on their end when ultra critical/rollback necessary write operations are required. For the rest of standard preferences persisting, performance is much more important.

@MRZ07

MRZ07 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor Author

A verified write must not block the game thread, so I’m keeping the existing no-argument flush() and adding an asynchronous flush(PreferencesSaveCallback) overload. Android and the JVM backends use a worker thread, RoboVM uses GCD, and GWT defers the write to the next event-loop turn.

@Frosty-J

Frosty-J commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

For async with callback I think a flush() overload would be clearer? To use the same terminology for async writes with or without callback.

@MRZ07

MRZ07 commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Implemented in 4599801. The callback now lives on Preferences.flush(...), keeping the existing terminology while leaving the no-argument flush() unchanged; backend writes run off the game thread where the platform allows it.

… outcome

flush() gives no way to tell whether a write reached disk: Android drops
failures silently via apply(), LWJGL3 throws GdxRuntimeException, iOS
ignores the writeToFile boolean. Add an additive save() that reports
SUCCESS or a best-effort PreferencesSaveResult through a callback,
implemented across all backends.
- FileNotFoundException no longer implies ACCESS_DENIED: it is thrown for
  many non-access reasons (missing parent dir, ...). Only explicit
  permission evidence (SecurityException, java.nio.file
  .AccessDeniedException matched by name for GWT compatibility,
  permission-related messages) maps to ACCESS_DENIED.
- DISK_FULL is now actually detected: POSIX/Windows disk-full messages and
  GWT QuotaExceededError map to it; removed the GWT backend override in
  favor of the shared classifier.
- Classification walks cause chains and prefers the most specific finding;
  bare IOException degrades to IO_ERROR.
- Deduplicated write logic in LWJGL/LWJGL3/Headless backends: flush() and
  save() share one writeToDisk() path.
- save(null) fails fast; javadoc documents threading and callback
  exception behavior.
- Added PreferencesSaveResultTest covering all classification rules.
OS error strings come from strerror() and are locale-dependent, so
message matching was fragile. Instead:

- Core from() uses only universally available types: SecurityException ->
  ACCESS_DENIED, IOException -> IO_ERROR, else UNKNOWN.
- LWJGL3/LWJGL/Headless write through java.nio.file.Files.newOutputStream
  so the JVM's errno translation is preserved as exception types;
  AccessDeniedException upgrades to ACCESS_DENIED locally.
- Desktop produces no DISK_FULL: ENOSPC has no dedicated NIO type and
  recovering it would require message parsing.
- GWT classifies locally: QuotaExceededError (a JS identifier, not
  localized prose) maps to DISK_FULL, else IO_ERROR.

Tests updated to type-based expectations.
- Async variant saveAsync(PreferencesSaveCallback) dispatches write to background:
  - Android: HandlerThread + commit()
  - LWJGL3/LWJGL/Headless: cached thread pool
  - iOS: GCD global queue (DispatchQueue)
  - GWT: Timer.schedule(0) to yield event loop
- Sync save() unchanged for callers needing immediate verification
- All backends override with platform-appropriate threading
@MRZ07
MRZ07 force-pushed the preferences-save-result branch from d52db02 to 4599801 Compare September 2, 2026 19:02
@MRZ07

MRZ07 commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Reworked this in 4599801 based on the feedback here. flush(PreferencesSaveCallback) now handles the asynchronous verified write, JVM backends use typed NIO errors instead of localized messages, and GWT handles QuotaExceededError separately. The affected builds and tests pass locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants