mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-09 19:29:04 +02:00
feat: add FRANCIS_HOST to connect to a standalone Francis runtime
FRANCIS_HOST decides where the Francis actor runtime lives. When set to "embedded" (the default), Pocket ID starts the runtime inside its own process. 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. Because when using a remote runtime, it's likewise not possible to enforce a single instance of Pocket ID is running at once, the env vars currently have the `EXPERIMENTAL_` prefix, are **undocumented**, and show a warning if used. Notes: - Connecting to a standalone runtime also needs FRANCIS_HOST_PSK or FRANCIS_HOST_JWT_FILE, and optionally (but recommended) FRANCIS_CA. - When connecting to a remote runtime, exporting Pocket ID data does not include the actor state, which will need to be backed up and restored separately
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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 is the timeout for 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 err = <-runErrCh:
|
||||
return fmt.Errorf("failed to connect to the Francis runtime: %w", err)
|
||||
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()
|
||||
err = <-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 err != nil && !errors.Is(err, context.Canceled):
|
||||
return fmt.Errorf("error disconnecting from the Francis runtime: %w", err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user