-
-
Notifications
You must be signed in to change notification settings - Fork 55
ADFA-4128 (8/11): quickbuild:core — session orchestration #1720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fryanpan
wants to merge
2
commits into
feature/ADFA-4128-qb-07-core-provisioning
from
feature/ADFA-4128-qb-08-core-orchestration
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
543 changes: 543 additions & 0 deletions
543
...e/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/QuickBuildSessionState.kt
Large diffs are not rendered by default.
Oops, something went wrong.
167 changes: 167 additions & 0 deletions
167
...ld/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/QuickBuildStatus.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| package org.appdevforall.cotg.quickbuild.domain.session | ||
|
|
||
| import org.appdevforall.cotg.quickbuild.domain.classify.InvalidationReason | ||
|
|
||
| /** | ||
| * What the status surface should show, derived purely from session state rather than set and | ||
| * cleared imperatively. | ||
| * | ||
| * Deriving it makes a stuck banner unrepresentable: every state maps to exactly one status, so | ||
| * every terminal state clears the transient one. A banner cleared only on successful render | ||
| * would leave "Compiling..." up forever after a compile error or a payload crash. | ||
| */ | ||
| sealed interface QuickBuildStatus { | ||
| /** | ||
| * No session - nothing in progress to narrate. | ||
| * | ||
| * @property lastStartFailed the last session start failed | ||
| * ([QuickBuildSessionState.Idle.lastStartFailed]), so the bolt keeps the error tone until | ||
| * the next tap or save; carried here because a failed start rests in Hidden and the tone | ||
| * is derived from status alone. | ||
| */ | ||
| data class Hidden( | ||
| val lastStartFailed: Boolean = false, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * Proxy app build, install and daemon spawn in progress. | ||
| * | ||
| * @property rebaselineReason what invalidated the old baseline, or null on a session's first | ||
| * provision; it has to travel in the status because this conflating | ||
| * [kotlinx.coroutines.flow.StateFlow] lets a surface miss the [NeedsFullBuild] that preceded a | ||
| * rebaseline and then call it "the initial full build". | ||
| */ | ||
| data class Provisioning( | ||
| val rebaselineReason: InvalidationReason? = null, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * A build is running; the proxy app still runs [runningGeneration]. | ||
| * | ||
| * @property runningGeneration the generation live in the proxy app right now, one behind the | ||
| * build in flight. | ||
| */ | ||
| data class Building( | ||
| val runningGeneration: Long, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * The proxy app is running the latest edit. | ||
| * | ||
| * @property generation the generation the proxy app runs, which is also the latest built. | ||
| * @property buildDurationMillis how long the landed save-to-live loop took, in milliseconds - | ||
| * the whole wait, not the build alone; null when no build landed in this session yet, and | ||
| * the surface then shows no timing. | ||
| * @property restarted the deploy relaunched the proxy-app process (service/provider/Application | ||
| * code changed), so the surface phrases it as a restart rather than a plain reload. | ||
| */ | ||
| data class UpToDate( | ||
| val generation: Long, | ||
| val buildDurationMillis: Long?, | ||
| val restarted: Boolean = false, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * The edit did not land; the proxy app still runs [runningGeneration]. | ||
| * | ||
| * @property runningGeneration the generation still live in the proxy app - a failure never | ||
| * moves it. | ||
| * @property failure what went wrong: a compile error, a failed deploy, or a crash of the | ||
| * running generation. | ||
| */ | ||
| data class Failed( | ||
| val runningGeneration: Long, | ||
| val failure: SessionFailure, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * The baseline is stale; only a full Gradle build can move the proxy app forward. | ||
| * | ||
| * @property reason what the live reload path could not absorb, which the surface names to the | ||
| * user. | ||
| * @property runningGeneration the generation still live in the proxy app until the rebuild | ||
| * lands. | ||
| * @property awaitingRetry a rebaseline already ran and parked (build failed or install not | ||
| * confirmed), so the surface must read as a failure the user resolves rather than ordinary | ||
| * upcoming work; see [QuickBuildSessionState.Invalidated.awaitingRetry]. | ||
| */ | ||
| data class NeedsFullBuild( | ||
| val reason: InvalidationReason, | ||
| val runningGeneration: Long, | ||
| val awaitingRetry: Boolean = false, | ||
| ) : QuickBuildStatus | ||
|
|
||
| /** | ||
| * The compile daemon died and is being respawned. | ||
| * | ||
| * @property runningGeneration the generation the proxy app keeps running through the outage - | ||
| * its process is untouched. | ||
| * @property restartFailed the respawn did not stick and nothing is retrying it, so the surface | ||
| * must name the gesture that brings the compiler back rather than claim a restart is in | ||
| * progress; see [QuickBuildSessionState.Degraded.restartFailed]. | ||
| */ | ||
| data class Reconnecting( | ||
| val runningGeneration: Long, | ||
| val restartFailed: Boolean = false, | ||
| ) : QuickBuildStatus | ||
|
|
||
| companion object { | ||
| /** | ||
| * Maps a session state to the one status that represents it. | ||
| * | ||
| * @param state the current session state; every state maps, so no caller has to handle a | ||
| * missing status. | ||
| * @return the status to render, [Hidden] when the surface should show nothing. | ||
| */ | ||
| fun from(state: QuickBuildSessionState): QuickBuildStatus = | ||
| when (state) { | ||
| is QuickBuildSessionState.Idle -> { | ||
| Hidden(state.lastStartFailed) | ||
| } | ||
|
|
||
| // A warm-up the user never asked for stays invisible - but it must not clear a | ||
| // failed-start tone on its way through, so the flag rides along. | ||
| is QuickBuildSessionState.Prebuilding -> { | ||
| // A warm build has no baseline to replace, so a tap that queues on one is | ||
| // always a session's first provision. | ||
| if (state.tapQueued) Provisioning() else Hidden(state.lastStartFailed) | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Provisioning -> { | ||
| Provisioning(state.rebaselineReason) | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Ready -> { | ||
| state.lastFailure?.let { Failed(state.generation, it) } | ||
| ?: UpToDate(state.generation, buildDurationMillis = null) | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Building -> { | ||
| when { | ||
| // A real build: the proxy app is one generation behind, say so. | ||
| !state.warmingCompiler -> Building(state.deployedGeneration) | ||
|
|
||
| // A crash of the running generation surfaces immediately, exactly as it | ||
| // would outside the warm-compile window. | ||
| state.pendingCrash != null -> Failed(state.deployedGeneration, state.pendingCrash) | ||
|
|
||
| // The warm compile recompiles what already runs and deploys nothing, | ||
| // so the app is genuinely up to date for its whole window. | ||
| else -> UpToDate(state.deployedGeneration, buildDurationMillis = null) | ||
| } | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Deployed -> { | ||
| UpToDate(state.generation, state.buildDurationMillis, state.restarted) | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Invalidated -> { | ||
| NeedsFullBuild(state.reason, state.deployedGeneration, state.awaitingRetry) | ||
| } | ||
|
|
||
| is QuickBuildSessionState.Degraded -> { | ||
| Reconnecting(state.deployedGeneration, state.restartFailed) | ||
| } | ||
| } | ||
| } | ||
| } |
70 changes: 70 additions & 0 deletions
70
...uild/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/QuickBuildTone.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package org.appdevforall.cotg.quickbuild.domain.session | ||
|
|
||
| /** | ||
| * Colorblind-safe presentation tone for the Quick Build toolbar icon. | ||
| * | ||
| * Status is never carried by color alone: each tone maps to a distinct icon shape as well as a | ||
| * distinct color. The app module owns that drawable/color mapping because it needs a Context; | ||
| * this type is the JVM-testable half. | ||
| * | ||
| * Only [ERROR] is colored as a failure - a tone the user cannot act on, or that resolves by itself | ||
| * (a full rebuild during ordinary editing, a daemon respawn), must not read as one. | ||
| */ | ||
| enum class QuickBuildTone { | ||
| /** Ready to build - no session, or a session sitting on a successful build. */ | ||
| READY, | ||
|
|
||
| /** A build is running (provisioning or an active quick build). Tapping stops it. */ | ||
| BUILDING, | ||
|
|
||
| /** The next build cannot take the fast path and will be a full one. Not a failure. */ | ||
| SLOW, | ||
|
|
||
| /** The compile daemon is being respawned. Transient, resolves itself, nothing to do. */ | ||
| RECONNECTING, | ||
|
|
||
| /** A failure the user has to deal with. */ | ||
| ERROR, | ||
| } | ||
|
|
||
| /** | ||
| * Derives the toolbar tone from the status the session surface already exposes. | ||
| * | ||
| * @receiver the status currently rendered, so tone and status can never disagree. | ||
| * @return the tone for that status; [QuickBuildTone.READY] also covers a plain | ||
| * [QuickBuildStatus.Hidden], where the icon is present but no session is running. | ||
| */ | ||
| fun QuickBuildStatus.toTone(): QuickBuildTone = | ||
| when (this) { | ||
| // A failed START is a failure the user has to deal with - only a tap retries it - so | ||
| // it must not settle back to the green bolt the moment the failure flash fades. | ||
| is QuickBuildStatus.Hidden -> { | ||
| if (lastStartFailed) QuickBuildTone.ERROR else QuickBuildTone.READY | ||
| } | ||
|
|
||
| is QuickBuildStatus.UpToDate -> { | ||
| QuickBuildTone.READY | ||
| } | ||
|
|
||
| is QuickBuildStatus.Provisioning, | ||
| is QuickBuildStatus.Building, | ||
| -> { | ||
| QuickBuildTone.BUILDING | ||
| } | ||
|
|
||
| // A rebaseline that failed and parked is not ordinary upcoming work: nothing moves | ||
| // until the user acts, which is exactly what ERROR means here. | ||
| is QuickBuildStatus.NeedsFullBuild -> { | ||
| if (awaitingRetry) QuickBuildTone.ERROR else QuickBuildTone.SLOW | ||
| } | ||
|
|
||
| // A respawn that failed is not invisible work resolving itself: the compiler is down | ||
| // until the user taps, which is exactly what ERROR means here. | ||
| is QuickBuildStatus.Reconnecting -> { | ||
| if (restartFailed) QuickBuildTone.ERROR else QuickBuildTone.RECONNECTING | ||
| } | ||
|
|
||
| is QuickBuildStatus.Failed -> { | ||
| QuickBuildTone.ERROR | ||
| } | ||
| } |
74 changes: 74 additions & 0 deletions
74
...ld/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # `domain/session/` - the session state machine | ||
|
|
||
| Pure-JVM state machine for a quick-build session: its states, the events that drive them, the effects the shell must run, and what the user is told. No Android. `SessionReducer.reduce` is total - an unhandled (state, event) pair keeps the state and emits no effects, so a late or duplicate event can never corrupt the session. `QuickBuildStatus` and `QuickBuildTone` derive purely from state, so a stuck banner or a wrong icon color is unrepresentable. | ||
|
|
||
| | File | Purpose | | ||
| | --- | --- | | ||
| | [`SessionReducer.kt`](SessionReducer.kt) | The total transition function: maps (state, event) to next state plus ordered effects. | | ||
| | [`QuickBuildSessionState.kt`](QuickBuildSessionState.kt) | The state sealed type plus `SessionFailure`, `SessionEvent`, `SessionEffect`, and `SessionTransition`. | | ||
| | [`QuickBuildStatus.kt`](QuickBuildStatus.kt) | The status surface derived from state via `from(state)`. | | ||
| | [`QuickBuildTone.kt`](QuickBuildTone.kt) | The colorblind-safe toolbar tone derived from status via `toTone()`. | | ||
| | [`QuickBuildNotice.kt`](QuickBuildNotice.kt) | Enum of host-shown notices (named, not written, since this module has no `R`), each carrying its own tone. | | ||
| | [`QuickBuildMessage.kt`](QuickBuildMessage.kt) | Sealed type of named failure messages the host maps to string resources; `Literal` passes final text through. | | ||
|
|
||
| ## State machine | ||
|
|
||
| This is the authoritative rendering: every transition with a guard, drawn in full. The copies in [quickbuild/README.md](../../../../../../../../../../README.md) and [docs/pipeline.md](../../../../../../../../../../docs/pipeline.md) are deliberately simplified for orientation. | ||
|
|
||
| Arrows are labeled with the `SessionEvent` that drives them; parentheticals note the guard or a key effect. Self-loops that only run an effect (a tap that triggers a live reload, a retry that kicks off a rebuild) are shown; pure no-ops are not. | ||
|
|
||
| ```mermaid | ||
| stateDiagram-v2 | ||
| [*] --> Idle | ||
|
|
||
| Idle --> Provisioning: QuickBuildTapped | ||
| Idle --> Prebuilding: PrebuildRequested | ||
|
|
||
| Prebuilding --> Prebuilding: QuickBuildTapped (queue the tap) | ||
| Prebuilding --> Provisioning: PrebuildFinished (tap queued) | ||
| Prebuilding --> Idle: PrebuildFinished (no tap) | ||
| Prebuilding --> Idle: CancelRequested (tap queued) | ||
|
|
||
| Provisioning --> Ready: ProvisioningSucceeded | ||
| Provisioning --> Idle: ProvisioningFailed | ||
| Provisioning --> Idle: CancelRequested | ||
| Provisioning --> Invalidated: ProxyAppRebuildInstallNotConfirmed | ||
| Provisioning --> Invalidated: ProxyAppRebuildDeferred | ||
|
|
||
| Ready --> Ready: QuickBuildTapped (TriggerLiveReload) | ||
| Ready --> Building: BuildStarted | ||
| Ready --> Building: WarmCompileStarted | ||
| Ready --> Invalidated: InvalidationDetected | ||
| Ready --> Degraded: DaemonDied | ||
| Ready --> Ready: ProxyAppCrashed (record failure) | ||
| Ready --> Ready: ExternalBuildCompleted (RefreshBaseline) | ||
|
|
||
| Building --> Deployed: BuildSucceeded | ||
| Building --> Ready: BuildFailed | ||
| Building --> Ready: CancelRequested (not warming) | ||
| Building --> Ready: WarmCompileFinished | ||
| Building --> Invalidated: InvalidationDetected | ||
| Building --> Degraded: DaemonDied | ||
|
|
||
| Deployed --> Deployed: QuickBuildTapped (TriggerLiveReload) | ||
| Deployed --> Building: BuildStarted | ||
| Deployed --> Building: WarmCompileStarted | ||
| Deployed --> Invalidated: InvalidationDetected | ||
| Deployed --> Degraded: DaemonDied | ||
| Deployed --> Ready: ProxyAppCrashed (record failure) | ||
| Deployed --> Deployed: ExternalBuildCompleted (RefreshBaseline) | ||
|
|
||
| Invalidated --> Provisioning: ProxyAppRebuildStarted | ||
| Invalidated --> Invalidated: QuickBuildTapped / HostForegrounded (RunProxyAppRebuild) | ||
|
|
||
| Degraded --> Ready: DaemonRespawned | ||
| Degraded --> Invalidated: InvalidationDetected | ||
| Degraded --> Degraded: ExternalBuildCompleted (RefreshBaseline) | ||
|
|
||
| note right of Idle | ||
| SessionRestartRequested from any | ||
| non-Idle state -> Idle (TeardownSession) | ||
| end note | ||
| ``` | ||
|
|
||
| The reducer is total: any (state, event) pair not drawn above keeps the current state and emits no effects. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the session-state diagram or soften its claim of being authoritative and complete. The current reducer includes additional transitions, especially the build-result and retry paths from Invalidated and Degraded, that are not shown and can mislead readers about supported session behavior.
📍 Affects 1 file
quickbuild/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/README.md#L16-L18(this comment)quickbuild/core/src/main/java/org/appdevforall/cotg/quickbuild/domain/session/README.md#L16-L16🤖 Prompt for AI Agents
Source: Coding guidelines