Skip to content

Commit

Permalink
add error status (#2746)
Browse files Browse the repository at this point in the history
* add error status

* update types

* purge the void̴͉̀

* fix some extra voids

* add `package.rebuild`

* introduce error status and pkg rebuild and fix mocks

* minor fixes

* fix build

---------

Co-authored-by: Matt Hill <[email protected]>
  • Loading branch information
dr-bonez and MattDHill authored Sep 27, 2024
1 parent db06951 commit e7fa94c
Show file tree
Hide file tree
Showing 49 changed files with 638 additions and 409 deletions.
4 changes: 2 additions & 2 deletions container-runtime/src/Adapters/EffectCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export function makeEffects(context: EffectContext): Effects {
T.Effects["subcontainer"]["createFs"]
>
},
destroyFs(options: { guid: string }): Promise<void> {
destroyFs(options: { guid: string }): Promise<null> {
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
T.Effects["subcontainer"]["destroyFs"]
>
Expand Down Expand Up @@ -284,7 +284,7 @@ export function makeEffects(context: EffectContext): Effects {
>
},

setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
return rpcRound("set-main-status", o) as ReturnType<
T.Effects["setHealth"]
>
Expand Down
1 change: 1 addition & 0 deletions container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export class SystemForStartOs implements System {
const started = async (onTerm: () => Promise<void>) => {
await effects.setMainStatus({ status: "running" })
mainOnTerm = onTerm
return null
}
const daemons = await (
await this.abi.main({
Expand Down
8 changes: 8 additions & 0 deletions core/models/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,14 @@ impl Debug for ErrorData {
}
}
impl std::error::Error for ErrorData {}
impl From<Error> for ErrorData {
fn from(value: Error) -> Self {
Self {
details: value.to_string(),
debug: format!("{:?}", value),
}
}
}
impl From<&RpcError> for ErrorData {
fn from(value: &RpcError) -> Self {
Self {
Expand Down
18 changes: 11 additions & 7 deletions core/startos/src/disk/mount/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ use tracing::instrument;
use crate::util::Invoke;
use crate::Error;

pub async fn is_mountpoint(path: impl AsRef<Path>) -> Result<bool, Error> {
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(path.as_ref())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await?;
Ok(is_mountpoint.success())
}

#[instrument(skip_all)]
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
src: P0,
Expand All @@ -16,13 +26,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
src.as_ref().display(),
dst.as_ref().display()
);
let is_mountpoint = tokio::process::Command::new("mountpoint")
.arg(dst.as_ref())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await?;
if is_mountpoint.success() {
if is_mountpoint(&dst).await? {
unmount(dst.as_ref(), true).await?;
}
tokio::fs::create_dir_all(&src).await?;
Expand Down
7 changes: 7 additions & 0 deletions core/startos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand(
"rebuild",
from_fn_async(service::rebuild)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand("logs", logs::package_logs())
.subcommand(
"logs",
Expand Down
3 changes: 2 additions & 1 deletion core/startos/src/lxc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ impl LxcManager {
Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"),
true,
)
.await?;
.await
.log_err();
if tokio_stream::wrappers::ReadDirStream::new(
tokio::fs::read_dir(&rootfs_path).await?,
)
Expand Down
9 changes: 9 additions & 0 deletions core/startos/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,15 @@ impl ServiceActorSeed {
}
}

#[derive(Deserialize, Serialize, Parser, TS)]
pub struct RebuildParams {
pub id: PackageId,
}
pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Result<(), Error> {
ctx.services.load(&ctx, &id, LoadDisposition::Retry).await?;
Ok(())
}

#[derive(Deserialize, Serialize, Parser, TS)]
pub struct ConnectParams {
pub id: PackageId,
Expand Down
28 changes: 26 additions & 2 deletions core/startos/src/service/service_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use futures::{Future, FutureExt};
use helpers::NonDetachingJoinHandle;
use imbl::OrdMap;
use imbl_value::InternedString;
use models::ErrorData;
use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
use tracing::instrument;

Expand All @@ -22,6 +23,7 @@ use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressT
use crate::s9pk::manifest::PackageId;
use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk;
use crate::service::start_stop::StartStop;
use crate::service::{LoadDisposition, Service, ServiceRef};
use crate::status::MainStatus;
use crate::util::serde::Pem;
Expand Down Expand Up @@ -87,8 +89,30 @@ impl ServiceMap {
if let Some(service) = service.take() {
shutdown_err = service.shutdown().await;
}
// TODO: retry on error?
*service = Service::load(ctx, id, disposition).await?.map(From::from);
match Service::load(ctx, id, disposition).await {
Ok(s) => *service = s.into(),
Err(e) => {
let e = ErrorData::from(e);
ctx.db
.mutate(|db| {
if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) {
pde.as_status_mut().map_mutate(|s| {
Ok(MainStatus::Error {
on_rebuild: if s.running() {
StartStop::Start
} else {
StartStop::Stop
},
message: e.details,
debug: Some(e.debug),
})
})?;
}
Ok(())
})
.await?;
}
}
shutdown_err?;
Ok(())
}
Expand Down
16 changes: 15 additions & 1 deletion core/startos/src/status/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub mod health_check;
#[serde(rename_all = "camelCase")]
#[serde(rename_all_fields = "camelCase")]
pub enum MainStatus {
Error {
on_rebuild: StartStop,
message: String,
debug: Option<String>,
},
Stopped,
Restarting,
Restoring,
Expand All @@ -43,12 +48,20 @@ impl MainStatus {
| MainStatus::Restarting
| MainStatus::BackingUp {
on_complete: StartStop::Start,
}
| MainStatus::Error {
on_rebuild: StartStop::Start,
..
} => true,
MainStatus::Stopped
| MainStatus::Restoring
| MainStatus::Stopping { .. }
| MainStatus::BackingUp {
on_complete: StartStop::Stop,
}
| MainStatus::Error {
on_rebuild: StartStop::Stop,
..
} => false,
}
}
Expand All @@ -70,7 +83,8 @@ impl MainStatus {
| MainStatus::Stopped
| MainStatus::Restoring
| MainStatus::Stopping { .. }
| MainStatus::Restarting => None,
| MainStatus::Restarting
| MainStatus::Error { .. } => None,
}
}
}
36 changes: 18 additions & 18 deletions sdk/base/lib/Effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ export type Effects = {
constRetry: () => void
clearCallbacks: (
options: { only: number[] } | { except: number[] },
) => Promise<void>
) => Promise<null>

// action
action: {
/** Define an action that can be invoked by a user or service */
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<void>
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<null>
/** Remove all exported actions */
clear(options: { except: ActionId[] }): Promise<void>
clear(options: { except: ActionId[] }): Promise<null>
getInput(options: {
packageId?: PackageId
actionId: ActionId
Expand All @@ -50,23 +50,23 @@ export type Effects = {
}): Promise<ActionResult | null>
request<Input extends Record<string, unknown>>(
options: RequestActionParams,
): Promise<void>
): Promise<null>
clearRequests(
options: { only: ActionId[] } | { except: ActionId[] },
): Promise<void>
): Promise<null>
}

// control
/** restart this service's main function */
restart(): Promise<void>
restart(): Promise<null>
/** stop this service's main function */
shutdown(): Promise<void>
shutdown(): Promise<null>
/** indicate to the host os what runstate the service is in */
setMainStatus(options: SetMainStatus): Promise<void>
setMainStatus(options: SetMainStatus): Promise<null>

// dependency
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
setDependencies(options: { dependencies: Dependencies }): Promise<void>
setDependencies(options: { dependencies: Dependencies }): Promise<null>
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
getDependencies(): Promise<DependencyRequirement[]>
/** Test whether current dependency requirements are satisfied */
Expand All @@ -86,11 +86,11 @@ export type Effects = {
/** Returns a list of the ids of all installed packages */
getInstalledPackages(): Promise<string[]>
/** grants access to certain paths in the store to dependents */
exposeForDependents(options: { paths: string[] }): Promise<void>
exposeForDependents(options: { paths: string[] }): Promise<null>

// health
/** sets the result of a health check */
setHealth(o: SetHealth): Promise<void>
setHealth(o: SetHealth): Promise<null>

// subcontainer
subcontainer: {
Expand All @@ -100,13 +100,13 @@ export type Effects = {
name: string | null
}): Promise<[string, string]>
/** A low level api used by SubContainer */
destroyFs(options: { guid: string }): Promise<void>
destroyFs(options: { guid: string }): Promise<null>
}

// net
// bind
/** Creates a host connected to the specified port with the provided options */
bind(options: BindParams): Promise<void>
bind(options: BindParams): Promise<null>
/** Get the port address for a service */
getServicePortForward(options: {
packageId?: PackageId
Expand All @@ -116,7 +116,7 @@ export type Effects = {
/** Removes all network bindings, called in the setupInputSpec */
clearBindings(options: {
except: { id: HostId; internalPort: number }[]
}): Promise<void>
}): Promise<null>
// host
/** Returns information about the specified host, if it exists */
getHostInfo(options: {
Expand All @@ -134,7 +134,7 @@ export type Effects = {
getContainerIp(): Promise<string>
// interface
/** Creates an interface bound to a specific host and port to show to the user */
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<void>
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<null>
/** Returns an exported service interface */
getServiceInterface(options: {
packageId?: PackageId
Expand All @@ -149,7 +149,7 @@ export type Effects = {
/** Removes all service interfaces */
clearServiceInterfaces(options: {
except: ServiceInterfaceId[]
}): Promise<void>
}): Promise<null>
// ssl
/** Returns a PEM encoded fullchain for the hostnames specified */
getSslCertificate: (options: {
Expand Down Expand Up @@ -178,10 +178,10 @@ export type Effects = {
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
path: StorePath
value: ExtractStore
}): Promise<void>
}): Promise<null>
}
/** sets the version that this service's data has been migrated to */
setDataVersion(options: { version: string }): Promise<void>
setDataVersion(options: { version: string }): Promise<null>
/** returns the version that this service's data has been migrated to */
getDataVersion(): Promise<string | null>

Expand Down
25 changes: 12 additions & 13 deletions sdk/base/lib/actions/setupActions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InputSpec } from "./input/builder"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
import * as T from "../types"
import { once } from "../util"

export type Run<
A extends
Expand Down Expand Up @@ -130,21 +131,19 @@ export class Actions<
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
return new Actions({ ...this.actions, [action.id]: action })
}
update(options: { effects: T.Effects }): Promise<void> {
const updater = async (options: { effects: T.Effects }) => {
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}
await options.effects.action.clear({ except: Object.keys(this.actions) })
async update(options: { effects: T.Effects }): Promise<null> {
options.effects = {
...options.effects,
constRetry: once(() => {
this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem
}),
}
const updaterCtx = { options }
updaterCtx.options = {
effects: {
...options.effects,
constRetry: () => updater(updaterCtx.options),
},
for (let action of Object.values(this.actions)) {
await action.exportMetadata(options)
}
return updater(updaterCtx.options)
await options.effects.action.clear({ except: Object.keys(this.actions) })

return null
}
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
return this.actions[actionId]
Expand Down
Loading

0 comments on commit e7fa94c

Please sign in to comment.