Skip to main content

RemoteStateProvider

Keeps state in Octo's managed backend, so a team shares one store and no machine holds the only copy.

A run reads and writes a state directory on disk through LocalStateProvider, which archives each run there when its lock is released, and the backend is kept in step with it:

  • RemoteStateProvider.refreshState brings the directory up to the newest sealed state, and is what octo generate plans against.
  • RemoteStateProvider.lockApp opens a new record for the run, refusing when the newest sealed state has moved since the directory was refreshed.
  • Every write under the lock lands in the directory and in the run's record before it returns, so a run that dies leaves a record somebody can finish from another machine.
  • RemoteStateProvider.unlockApp seals the record and releases the lock.

Two files of the provider's own sit beside the state files and are never uploaded: remote-state.lock records the run the directory belongs to and is deleted when the lock is released, and remote-state-upload.lock makes the octo processes of one machine write to the backend one at a time and is deleted after every write.

@example
await octo.initialize(
new RemoteStateProvider({
appId: 'my-app',
localStateDirectoryPath: './.octo',
organizationId: 'my-organization',
sessionId: process.env.OCTO_SESSION!,
stateManagementServiceUrl: 'https://octo.quadnix.com/api/v1/state',
}),
);
@group

Services/State Management

@see

Techniques for State Providers.

Implements

Index

Constructors

constructor

  • RemoteStateProvider(options: { appId: string; localStateDirectoryPath: string; lockContext?: string; organizationId: string; sessionId: string; stateManagementServiceUrl: string }): RemoteStateProvider
  • Parameters

    • options: { appId: string; localStateDirectoryPath: string; lockContext?: string; organizationId: string; sessionId: string; stateManagementServiceUrl: string }

      organizationId and appId name the app whose state this is, and sessionId is the session the run acts as. stateManagementServiceUrl is the backend service, including the prefix its routes sit behind. localStateDirectoryPath is the directory state files are written into. lockContext is what a blocked run is shown about the holder.

      • appId: string
      • localStateDirectoryPath: string
      • optionallockContext: string
      • organizationId: string
      • sessionId: string
      • stateManagementServiceUrl: string

    Returns RemoteStateProvider

Methods

getAppLock

  • getAppLock(): Promise<string | undefined>
  • The id of the lock currently held, or undefined when the app is unlocked.


    Returns Promise<string | undefined>

getSealedStates

  • getSealedStates(stateFileNames: string[]): Promise<(Buffer<ArrayBufferLike> | undefined)[]>
  • Reads state files from the newest sealed state, bringing the directory up to it first, and holding the upload lock throughout so that nothing replaces the directory between the reads.


    Parameters

    • stateFileNames: string[]

    Returns Promise<(Buffer<ArrayBufferLike> | undefined)[]>

getState

  • getState(stateFileName: string): Promise<Buffer<ArrayBufferLike>>
  • Reads a state file.

    @throws

    TransactionError With the message No state found! when the file does not exist.


    Parameters

    • stateFileName: string

    Returns Promise<Buffer<ArrayBufferLike>>

getTemporaryFilePath

  • getTemporaryFilePath(name: string): Promise<string>

isAppLocked

  • isAppLocked(lockId: string): Promise<boolean>
  • Whether lockId is the lock currently held.

    @throws

    TransactionError With the message Invalid lock ID! when a different lock is held, and when lockId is held but was neither taken nor resumed in this state directory.


    Parameters

    • lockId: string

    Returns Promise<boolean>

listStates

  • listStates(prefix: string): Promise<string[]>
  • The names of the state files whose name starts with prefix.


    Parameters

    • prefix: string

    Returns Promise<string[]>

lockApp

  • lockApp(lock: { commitId: string } | { lockId: string }): Promise<{ commitId?: string; lockId: string; resumedFrom?: directory | run | sealed }>
  • Takes the app lock and opens the run's record with everything the state directory holds.

    With lockId, resumes the run already holding that lock instead - from any machine, as the account that took it or an organization administrator. The state directory is replaced with what that run wrote so far, its journal included, keeping this machine's journal files, which join the record, and every write after lands in its record. A run that wrote nothing is resumed from the newest sealed state, without that state's journal.

    @throws

    AppLockError When a lock is held, naming the id that holds it. When the newest sealed state moved since RemoteStateProvider.refreshState last ran. When lockId is given and is not held.


    Parameters

    • lock: { commitId: string } | { lockId: string }
      • commitId: string
      • lockId: string

    Returns Promise<{ commitId?: string; lockId: string; resumedFrom?: directory | run | sealed }>

refreshState

  • refreshState(): Promise<{ commitId?: string; previousCommitId?: string; replaced: boolean }>
  • Brings the state directory up to the newest sealed state, which is what a run plans against.

    A directory an open run is still writing is left alone. A directory the backend holds nothing for is left alone too: that is a first run, or a project arriving from another provider, and the files on disk are the only copy there is. Journal files are neither replaced nor taken.


    Returns Promise<{ commitId?: string; previousCommitId?: string; replaced: boolean }>

saveStates

  • saveStates(stateFiles: ({ data: Buffer<ArrayBufferLike> | undefined; stateFileName: string } | { filePath: string; stateFileName: string })[]): Promise<void>
  • Writes state files, deleting each one given no data, and then writes the run's record once for all of them when a run is open.

    @throws

    TransactionError When the backend would refuse a file's name, before anything is written. When the backend refuses the write, which is where the limits it holds the app to apply.


    Parameters

    • stateFiles: ({ data: Buffer<ArrayBufferLike> | undefined; stateFileName: string } | { filePath: string; stateFileName: string })[]

    Returns Promise<void>

unlockApp

  • unlockApp(lockId: string): Promise<void>
  • Writes the directory into the run's record, seals it, and releases the app lock, then archives the directory with LocalStateProvider.archiveState when it holds that run. Unlocking an app that is not locked is not an error.

    A write that failed earlier left the directory ahead of the record, so the record is written once more before it is sealed: a sealed record is what every machine plans against next. The archive is this machine's history, so a failure to write it is reported on stderr rather than thrown.

    @throws

    TransactionError With the message Invalid lock ID! when a different lock is held. When the directory cannot be written into the record, keeping the lock held.


    Parameters

    • lockId: string

    Returns Promise<void>