Files
onvif/event/stream/reconnect.go
Sebastian Norling de3f049a63 feat(event/stream): echo WS-Addressing ReferenceParameters for AXIS
AXIS encodes pull-point subscription identity in
<wsa:ReferenceParameters> inside the CreatePullPointSubscription
response — a generic /onvif/services endpoint plus a
<dom0:SubscriptionId> child — rather than a per-subscription URL.
We were discarding the ReferenceParameters and POSTing to the
generic endpoint, which AXIS rejected with ter:InvalidArgs on every
PullMessages/Renew/Unsubscribe.

Per WS-Addressing 1.0 §3.1 each reference parameter MUST be echoed
as a SOAP Header block carrying wsa:IsReferenceParameter="true".

  - subscriptionRef now carries Address + the verbatim
    ReferenceParameters inner XML extracted from the create response.
  - buildRefParamsHeader walks the children, adds the attribute, and
    produces the SOAP Header content.
  - pullMessages, renewPullPoint, unsubscribePullPoint switch from
    SendSoap to a new SendSoapWithHeader path on the caller interface.
  - onvif.Device gains SendSoapWithHeader as a thin variant of
    SendSoap (existing SendSoap is now a one-liner delegating to it
    with empty header content, so all external callers are unaffected).

Verified end-to-end against an AXIS camera at 192.168.1.10: pulls
now stream the full topic tree (VMD, Object Analytics, IO, storage,
hardware-failure topics) instead of looping on ter:InvalidArgs.
2026-05-27 18:07:56 +02:00

109 lines
2.9 KiB
Go

package stream
import (
"context"
"math/rand"
"time"
)
// maxRecreateBackoff caps exponential backoff between recreate
// attempts. Sized for fleet deployments: at 30s a 1000-camera setup
// recovering from a switch reboot would generate sustained
// reconnect traffic; 5 minutes lets the network settle.
const maxRecreateBackoff = 5 * time.Minute
// jitterFraction prevents thundering-herd reconnects when many
// cameras drop together (switch reboot, NAT timeout).
const jitterFraction = 0.25
// pullLoop runs PullMessages → decode → Events. After
// ReconnectAfterFailures consecutive errors it asks attemptRecreate
// to rebuild the subscription. The next batch's events carry
// AfterReconnect=true so consumers can suppress the Initialized
// replay ONVIF emits on a new subscription.
func (s *Stream) pullLoop(ctx context.Context) {
var failures int
recreateBackoff := s.opts.RetryBackoff
var afterReconnect bool
for {
if ctx.Err() != nil {
return
}
msgs, err := pullMessages(s.caller, s.getPullPoint(), s.opts)
if err != nil {
s.surfaceError(ErrPullFailed{Err: err})
failures++
if !s.opts.DisableReconnect && failures >= s.opts.ReconnectAfterFailures {
justRecreated, cont := s.attemptRecreate(ctx, &failures, &recreateBackoff)
if !cont {
return
}
if justRecreated {
afterReconnect = true
}
continue
}
if !sleepCtx(ctx, s.opts.RetryBackoff) {
return
}
continue
}
failures = 0
recreateBackoff = s.opts.RetryBackoff
observedAt := s.now()
for _, m := range msgs {
ev := decode(m, s.opts.DeviceID, observedAt)
if afterReconnect {
ev.AfterReconnect = true
// Clear once the camera transitions past the
// Initialized replay to live events.
if ev.Operation != PropertyInitialized {
afterReconnect = false
}
}
select {
case <-ctx.Done():
return
case s.events <- ev:
}
}
}
}
// attemptRecreate returns (justRecreated, cont). cont is false only
// when ctx cancelled during backoff so the caller exits the loop.
func (s *Stream) attemptRecreate(ctx context.Context, failures *int, backoff *time.Duration) (justRecreated, cont bool) {
ref, err := createPullPoint(s.caller, s.opts)
if err != nil {
s.surfaceError(ErrRecreateFailed{Err: err})
if !sleepCtx(ctx, jitter(*backoff)) {
return false, false
}
*backoff *= 2
if *backoff > maxRecreateBackoff {
*backoff = maxRecreateBackoff
}
return false, true
}
s.setPullPoint(ref)
*failures = 0
*backoff = s.opts.RetryBackoff
return true, true
}
// jitter perturbs d by ±jitterFraction so synchronised drops do not
// produce a synchronised reconnect surge.
func jitter(d time.Duration) time.Duration {
if d <= 0 {
return time.Nanosecond
}
spread := float64(d) * jitterFraction
delta := (rand.Float64()*2 - 1) * spread
out := time.Duration(float64(d) + delta)
if out <= 0 {
out = time.Nanosecond
}
return out
}