87 lines
2.6 KiB
Go
87 lines
2.6 KiB
Go
package mtprotoedge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
rpcReplayRestoreTimeout = 5 * time.Second
|
|
rpcReplayRestoreMaxActive = 8
|
|
)
|
|
|
|
// rpcReplayRestoreSlots bounds callbacks that ignore their own store/context
|
|
// deadline. The caller waits only until ctx (and at most
|
|
// rpcReplayRestoreTimeout); a stuck callback can retain one of these fixed
|
|
// slots, but it cannot retain a rewrap worker, a live Conn scheduler barrier,
|
|
// or create an unbounded goroutine population.
|
|
var rpcReplayRestoreSlots = make(chan struct{}, rpcReplayRestoreMaxActive)
|
|
|
|
func boundedRPCReplayRestoreContext(parent context.Context) (context.Context, context.CancelFunc) {
|
|
if parent == nil {
|
|
parent = context.Background()
|
|
}
|
|
return context.WithTimeout(parent, rpcReplayRestoreTimeout)
|
|
}
|
|
|
|
// runBoundedRPCReplayRestore runs replacement metadata first and the shared
|
|
// logical delivery hook second. On timeout it fences the physical generation
|
|
// before returning, so releasing that Conn's scheduler barrier cannot expose a
|
|
// following RPC to partially restored state. A claim that has not entered the
|
|
// hook is abandoned and may be acquired by a later replacement replay; an
|
|
// in-progress hook remains at-most-once and every later replay waits for Done.
|
|
func (s *Server) runBoundedRPCReplayRestore(
|
|
ctx context.Context,
|
|
c *Conn,
|
|
source string,
|
|
claim *rpcResultDeliveryHookClaim,
|
|
replacement func() error,
|
|
) error {
|
|
if claim == nil && replacement == nil {
|
|
return nil
|
|
}
|
|
boundedCtx, cancel := boundedRPCReplayRestoreContext(ctx)
|
|
defer cancel()
|
|
|
|
select {
|
|
case rpcReplayRestoreSlots <- struct{}{}:
|
|
case <-boundedCtx.Done():
|
|
claim.abandon()
|
|
if c != nil {
|
|
c.fenceUndeliveredRPCResult()
|
|
}
|
|
return fmt.Errorf("restore replay state after %s: %w", source, boundedCtx.Err())
|
|
}
|
|
|
|
result := make(chan error, 1)
|
|
var logical func()
|
|
if claim != nil {
|
|
logical = claim.run
|
|
}
|
|
go func() {
|
|
defer func() { <-rpcReplayRestoreSlots }()
|
|
result <- s.runRPCReplayRestore(c, source, composeRPCReplayRestore(logical, replacement))
|
|
}()
|
|
|
|
select {
|
|
case err := <-result:
|
|
return err
|
|
case <-boundedCtx.Done():
|
|
// Prefer a restore that reached its terminal state concurrently with the
|
|
// deadline; its result channel publishes coordinator Done before send.
|
|
select {
|
|
case err := <-result:
|
|
return err
|
|
default:
|
|
}
|
|
// If replacement metadata is still blocked, the hook is only Claimed and
|
|
// can be safely re-acquired. Once hook execution is InProgress, abandon
|
|
// deliberately fails: retrying an unknown partial side effect is forbidden.
|
|
claim.abandon()
|
|
if c != nil {
|
|
c.fenceUndeliveredRPCResult()
|
|
}
|
|
return fmt.Errorf("restore replay state after %s: %w", source, boundedCtx.Err())
|
|
}
|
|
}
|