mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-09 03:09:04 +02:00
refactor: migrate LDAP sync to an actor (#1651)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
co-authored by
Kyle Mendell
parent
f8db1d8a86
commit
563c0f93a6
@@ -0,0 +1,130 @@
|
||||
package ldapsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/italypaleale/francis/actor"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
)
|
||||
|
||||
// The LdapSync singleton actor decides when the recurring LDAP synchronization runs.
|
||||
|
||||
// SyncActorType is the actor type for the LDAP sync actor
|
||||
const SyncActorType = "LdapSync"
|
||||
|
||||
const (
|
||||
// alarmSync is the name of the repeating alarm that runs the synchronization
|
||||
alarmSync = "sync"
|
||||
|
||||
// syncInterval is how often the synchronization runs, as the ISO8601 duration the alarm repetition expects
|
||||
// There's no jitter: the alarm is cluster-wide, so there are no replicas to spread apart
|
||||
syncInterval = "PT1H"
|
||||
|
||||
// Delay the initial sync by 5s
|
||||
initialSyncDelay = 5 * time.Second
|
||||
|
||||
// alarmTimeout bounds the alarm operations performed by the actor
|
||||
alarmTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// syncActor is the cluster-wide singleton that triggers the recurring LDAP synchronization
|
||||
type syncActor struct {
|
||||
log *slog.Logger
|
||||
service *Service
|
||||
appConfig appconfig.AppConfigResolver
|
||||
// scheduleDisabled removes the alarm instead of arming it, for environments that drive syncs explicitly
|
||||
scheduleDisabled bool
|
||||
client actor.Client[struct{}]
|
||||
}
|
||||
|
||||
// NewSyncActor returns the factory that allocates the LDAP sync actor
|
||||
func NewSyncActor(service *Service, appConfig appconfig.AppConfigResolver, scheduleDisabled bool) actor.Factory {
|
||||
return func(actorID string, actorService *actor.Service) actor.Actor {
|
||||
return &syncActor{
|
||||
log: slog.With(
|
||||
slog.String("scope", "actor"),
|
||||
slog.String("actorType", SyncActorType),
|
||||
),
|
||||
service: service,
|
||||
appConfig: appConfig,
|
||||
scheduleDisabled: scheduleDisabled,
|
||||
// The actor keeps no state of its own: the client is only used to manage the alarm
|
||||
client: actor.NewActorClient[struct{}](SyncActorType, actorID, actorService),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap implements actor.ActorBootstrapper
|
||||
// The host drives it on every startup, routed to the single owning host, so it must stay idempotent
|
||||
func (a *syncActor) Bootstrap(parentCtx context.Context, _ actor.Envelope) error {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, alarmTimeout)
|
||||
defer cancel()
|
||||
|
||||
// The schedule may have been enabled in a previous run, so make sure a leftover alarm doesn't keep firing
|
||||
if a.scheduleDisabled {
|
||||
err := a.client.DeleteAlarm(ctx, alarmSync)
|
||||
if err != nil && !errors.Is(err, actor.ErrAlarmNotFound) {
|
||||
return fmt.Errorf("error deleting the LDAP sync alarm: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setting the alarm replaces whatever is registered, which both restores an alarm that was lost and picks up a change to the interval
|
||||
// It's due right away (with a small delay) so the directory is synchronized as soon as the cluster starts, matching what the pre-actor scheduled job did
|
||||
err := a.client.SetAlarm(ctx, alarmSync, actor.AlarmProperties{
|
||||
DueTime: time.Now().Add(initialSyncDelay),
|
||||
Interval: syncInterval,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting the LDAP sync alarm: %w", err)
|
||||
}
|
||||
|
||||
a.log.DebugContext(parentCtx, "Registered the recurring LDAP sync alarm", slog.String("interval", syncInterval))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Alarm implements actor.ActorAlarm
|
||||
func (a *syncActor) Alarm(ctx context.Context, name string, _ actor.Envelope) error {
|
||||
if name != alarmSync {
|
||||
return fmt.Errorf("unsupported alarm '%s' for the %s actor", name, SyncActorType)
|
||||
}
|
||||
|
||||
a.sync(ctx)
|
||||
|
||||
// A failed sync never surfaces as an error: the framework would retry the occurrence and then delete the alarm once the attempts run out, which would stop the synchronization altogether
|
||||
// The next occurrence comes around on its own, exactly like the pre-actor scheduled job
|
||||
return nil
|
||||
}
|
||||
|
||||
// sync runs one synchronization, unless LDAP is disabled
|
||||
// It logs failures rather than returning them, since the alarm has nowhere useful to send the error
|
||||
func (a *syncActor) sync(ctx context.Context) {
|
||||
dbConfig, err := a.appConfig.GetConfig(ctx)
|
||||
if err != nil {
|
||||
a.log.ErrorContext(ctx, "Failed to load the app configuration, skipping the LDAP sync", slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
if !dbConfig.LdapEnabled.IsTrue() {
|
||||
a.log.DebugContext(ctx, "LDAP is disabled, skipping the sync")
|
||||
return
|
||||
}
|
||||
|
||||
a.log.InfoContext(ctx, "Starting the LDAP sync")
|
||||
start := time.Now()
|
||||
|
||||
err = a.service.SyncAll(ctx, dbConfig)
|
||||
if err != nil {
|
||||
a.log.ErrorContext(ctx, "LDAP sync failed, will try again on the next run", slog.Duration("duration", time.Since(start)), slog.Any("error", err))
|
||||
return
|
||||
}
|
||||
|
||||
a.log.InfoContext(ctx, "LDAP sync completed", slog.Duration("duration", time.Since(start)))
|
||||
}
|
||||
Reference in New Issue
Block a user