132 lines
3.0 KiB
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
|
|
}
|