2020-09-28 14:13:10 +00:00
|
|
|
package stream
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2020-10-04 19:12:35 +00:00
|
|
|
|
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2020-09-28 14:13:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// NDJsonHeartbeat is the NDJson to send as a heartbeat
|
|
|
|
// Avoids creating many heartbeat instances
|
2020-10-04 19:12:35 +00:00
|
|
|
NDJsonHeartbeat = &structs.NDJson{Data: []byte("{}\n")}
|
2020-09-28 14:13:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// NDJsonStream is used to send new line delimited JSON and heartbeats
|
|
|
|
// to a destination (out channel)
|
|
|
|
type NDJsonStream struct {
|
2020-10-04 19:12:35 +00:00
|
|
|
out chan<- *structs.NDJson
|
2020-09-28 14:13:10 +00:00
|
|
|
|
|
|
|
// heartbeat is the interval to send heartbeat messages to keep a connection
|
|
|
|
// open.
|
|
|
|
heartbeat *time.Ticker
|
|
|
|
|
2020-10-04 19:12:35 +00:00
|
|
|
publishCh chan structs.NDJson
|
2020-09-28 14:13:10 +00:00
|
|
|
exitCh chan struct{}
|
|
|
|
|
|
|
|
l sync.Mutex
|
|
|
|
running bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewNNewNDJsonStream creates a new NDJson stream that will output NDJson structs
|
|
|
|
// to the passed output channel
|
2020-10-04 19:12:35 +00:00
|
|
|
func NewNDJsonStream(out chan<- *structs.NDJson, heartbeat time.Duration) *NDJsonStream {
|
2020-09-28 14:13:10 +00:00
|
|
|
return &NDJsonStream{
|
|
|
|
out: out,
|
|
|
|
heartbeat: time.NewTicker(heartbeat),
|
|
|
|
exitCh: make(chan struct{}),
|
2020-10-04 19:12:35 +00:00
|
|
|
publishCh: make(chan structs.NDJson),
|
2020-09-28 14:13:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run starts a long lived goroutine that handles sending
|
|
|
|
// heartbeats and processed json objects to the streams out channel as well
|
|
|
|
func (n *NDJsonStream) Run(ctx context.Context) {
|
|
|
|
n.l.Lock()
|
|
|
|
if n.running {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
n.running = true
|
|
|
|
n.l.Unlock()
|
|
|
|
|
|
|
|
go n.run(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (n *NDJsonStream) run(ctx context.Context) {
|
|
|
|
defer func() {
|
|
|
|
n.l.Lock()
|
|
|
|
n.running = false
|
|
|
|
n.l.Unlock()
|
|
|
|
close(n.exitCh)
|
|
|
|
}()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case msg := <-n.publishCh:
|
|
|
|
n.out <- msg.Copy()
|
|
|
|
case <-n.heartbeat.C:
|
|
|
|
// Send a heartbeat frame
|
|
|
|
select {
|
|
|
|
case n.out <- NDJsonHeartbeat:
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send encodes an object into Newline delimited json. An error is returned
|
|
|
|
// if json encoding fails or if the stream is no longer running.
|
|
|
|
func (n *NDJsonStream) Send(obj interface{}) error {
|
|
|
|
n.l.Lock()
|
|
|
|
defer n.l.Unlock()
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
if err := json.NewEncoder(buf).Encode(obj); err != nil {
|
|
|
|
return fmt.Errorf("marshaling json for stream: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
2020-10-04 19:12:35 +00:00
|
|
|
case n.publishCh <- structs.NDJson{Data: buf.Bytes()}:
|
2020-09-28 14:13:10 +00:00
|
|
|
case <-n.exitCh:
|
|
|
|
return fmt.Errorf("stream is no longer running")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|