Skip to content

refactor(github): generalize property fetching, fix cancellation swallowing - #2828

Closed
PonceGL wants to merge 2 commits into
PixelPlayerHQ:masterfrom
PonceGL:chore/p9-generic-properties-fetch
Closed

refactor(github): generalize property fetching, fix cancellation swallowing#2828
PonceGL wants to merge 2 commits into
PixelPlayerHQ:masterfrom
PonceGL:chore/p9-generic-properties-fetch

Conversation

@PonceGL

@PonceGL PonceGL commented Sep 10, 2026

Copy link
Copy Markdown

What

GitHubAnnouncementPropertiesService.fetchPlayStoreAnnouncement() mixed two
concerns: the generic mechanics of reading a raw .properties file from a
GitHub repo, and the Play Store announcement's own field mapping. Splitting
them lets other consumers (a remote feature-flag file, for instance) read
their own config without knowing anything about announcements. One of the
small independent fixes listed in #2813.

While generalizing it, found and fixed a real bug: the catch wrapped
Exception broadly, silently turning a coroutine cancellation into an
ordinary Result.failure instead of letting it propagate.

Change

  • fetchProperties(owner, repo, branch, configPath): public, generic, no
    defaults (a generic utility shouldn't default to any one caller's repo).
  • Split into three internal pieces: buildRawContentUrl (pure URL
    construction), fetchRawProperties(rawUrl) (the actual HTTP call),
    Properties.toPlayStoreAnnouncementRemoteConfig() (the mapping).
    fetchProperties() itself always points at real GitHub with no way to
    intercept it in a test — fetchRawProperties accepts any URL, so tests
    point it at a local com.sun.net.httpserver.HttpServer (JDK, no new
    dependency) instead.
  • fetchPlayStoreAnnouncement() keeps its exact behavior and defaults
    (including the pre-rename repo name — that's a separate, already-open PR,
    not this one).
  • catch (e: CancellationException) { throw e } before the broad catch —
    the actual fix.

Honest about what the cancellation fix does and doesn't guarantee:
HttpURLConnection is a classic blocking-socket API and does not react to
Thread.interrupt() — verified empirically (tried runInterruptible and a
disconnect() triggered from invokeOnCompletion; neither achieves real
cancellation of a call already blocked in getResponseCode()). What this
fix guarantees is narrower but real: cancellation is never swallowed once
it does reach this code. Making the blocking call itself promptly
interruptible would mean replacing HttpURLConnection with something that
supports it (OkHttp) — out of scope here, noted in the code for whoever
touches this next.

Testing

10 JVM tests, stable across repeated runs. The cancellation test is
deterministic (cancel the coroutine before the call starts) rather than
timing-dependent, for the reason above — a during-flight cancellation test
would be flaky by construction against a blocking API that doesn't actually
interrupt.

assembleDebug succeeds, full JVM baseline unaffected (5 pre-existing
failures, none new).

…lowing

GitHubAnnouncementPropertiesService only knew how to fetch the Play
Store announcement config. Split it into three pieces so any future
consumer (this repo's downloads feature, upstream or not) can read
its own properties file without knowing anything about Play Store
announcements:

- buildRawContentUrl: pure URL construction.
- fetchRawProperties(rawUrl): the actual HTTP GET + Properties
  parsing, given any URL — not just github.com. This is what makes
  the 404/error paths testable at all: fetchProperties() always
  builds a real GitHub URL by design, so there is no seam to
  intercept it in a unit test without hitting the network for real.
  fetchRawProperties has none of that restriction, so tests point it
  at a local com.sun.net.httpserver.HttpServer instead (JDK-included,
  no new dependency).
- Properties.toPlayStoreAnnouncementRemoteConfig(): the mapping,
  pulled out so it's testable directly against a hand-built
  Properties with no network involved at all.

fetchPlayStoreAnnouncement() is now just fetchProperties(...).mapCatching { it.toPlayStoreAnnouncementRemoteConfig() }
— same signature, same defaults (including the stale
theovilardo/PixelPlay repo; that's a separate fix), same behavior on
200/404/error, verified with a characterization test written before
touching the mapping logic.

Also fixed: the original catch (e: Exception) silently turned a
cancelled coroutine into a Result.failure instead of letting
CancellationException propagate (AND-CONC-04). Worth being explicit
about what this does and doesn't achieve: HttpURLConnection is a
classic blocking-socket API that does not react to
Thread.interrupt(), which I confirmed empirically — neither
runInterruptible nor closing the connection from a cancellation
handler actually interrupts a call that's genuinely blocked in
getResponseCode(). So a cancellation that arrives while the network
call is in flight still has to wait for that call to finish on its
own (success, error, or its 10s timeout) before it can surface. What
the fix does guarantee: once a CancellationException does reach this
code, it is never swallowed. Making the call itself promptly
interruptible would mean moving off HttpURLConnection entirely
(OkHttp, e.g.) — out of scope here, noted in the KDoc for whoever
touches this file next.

10 new JVM tests, stable across repeated runs: the URL builder, the
mapping (including every boolean spelling and blank-vs-absent
values), fetchRawProperties against a real local server for
200/404/500/unreachable-host, and a deterministic cancellation test
(cancel before the call starts, rather than racing real socket
timing — which the empirical finding above shows doesn't actually
cancel anyway).

No consumer changes: MainActivity's fetchPlayStoreAnnouncement() call
is untouched.

Preparatory task for the downloads feature (P.9, cut from master —
generic, unrelated to that feature's UI).
Comments cited local-only planning files and rule IDs (docs/downloads/*.md,
GEN-/AND- rule docs) that don't exist outside this machine and are
meaningless to anyone reviewing the PR. Reworded to keep the same
technical reasoning without the dangling references.
@PonceGL

PonceGL commented Sep 10, 2026

Copy link
Copy Markdown
Author

Closing for now — reorganizing how this work is staged. It'll go through our fork first and we'll propose it upstream again, possibly bundled differently, once the larger feature it's part of is further along. Not a rejection, just a process change on our side.

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.

1 participant