ActivityPub background actor validation, update and cleanup process (#4765)
* feat(ap): add validation+update+cleanup AP actor job * fix(test): use time.Time instead of sql.NullTime * feat(test): add integration test for AP follower cleanup/validation job
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/activitypub/persistence/followersrepository"
|
||||
"github.com/owncast/owncast/activitypub/resolvers"
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// ValidationInterval is how often the validation job runs.
|
||||
ValidationInterval = 1 * time.Hour
|
||||
// FollowersPerRun is how many followers to validate per job run.
|
||||
FollowersPerRun = 5
|
||||
// FailureDurationThreshold is how long a follower must be unreachable before removal.
|
||||
FailureDurationThreshold = 7 * 24 * time.Hour // 7 days
|
||||
// DelayBetweenFollowers is the delay between validating individual followers.
|
||||
DelayBetweenFollowers = 2 * time.Second
|
||||
)
|
||||
|
||||
// GetValidationInterval returns the configured validation interval, or the default.
|
||||
func GetValidationInterval() time.Duration {
|
||||
if config.FollowerValidationInterval > 0 {
|
||||
return config.FollowerValidationInterval
|
||||
}
|
||||
return ValidationInterval
|
||||
}
|
||||
|
||||
// StartFollowerValidationJob starts the background job that periodically validates followers.
|
||||
func StartFollowerValidationJob() {
|
||||
interval := GetValidationInterval()
|
||||
ticker := time.NewTicker(interval)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
runFollowerValidation()
|
||||
}
|
||||
}()
|
||||
log.Debugf("Follower validation job scheduled with interval %v", interval)
|
||||
}
|
||||
|
||||
func runFollowerValidation() {
|
||||
configRepo := configrepository.Get()
|
||||
if !configRepo.GetFederationEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
followersRepo := followersrepository.Get()
|
||||
followers, err := followersRepo.GetFollowersToValidate(FollowersPerRun)
|
||||
if err != nil {
|
||||
log.Errorln("Failed to get followers for validation:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, follower := range followers {
|
||||
validateAndUpdateFollower(followersRepo, follower)
|
||||
time.Sleep(DelayBetweenFollowers)
|
||||
}
|
||||
}
|
||||
|
||||
func validateAndUpdateFollower(repo followersrepository.FollowersRepository, follower models.Follower) {
|
||||
resolvedActor, err := resolvers.GetResolvedActorFromIRI(follower.ActorIRI)
|
||||
if err != nil {
|
||||
handleValidationFailure(repo, follower, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Success - clear failure timestamp and update data
|
||||
if err := repo.UpdateFollowerValidationSuccess(follower.ActorIRI); err != nil {
|
||||
log.Errorln("Failed to update validation success:", err)
|
||||
}
|
||||
|
||||
// Update follower data
|
||||
if err := repo.Update(
|
||||
resolvedActor.ActorIriString(),
|
||||
resolvedActor.InboxString(),
|
||||
resolvedActor.SharedInboxString(),
|
||||
resolvedActor.Name,
|
||||
resolvedActor.FullUsername,
|
||||
resolvedActor.ImageString(),
|
||||
); err != nil {
|
||||
log.Errorln("Failed to update follower data:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleValidationFailure(repo followersrepository.FollowersRepository, follower models.Follower, resolveErr error) {
|
||||
log.Debugf("Follower validation failed for %s: %v", follower.ActorIRI, resolveErr)
|
||||
|
||||
// Check removal eligibility BEFORE updating failure timestamp.
|
||||
// We use the existing FirstValidationFailureAt from the database to determine
|
||||
// if this follower has been failing long enough to be removed.
|
||||
// If FirstValidationFailureAt is not set, this is a new failure and we just record it.
|
||||
shouldRemove := false
|
||||
if follower.FirstValidationFailureAt.Valid {
|
||||
failureDuration := time.Since(follower.FirstValidationFailureAt.Time)
|
||||
shouldRemove = failureDuration >= FailureDurationThreshold
|
||||
}
|
||||
|
||||
// Update failure timestamp (sets first_validation_failure_at if not already set)
|
||||
if err := repo.UpdateFollowerValidationFailure(follower.ActorIRI); err != nil {
|
||||
log.Errorln("Failed to update validation failure:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove follower if they've exceeded the failure threshold
|
||||
if shouldRemove {
|
||||
failureDuration := time.Since(follower.FirstValidationFailureAt.Time)
|
||||
log.Infof("Removing follower %s after %v of consecutive failures",
|
||||
follower.ActorIRI, failureDuration.Round(time.Hour))
|
||||
if err := repo.RemoveByIRI(follower.ActorIRI); err != nil {
|
||||
log.Errorln("Failed to remove invalid follower:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user