741 lines
14 KiB
Go
741 lines
14 KiB
Go
package prism
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base32"
|
|
"fmt"
|
|
"git.wisehodl.dev/jay/go-mana-component"
|
|
"git.wisehodl.dev/jay/go-roots-ws"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
_ fmt.Formatter
|
|
)
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Types
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Outputs
|
|
|
|
type ReqEvent struct {
|
|
PeerID string
|
|
ReceivedAt time.Time
|
|
Data []byte
|
|
}
|
|
|
|
type ReqMessage struct {
|
|
PeerID string
|
|
ReceivedAt time.Time
|
|
Data string
|
|
}
|
|
|
|
// Options
|
|
|
|
const (
|
|
defaultLabel = "REQ"
|
|
defaultEOSETimeout = 30 * time.Second
|
|
)
|
|
|
|
type reqConfig struct {
|
|
id string
|
|
label string
|
|
eoseTimeout time.Duration
|
|
}
|
|
|
|
type ReqOption func(*reqConfig)
|
|
|
|
// Request Manager
|
|
|
|
type ReqManager struct {
|
|
subs map[string]Request
|
|
byPeer map[string]map[string]struct{} // peerID -> subID set
|
|
|
|
postmaster *Postmaster
|
|
collector *JournalCollector
|
|
journals chan JournalEntry // JournalCollector.Enroll
|
|
|
|
isConnected func(peerID string) bool // Adapter.IsConnected
|
|
poolEvents <-chan PoolEvent // Adapter.Subscribe
|
|
poolInbox <-chan InboundLetter // Clerk.Subscribe
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
mu sync.RWMutex
|
|
wg sync.WaitGroup
|
|
handler slog.Handler
|
|
logger *slog.Logger
|
|
}
|
|
|
|
type Request interface {
|
|
order(reqTask)
|
|
Peers() []string
|
|
Close()
|
|
}
|
|
|
|
// Base Request
|
|
|
|
type request struct {
|
|
id string
|
|
req Envelope
|
|
|
|
tasks chan reqTask
|
|
closing bool
|
|
done chan struct{}
|
|
|
|
buffer chan ReqEvent
|
|
events chan ReqEvent
|
|
messages chan ReqMessage
|
|
|
|
postmaster *Postmaster
|
|
journals chan JournalEntry
|
|
isConnected func(peerID string) bool
|
|
onClose func()
|
|
|
|
ctx context.Context
|
|
wg sync.WaitGroup
|
|
peerWg sync.WaitGroup
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// Stream Request
|
|
|
|
type StreamReq struct {
|
|
*request
|
|
peers map[string]*streamPeer
|
|
}
|
|
|
|
type streamPeer struct {
|
|
reqSent bool
|
|
closeSent bool
|
|
closed bool
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
// Query Request
|
|
|
|
type QueryReq struct {
|
|
*request
|
|
peers map[string]*queryPeer
|
|
eoseTimeout time.Duration
|
|
}
|
|
|
|
type queryPeer struct {
|
|
reqSent bool
|
|
eoseTimer *time.Timer
|
|
closeSent bool
|
|
closed bool
|
|
closeOnce sync.Once
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Request Options
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func newReqConfig(opts ...ReqOption) reqConfig {
|
|
cfg := reqConfig{
|
|
id: "",
|
|
label: defaultLabel,
|
|
eoseTimeout: defaultEOSETimeout,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(&cfg)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func WithID(id string) ReqOption {
|
|
return func(c *reqConfig) {
|
|
c.id = id
|
|
}
|
|
}
|
|
|
|
func WithLabel(label string) ReqOption {
|
|
return func(c *reqConfig) {
|
|
c.label = label
|
|
}
|
|
}
|
|
|
|
func WithEOSETimeout(timeout time.Duration) ReqOption {
|
|
return func(c *reqConfig) {
|
|
c.eoseTimeout = timeout
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Request Manager
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func NewReqManager(
|
|
ctx context.Context,
|
|
postmaster *Postmaster,
|
|
isConnected func(string) bool,
|
|
poolEvents <-chan PoolEvent,
|
|
poolInbox <-chan InboundLetter,
|
|
collector *JournalCollector,
|
|
handler slog.Handler,
|
|
) *ReqManager {
|
|
return nil
|
|
}
|
|
|
|
func (m *ReqManager) OpenStream(
|
|
filters [][]byte,
|
|
peers []string,
|
|
opts ...ReqOption,
|
|
) (
|
|
id string,
|
|
events <-chan ReqEvent,
|
|
messages <-chan ReqMessage,
|
|
err error,
|
|
) {
|
|
return "", nil, nil, nil
|
|
}
|
|
|
|
func (m *ReqManager) OpenQuery(
|
|
filters [][]byte,
|
|
peers []string,
|
|
opts ...ReqOption,
|
|
) (
|
|
id string,
|
|
events <-chan ReqEvent,
|
|
messages <-chan ReqMessage,
|
|
err error,
|
|
) {
|
|
return "", nil, nil, nil
|
|
}
|
|
|
|
func (m *ReqManager) CloseReq(id string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *ReqManager) Close() {}
|
|
|
|
func (m *ReqManager) makeOnClose(subID string, peers []string) func() {
|
|
return func() {}
|
|
}
|
|
|
|
func (m *ReqManager) routeInbox() {
|
|
// parses envelope label and sub ID from the letter
|
|
// looks up in sub registry
|
|
// calls req.order()
|
|
}
|
|
|
|
func (m *ReqManager) routeEvents() {
|
|
// reads PoolEvent
|
|
// looks up in m.byPeer
|
|
// calls req.order() on each matching request
|
|
}
|
|
|
|
// Helpers
|
|
|
|
func cleanPeers(peers []string) []string {
|
|
return nil
|
|
}
|
|
|
|
var encoder = base32.StdEncoding.WithPadding(base32.NoPadding)
|
|
|
|
func generateID(prefix string) string {
|
|
return ""
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Base Request
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func (r *request) runReturnEvents() {
|
|
defer r.wg.Done()
|
|
defer close(r.events)
|
|
defer close(r.messages)
|
|
bufferedPipe(r.buffer, r.events)
|
|
}
|
|
|
|
func (r *request) dispatchEvent(task taskEvent) {
|
|
select {
|
|
case <-r.done:
|
|
case r.buffer <- ReqEvent{
|
|
PeerID: task.peerID, ReceivedAt: task.at, Data: task.data}:
|
|
}
|
|
}
|
|
|
|
func (r *request) emit(entry JournalEntry) {
|
|
select {
|
|
case <-r.done:
|
|
case r.journals <- entry:
|
|
}
|
|
}
|
|
|
|
func (r *request) order(task reqTask) {
|
|
select {
|
|
case <-r.done:
|
|
case r.tasks <- task:
|
|
}
|
|
}
|
|
|
|
func (r *request) Close() {
|
|
r.order(newCloseReq())
|
|
r.wg.Wait()
|
|
}
|
|
|
|
func (r *request) terminate() {
|
|
defer r.wg.Done()
|
|
r.peerWg.Wait()
|
|
close(r.done)
|
|
close(r.buffer)
|
|
if r.journals != nil {
|
|
close(r.journals)
|
|
}
|
|
r.onClose()
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Stream Request
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func NewStreamReq(
|
|
ctx context.Context,
|
|
id string,
|
|
filters [][]byte,
|
|
peers []string,
|
|
postmaster *Postmaster,
|
|
isConnected func(string) bool,
|
|
collector *JournalCollector,
|
|
onClose func(),
|
|
handler slog.Handler,
|
|
) *StreamReq {
|
|
ctx = component.MustExtend(ctx, "stream")
|
|
|
|
r := &StreamReq{
|
|
request: &request{
|
|
id: id,
|
|
req: envelope.EncloseReq(id, filters),
|
|
|
|
tasks: make(chan reqTask, len(peers)*16),
|
|
done: make(chan struct{}),
|
|
|
|
buffer: make(chan ReqEvent, len(peers)*16),
|
|
events: make(chan ReqEvent),
|
|
messages: make(chan ReqMessage, len(peers)),
|
|
|
|
postmaster: postmaster,
|
|
isConnected: isConnected,
|
|
onClose: onClose,
|
|
|
|
ctx: ctx,
|
|
},
|
|
peers: make(map[string]*streamPeer),
|
|
}
|
|
|
|
if collector != nil {
|
|
r.journals = make(chan JournalEntry, len(peers)*16)
|
|
collector.Enroll(r.journals)
|
|
}
|
|
|
|
if handler != nil {
|
|
c := component.FromContext(ctx)
|
|
r.logger = slog.New(handler).With(slog.Any("component", c))
|
|
}
|
|
|
|
for _, peerID := range peers {
|
|
r.peers[peerID] = &streamPeer{}
|
|
r.peerWg.Add(1)
|
|
}
|
|
|
|
r.wg.Add(2)
|
|
go r.run()
|
|
go r.runReturnEvents()
|
|
|
|
// send initial REQs
|
|
for id := range r.peers {
|
|
if r.isConnected(id) {
|
|
r.sendReq(id)
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (r *StreamReq) run() {
|
|
defer r.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-r.done:
|
|
return
|
|
case t := <-r.tasks:
|
|
r.dispatch(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *StreamReq) Peers() []string {
|
|
peers := make([]string, 0, len(r.peers))
|
|
for p := range r.peers {
|
|
peers = append(peers, p)
|
|
}
|
|
return peers
|
|
}
|
|
|
|
func (r *StreamReq) sendReq(peerID string) {
|
|
_, ok := r.peers[peerID]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
id, _ := r.postmaster.Send(r.ctx, peerID, r.req,
|
|
func(o LetterOutcome) { r.order(newReqOutcomeTask(peerID, o)) })
|
|
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewReqQueuedJournal(peerID, c, ReqQueuedData{
|
|
SubID: r.id, LetterID: id, QueuedAt: time.Now(),
|
|
}))
|
|
}
|
|
|
|
func (r *StreamReq) sendClose(peerID string) {
|
|
peer, ok := r.peers[peerID]
|
|
if !ok || peer.closeSent {
|
|
return
|
|
}
|
|
|
|
if !peer.reqSent {
|
|
r.closePeer(peerID)
|
|
return
|
|
}
|
|
|
|
id, _ := r.postmaster.Send(r.ctx, peerID, envelope.EncloseClose(r.id),
|
|
func(o LetterOutcome) { r.order(newCloseOutcomeTask(peerID, o)) })
|
|
|
|
peer.closeSent = true
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewCloseQueuedJournal(peerID, c, CloseQueuedData{
|
|
SubID: r.id, LetterID: id, QueuedAt: time.Now(),
|
|
}))
|
|
}
|
|
|
|
func (r *StreamReq) closePeer(peerID string) {
|
|
peer, ok := r.peers[peerID]
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
peer.closeOnce.Do(func() {
|
|
r.peerWg.Done()
|
|
peer.closed = true
|
|
})
|
|
}
|
|
|
|
func (r *StreamReq) dispatch(task reqTask) {
|
|
switch t := task.(type) {
|
|
case taskReqOutcome:
|
|
r.dispatchReqOutcome(t)
|
|
|
|
case taskCloseOutcome:
|
|
r.dispatchCloseOutcome(t)
|
|
|
|
case taskEvent:
|
|
r.dispatchEvent(t)
|
|
|
|
case taskEOSE:
|
|
r.dispatchEOSE(t)
|
|
|
|
case taskClosed:
|
|
r.dispatchClosed(t)
|
|
|
|
case taskClosePeer:
|
|
r.dispatchClosePeer(t)
|
|
|
|
case taskCloseReq:
|
|
r.dispatchCloseReq(t)
|
|
|
|
case taskHandleReconnect:
|
|
r.dispatchHandleReconnect(t)
|
|
}
|
|
}
|
|
|
|
func (r *StreamReq) dispatchReqOutcome(task taskReqOutcome) {
|
|
peer := r.peers[task.peerID]
|
|
if task.outcome.Kind == OutcomeSent {
|
|
peer.reqSent = true
|
|
}
|
|
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewReqSendOutcomeJournal(task.peerID, c, ReqSendOutcomeData{
|
|
SubID: r.id,
|
|
LetterID: task.outcome.LetterID,
|
|
Outcome: task.outcome.Kind,
|
|
SentAt: task.outcome.SentAt,
|
|
MissedAt: task.outcome.MissedAt,
|
|
RetryCount: task.outcome.Retries,
|
|
}))
|
|
}
|
|
|
|
func (r *StreamReq) dispatchCloseOutcome(task taskCloseOutcome) {
|
|
r.closePeer(task.peerID)
|
|
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewCloseSendOutcomeJournal(task.peerID, c, CloseSendOutcomeData{
|
|
SubID: r.id,
|
|
LetterID: task.outcome.LetterID,
|
|
Outcome: task.outcome.Kind,
|
|
SentAt: task.outcome.SentAt,
|
|
MissedAt: task.outcome.MissedAt,
|
|
RetryCount: task.outcome.Retries,
|
|
}))
|
|
}
|
|
|
|
func (r *StreamReq) dispatchEOSE(task taskEOSE) {
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewReceivedEOSEJournal(task.peerID, c, ReceivedEOSEData{
|
|
SubID: r.id, At: task.at,
|
|
}))
|
|
}
|
|
|
|
func (r *StreamReq) dispatchClosed(task taskClosed) {
|
|
c := component.FromContext(r.ctx)
|
|
r.emit(NewReceivedClosedJournal(task.peerID, c, ReceivedClosedData{
|
|
SubID: r.id, At: task.at, Message: task.message,
|
|
}))
|
|
|
|
peer := r.peers[task.peerID]
|
|
if peer.closed {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-r.done:
|
|
case r.messages <- ReqMessage{
|
|
PeerID: task.peerID,
|
|
ReceivedAt: task.at,
|
|
Data: task.message,
|
|
}:
|
|
}
|
|
|
|
r.closePeer(task.peerID)
|
|
}
|
|
|
|
func (r *StreamReq) dispatchClosePeer(task taskClosePeer) {
|
|
r.closePeer(task.peerID)
|
|
}
|
|
|
|
func (r *StreamReq) dispatchCloseReq(task taskCloseReq) {
|
|
if r.closing {
|
|
return
|
|
}
|
|
|
|
r.closing = true
|
|
|
|
for id, peer := range r.peers {
|
|
if !peer.closed {
|
|
if !r.isConnected(id) {
|
|
r.closePeer(id)
|
|
} else {
|
|
r.sendClose(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
r.wg.Add(1)
|
|
go r.terminate()
|
|
}
|
|
|
|
func (r *StreamReq) dispatchHandleReconnect(task taskHandleReconnect) {
|
|
peer, ok := r.peers[task.peerID]
|
|
if !ok || peer.closed || r.closing || peer.closeSent {
|
|
return
|
|
}
|
|
r.sendReq(task.peerID)
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Query Request
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func NewQueryReq(
|
|
ctx context.Context,
|
|
id string,
|
|
filters [][]byte,
|
|
peers []string,
|
|
postmaster *Postmaster,
|
|
isConnected func(string) bool,
|
|
journals chan<- JournalEntry,
|
|
eoseTimeout time.Duration,
|
|
onClose func(),
|
|
handler slog.Handler,
|
|
) *QueryReq {
|
|
// start buffered pipe to event output
|
|
// pipe return drives channel closures
|
|
return nil
|
|
}
|
|
|
|
func (r *QueryReq) Peers() []string {
|
|
peers := make([]string, 0, len(r.peers))
|
|
for p := range r.peers {
|
|
peers = append(peers, p)
|
|
}
|
|
return peers
|
|
}
|
|
|
|
func (r *QueryReq) sendReq(peerID string) error {
|
|
return nil
|
|
}
|
|
|
|
func (r *QueryReq) sendClose(peerID string) error {
|
|
return nil
|
|
}
|
|
|
|
func (r *QueryReq) run() {
|
|
defer r.wg.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-r.done:
|
|
return
|
|
case t := <-r.tasks:
|
|
r.dispatch(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *QueryReq) dispatch(task reqTask) {
|
|
switch t := task.(type) {
|
|
case taskReqOutcome:
|
|
r.dispatchReqOutcome(t)
|
|
|
|
case taskCloseOutcome:
|
|
r.dispatchCloseOutcome(t)
|
|
|
|
case taskEvent:
|
|
r.dispatchEvent(t)
|
|
|
|
case taskEOSE:
|
|
r.dispatchEOSE(t)
|
|
|
|
case taskClosed:
|
|
r.dispatchClosed(t)
|
|
|
|
case taskClosePeer:
|
|
r.dispatchClosePeer(t)
|
|
|
|
case taskCloseReq:
|
|
r.dispatchCloseReq(t)
|
|
|
|
case taskMissedEOSE:
|
|
r.dispatchMissedEOSE(t)
|
|
}
|
|
}
|
|
|
|
func (r *QueryReq) dispatchReqOutcome(task taskReqOutcome) {}
|
|
|
|
func (r *QueryReq) dispatchCloseOutcome(task taskCloseOutcome) {}
|
|
|
|
func (r *QueryReq) dispatchEOSE(task taskEOSE) {}
|
|
|
|
func (r *QueryReq) dispatchClosed(task taskClosed) {}
|
|
|
|
func (r *QueryReq) dispatchClosePeer(task taskClosePeer) {}
|
|
|
|
func (r *QueryReq) dispatchCloseReq(task taskCloseReq) {}
|
|
|
|
func (r *QueryReq) dispatchMissedEOSE(task taskMissedEOSE) {}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Request Tasks
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Types
|
|
|
|
type reqTask interface{ reqTask() } // gates task channel
|
|
|
|
type taskReqOutcome struct {
|
|
peerID string
|
|
outcome LetterOutcome
|
|
}
|
|
|
|
func (taskReqOutcome) reqTask() {}
|
|
|
|
type taskCloseOutcome struct {
|
|
peerID string
|
|
outcome LetterOutcome
|
|
}
|
|
|
|
func (taskCloseOutcome) reqTask() {}
|
|
|
|
type taskEvent struct {
|
|
peerID string
|
|
at time.Time
|
|
data Envelope
|
|
}
|
|
|
|
func (taskEvent) reqTask() {}
|
|
|
|
type taskEOSE struct {
|
|
peerID string
|
|
at time.Time
|
|
}
|
|
|
|
func (taskEOSE) reqTask() {}
|
|
|
|
type taskClosed struct {
|
|
peerID string
|
|
at time.Time
|
|
message string
|
|
}
|
|
|
|
func (taskClosed) reqTask() {}
|
|
|
|
type taskClosePeer struct{ peerID string }
|
|
|
|
func (taskClosePeer) reqTask() {}
|
|
|
|
type taskCloseReq struct{}
|
|
|
|
func (taskCloseReq) reqTask() {}
|
|
|
|
type taskHandleReconnect struct{ peerID string }
|
|
|
|
func (taskHandleReconnect) reqTask() {}
|
|
|
|
type taskMissedEOSE struct{ peerID string }
|
|
|
|
func (taskMissedEOSE) reqTask() {}
|
|
|
|
// Constructors
|
|
|
|
func newReqOutcomeTask(peerID string, outcome LetterOutcome) taskReqOutcome {
|
|
return taskReqOutcome{peerID: peerID, outcome: outcome}
|
|
}
|
|
|
|
func newCloseOutcomeTask(peerID string, outcome LetterOutcome) taskCloseOutcome {
|
|
return taskCloseOutcome{peerID: peerID, outcome: outcome}
|
|
}
|
|
|
|
func newEventTask(peerID string, at time.Time, data Envelope) taskEvent {
|
|
return taskEvent{peerID: peerID, at: at, data: data}
|
|
}
|
|
|
|
func newEOSETask(peerID string, at time.Time) taskEOSE {
|
|
return taskEOSE{peerID: peerID, at: at}
|
|
}
|
|
|
|
func newClosedTask(peerID string, at time.Time, message string) taskClosed {
|
|
return taskClosed{peerID: peerID, at: at, message: message}
|
|
}
|
|
|
|
func newClosePeerTask(peerID string) taskClosePeer {
|
|
return taskClosePeer{peerID: peerID}
|
|
}
|
|
|
|
func newCloseReq() taskCloseReq {
|
|
return taskCloseReq{}
|
|
}
|
|
|
|
func newHandleReconnect(peerID string) taskHandleReconnect {
|
|
return taskHandleReconnect{peerID: peerID}
|
|
}
|
|
|
|
func newMissedEOSETask(peerID string) taskMissedEOSE {
|
|
return taskMissedEOSE{peerID: peerID}
|
|
}
|