Files
configcenter/internal/runtime/stream.go

132 lines
3.0 KiB
Go

package runtime
import (
"context"
"errors"
"time"
"github.com/longpeng/configcenter/internal/domain"
)
const (
streamInitialBackoff = 100 * time.Millisecond
streamMaxBackoff = 5 * time.Second
)
// StreamSnapshots exposes the runtime watch contract used by every transport.
// startRevision is inclusive. A value <= 0 means a fresh subscription: emit a
// FULL_SYNC snapshot first, then watch from snapshot revision + 1.
func StreamSnapshots(ctx context.Context, store Store, key string, startRevision int64) <-chan domain.ConfigEvent {
events := make(chan domain.ConfigEvent, 16)
go func() {
defer close(events)
nextRevision := startRevision
if nextRevision <= 0 {
current, ok := readSnapshot(ctx, store, key)
if !ok || !sendEvent(ctx, events, fullSync(current)) {
return
}
nextRevision = current.Revision + 1
}
backoff := streamInitialBackoff
for {
stream := store.Watch(ctx, key, nextRevision)
compacted := false
for {
select {
case <-ctx.Done():
return
case result, open := <-stream:
if !open {
goto reconnect
}
if result.Err != nil {
compacted = errors.Is(result.Err, ErrRevisionCompacted)
goto reconnect
}
if result.Event.Revision > 0 {
if nextRevision > 0 && result.Event.Revision < nextRevision {
continue
}
nextRevision = result.Event.Revision + 1
}
if !sendEvent(ctx, events, result.Event) {
return
}
backoff = streamInitialBackoff
}
}
reconnect:
if ctx.Err() != nil {
return
}
if compacted {
current, ok := readSnapshot(ctx, store, key)
if !ok || !sendEvent(ctx, events, fullSync(current)) {
return
}
nextRevision = current.Revision + 1
backoff = streamInitialBackoff
continue
}
if !waitForStreamRetry(ctx, backoff) {
return
}
backoff = nextStreamBackoff(backoff)
}
}()
return events
}
func readSnapshot(ctx context.Context, store Store, key string) (domain.RuntimeConfig, bool) {
backoff := streamInitialBackoff
for {
current, err := store.Get(ctx, key)
if err == nil {
return current, true
}
if !waitForStreamRetry(ctx, backoff) {
return domain.RuntimeConfig{}, false
}
backoff = nextStreamBackoff(backoff)
}
}
func fullSync(current domain.RuntimeConfig) domain.ConfigEvent {
return domain.ConfigEvent{Type: "FULL_SYNC", Items: current.Items, Revision: current.Revision}
}
func sendEvent(ctx context.Context, target chan<- domain.ConfigEvent, event domain.ConfigEvent) bool {
select {
case target <- event:
return true
case <-ctx.Done():
return false
}
}
func waitForStreamRetry(ctx context.Context, delay time.Duration) bool {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return false
case <-timer.C:
return true
}
}
func nextStreamBackoff(current time.Duration) time.Duration {
if current >= streamMaxBackoff {
return streamMaxBackoff
}
next := current * 2
if next > streamMaxBackoff {
return streamMaxBackoff
}
return next
}