Files
opencloud/services/postprocessing/pkg/postprocessing/postprocessing.go
T
junkerderprovinz 654f0e8e20 fix(postprocessing): retry publishing events instead of killing the server
A single transient nats: timeout on event publish was fatal to the whole
single-binary server, with no retry despite the service already having
backoff machinery for step failures. Now retries with the same exponential
backoff, capped at half the ack wait so the source event isn't redelivered
to another worker mid-retry. Also stops acking the source event when
publish ultimately fails, so the upload resumes after restart instead of
staying orphaned, and no longer exits during graceful shutdown.
2026-08-19 11:23:46 +02:00

148 lines
4.1 KiB
Go

package postprocessing
import (
"math"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/opencloud/services/postprocessing/pkg/config"
"github.com/opencloud-eu/reva/v2/pkg/events"
)
// Postprocessing handles postprocessing of a file
type Postprocessing struct {
ID string
URL string
User *user.User
ImpersonatingUser *user.User
Filename string
Filesize uint64
ResourceID *provider.ResourceId
Steps []events.Postprocessingstep
Status Status
Failures int
InitiatorID string
Finished bool
StartTime time.Time
config config.Postprocessing
}
// Status is helper struct to show current postprocessing status
type Status struct {
CurrentStep events.Postprocessingstep
Outcome events.PostprocessingOutcome
}
// New returns a new postprocessing instance
func New(config config.Postprocessing) *Postprocessing {
return &Postprocessing{
config: config,
}
}
// Init is the first step of the postprocessing
func (pp *Postprocessing) Init(_ events.BytesReceived) any {
if len(pp.Steps) == 0 {
return pp.finished(events.PPOutcomeContinue)
}
return pp.step(pp.Steps[0])
}
// NextStep returns the next postprocessing step
func (pp *Postprocessing) NextStep(ev events.PostprocessingStepFinished) any {
switch ev.Outcome {
case events.PPOutcomeContinue:
return pp.next(ev.FinishedStep)
case events.PPOutcomeRetry:
pp.Failures++
if pp.Failures > pp.config.MaxRetries {
return pp.finished(events.PPOutcomeAbort)
}
return pp.retry()
default:
return pp.finished(ev.Outcome)
}
}
// CurrentStep returns the current postprocessing step
func (pp *Postprocessing) CurrentStep() any {
if pp.Status.CurrentStep == events.PPStepFinished {
return pp.finished(pp.Status.Outcome)
}
return pp.step(pp.Status.CurrentStep)
}
// Delay will sleep the configured time then continue
func (pp *Postprocessing) Delay(f func(next any)) {
next := pp.next(events.PPStepDelay)
go func() {
time.Sleep(pp.config.Delayprocessing)
f(next)
}()
}
// BackoffDuration calculates the duration for exponential backoff based on the number of failures.
func (pp *Postprocessing) BackoffDuration() time.Duration {
return BackoffDuration(pp.config.RetryBackoffDuration, pp.Failures)
}
// BackoffDuration calculates an exponential backoff duration from a base duration and the
// number of failures that happened so far. The first failure waits the base duration, every
// subsequent one waits twice as long as the previous one.
func BackoffDuration(base time.Duration, failures int) time.Duration {
if failures < 1 {
return 0
}
return base * time.Duration(math.Pow(2, float64(failures-1)))
}
func (pp *Postprocessing) next(current events.Postprocessingstep) any {
l := len(pp.Steps)
for i, s := range pp.Steps {
if s == current && i+1 < l {
return pp.step(pp.Steps[i+1])
}
}
return pp.finished(events.PPOutcomeContinue)
}
func (pp *Postprocessing) step(next events.Postprocessingstep) events.StartPostprocessingStep {
pp.Status.CurrentStep = next
return events.StartPostprocessingStep{
UploadID: pp.ID,
URL: pp.URL,
ExecutingUser: pp.User,
Filename: pp.Filename,
Filesize: pp.Filesize,
ResourceID: pp.ResourceID,
StepToStart: next,
ImpersonatingUser: pp.ImpersonatingUser,
}
}
func (pp *Postprocessing) finished(outcome events.PostprocessingOutcome) events.PostprocessingFinished {
pp.Status.CurrentStep = events.PPStepFinished
pp.Status.Outcome = outcome
return events.PostprocessingFinished{
UploadID: pp.ID,
ExecutingUser: pp.User,
Filename: pp.Filename,
Outcome: outcome,
ImpersonatingUser: pp.ImpersonatingUser,
}
}
func (pp *Postprocessing) retry() events.PostprocessingRetry {
pp.Status.Outcome = events.PPOutcomeRetry
return events.PostprocessingRetry{
UploadID: pp.ID,
ExecutingUser: pp.User,
Filename: pp.Filename,
Failures: pp.Failures,
BackoffDuration: pp.BackoffDuration(),
}
}