mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-17 08:29:04 +02:00
FRANCIS_HOST decides where the Francis actor runtime lives. When it is empty or set to "embedded" (the default), Pocket ID starts the runtime inside its own process, backed by its own database. Any other value is the address, or a comma-separated list of addresses, of a standalone Francis runtime. Pocket ID then connects to it as a remote actor host and starts no embedded runtime. Note: connecting to a standalone runtime also needs FRANCIS_HOST_PSK or FRANCIS_HOST_JWT, and optionally (but recommended) FRANCIS_CA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMgoTZtznSjP4SHTaHbRen
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
francishost "github.com/italypaleale/francis/host"
|
|
"github.com/italypaleale/francis/host/remote"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
)
|
|
|
|
// actorClientConnectTimeout bounds how long a CLI command waits to join the cluster
|
|
// Francis reconnects to the runtime indefinitely, which is right for the server but would leave a command hanging against an unreachable runtime
|
|
const actorClientConnectTimeout = 30 * time.Second
|
|
|
|
// WithActorClient connects to the standalone Francis runtime, calls fn once the connection is live, and disconnects before returning.
|
|
// It's meant for CLI commands, which have no actor host of their own: the client joins the cluster only for the duration of fn, and hosts no actor while connected, so the runtime never places an actor on it.
|
|
// It requires FRANCIS_HOST to point to a standalone runtime, and returns ErrEmbeddedFrancisRuntime otherwise, since an embedded runtime is reached through the database instead.
|
|
func WithActorClient(parentCtx context.Context, envConfig *common.EnvConfigSchema, fn func(ctx context.Context, client francishost.Host) error) error {
|
|
if envConfig.HasEmbeddedFrancisRuntime() {
|
|
return ErrEmbeddedFrancisRuntime
|
|
}
|
|
|
|
log := slog.Default().With("scope", "actor-client")
|
|
|
|
// The client hosts no actor, so it advertises no address of its own and binds nothing
|
|
// The short grace period keeps a command from lingering on the way out, since there are no actors to drain
|
|
client, err := remote.NewHost(append(
|
|
remoteConnectionOptions(envConfig, log),
|
|
remote.WithClientOnly(),
|
|
remote.WithShutdownGracePeriod(2*time.Second),
|
|
)...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create the actor client: %w", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(parentCtx)
|
|
defer cancel()
|
|
|
|
runErrCh := make(chan error, 1)
|
|
go func() {
|
|
runErrCh <- client.Run(ctx)
|
|
}()
|
|
|
|
// Every operation travels on the runtime session, so nothing can run before the client has joined the cluster
|
|
connectCtx, connectCancel := context.WithTimeout(ctx, actorClientConnectTimeout)
|
|
defer connectCancel()
|
|
|
|
select {
|
|
case <-client.Ready():
|
|
case runErr := <-runErrCh:
|
|
return fmt.Errorf("failed to connect to the Francis runtime: %w", runErr)
|
|
case <-connectCtx.Done():
|
|
cancel()
|
|
<-runErrCh
|
|
return fmt.Errorf("timed out connecting to the Francis runtime after %v", actorClientConnectTimeout)
|
|
}
|
|
|
|
fnErr := fn(ctx, client)
|
|
|
|
// Leave the cluster before returning, so the runtime drops the registration instead of waiting for the health check to lapse
|
|
cancel()
|
|
runErr := <-runErrCh
|
|
|
|
// The error from fn is the one the caller asked for, and a canceled run is just the disconnect we asked for
|
|
switch {
|
|
case fnErr != nil:
|
|
return fnErr
|
|
case runErr != nil && !errors.Is(runErr, context.Canceled):
|
|
return fmt.Errorf("error disconnecting from the Francis runtime: %w", runErr)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|