150 lines
3.1 KiB
Go
150 lines
3.1 KiB
Go
package test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitlab.michelsen.id/phillmichelsen/tessera/services/data_service/internal/domain"
|
|
)
|
|
|
|
type TestProvider struct {
|
|
mu sync.Mutex
|
|
streams map[string]*stream
|
|
outputChannel chan<- domain.Message
|
|
tickDuration time.Duration
|
|
}
|
|
|
|
type stream struct {
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
}
|
|
|
|
// NewTestProvider wires the outbound channel.
|
|
func NewTestProvider(out chan<- domain.Message, tickDuration time.Duration) *TestProvider {
|
|
return &TestProvider{
|
|
streams: make(map[string]*stream),
|
|
outputChannel: out,
|
|
tickDuration: tickDuration,
|
|
}
|
|
}
|
|
|
|
func (t *TestProvider) Start() error { return nil }
|
|
|
|
func (t *TestProvider) Stop() {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
for key, s := range t.streams {
|
|
s.cancel()
|
|
<-s.done
|
|
delete(t.streams, key)
|
|
}
|
|
}
|
|
|
|
func (t *TestProvider) Subscribe(subject string) <-chan error {
|
|
errCh := make(chan error, 1)
|
|
|
|
if !t.IsValidSubject(subject, false) {
|
|
errCh <- errors.New("invalid subject")
|
|
close(errCh)
|
|
return errCh
|
|
}
|
|
|
|
t.mu.Lock()
|
|
// Already active: treat as success.
|
|
if _, ok := t.streams[subject]; ok {
|
|
t.mu.Unlock()
|
|
errCh <- nil
|
|
return errCh
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
s := &stream{cancel: cancel, done: make(chan struct{})}
|
|
t.streams[subject] = s
|
|
out := t.outputChannel
|
|
t.mu.Unlock()
|
|
|
|
// Stream goroutine.
|
|
go func(subj string, s *stream) {
|
|
slog.Default().Debug("new stream routine started", slog.String("cmp", "test_provider"), slog.String("subject", subj))
|
|
ticker := time.NewTicker(t.tickDuration)
|
|
ident, _ := domain.RawID("test_provider", subj)
|
|
defer func() {
|
|
ticker.Stop()
|
|
close(s.done)
|
|
}()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if out != nil {
|
|
msg := domain.Message{
|
|
Identifier: ident,
|
|
Payload: []byte(time.Now().UTC().Format(time.RFC3339Nano)),
|
|
}
|
|
// Non-blocking send avoids deadlock if caller stops reading.
|
|
select {
|
|
case out <- msg:
|
|
default:
|
|
slog.Default().Warn("dropping message due to backpressure", "cmp", "test_provider", "subject", subj)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}(subject, s)
|
|
|
|
// Signal successful subscription.
|
|
errCh <- nil
|
|
return errCh
|
|
}
|
|
|
|
func (t *TestProvider) Unsubscribe(subject string) <-chan error {
|
|
errCh := make(chan error, 1)
|
|
|
|
t.mu.Lock()
|
|
s, ok := t.streams[subject]
|
|
if !ok {
|
|
t.mu.Unlock()
|
|
errCh <- errors.New("not subscribed")
|
|
return errCh
|
|
}
|
|
delete(t.streams, subject)
|
|
t.mu.Unlock()
|
|
|
|
go func() {
|
|
s.cancel()
|
|
<-s.done
|
|
errCh <- nil
|
|
}()
|
|
return errCh
|
|
}
|
|
|
|
func (t *TestProvider) Fetch(subject string) (domain.Message, error) {
|
|
return domain.Message{}, fmt.Errorf("fetch not supported by provider")
|
|
}
|
|
|
|
func (t *TestProvider) GetActiveStreams() []string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
keys := make([]string, 0, len(t.streams))
|
|
for k := range t.streams {
|
|
keys = append(keys, k)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func (t *TestProvider) IsStreamActive(key string) bool {
|
|
t.mu.Lock()
|
|
_, ok := t.streams[key]
|
|
t.mu.Unlock()
|
|
return ok
|
|
}
|
|
|
|
func (t *TestProvider) IsValidSubject(key string, _ bool) bool {
|
|
return key != ""
|
|
}
|