Add fabric session stream runtime skeleton
This commit is contained in:
@@ -0,0 +1,342 @@
|
|||||||
|
package fabricproto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultInitialStreamCredit = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrSessionClosed = errors.New("fabric session is closed")
|
||||||
|
ErrStreamExists = errors.New("fabric stream already exists")
|
||||||
|
ErrStreamNotFound = errors.New("fabric stream not found")
|
||||||
|
ErrStreamClosed = errors.New("fabric stream is closed")
|
||||||
|
ErrStreamQueueFull = errors.New("fabric stream queue is full")
|
||||||
|
ErrStreamCreditExhausted = errors.New("fabric stream credit is exhausted")
|
||||||
|
)
|
||||||
|
|
||||||
|
type StreamState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StreamStateOpen StreamState = "open"
|
||||||
|
StreamStateClosed StreamState = "closed"
|
||||||
|
StreamStateReset StreamState = "reset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionConfig struct {
|
||||||
|
InitialStreamCredit int
|
||||||
|
ClassQueueCapacity map[TrafficClass]int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
|
streams map[uint64]*stream
|
||||||
|
metrics SessionMetrics
|
||||||
|
cfg SessionConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type stream struct {
|
||||||
|
id uint64
|
||||||
|
trafficClass TrafficClass
|
||||||
|
state StreamState
|
||||||
|
nextSequence uint64
|
||||||
|
credit int
|
||||||
|
queue []Frame
|
||||||
|
metrics StreamMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionMetrics struct {
|
||||||
|
StreamsOpened uint64
|
||||||
|
StreamsClosed uint64
|
||||||
|
StreamsReset uint64
|
||||||
|
FramesEnqueued uint64
|
||||||
|
FramesDequeued uint64
|
||||||
|
FramesAcked uint64
|
||||||
|
FramesDropped uint64
|
||||||
|
CreditExhausted uint64
|
||||||
|
QueueFull uint64
|
||||||
|
QueueDepths map[uint64]int
|
||||||
|
Streams map[uint64]StreamMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamMetrics struct {
|
||||||
|
StreamID uint64
|
||||||
|
TrafficClass TrafficClass
|
||||||
|
State StreamState
|
||||||
|
NextSequence uint64
|
||||||
|
Credit int
|
||||||
|
QueueDepth int
|
||||||
|
Enqueued uint64
|
||||||
|
Dequeued uint64
|
||||||
|
Acked uint64
|
||||||
|
Dropped uint64
|
||||||
|
CreditBlocked uint64
|
||||||
|
QueueBlocked uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(cfg SessionConfig) *Session {
|
||||||
|
return &Session{
|
||||||
|
streams: map[uint64]*stream{},
|
||||||
|
metrics: SessionMetrics{
|
||||||
|
QueueDepths: map[uint64]int{},
|
||||||
|
Streams: map[uint64]StreamMetrics{},
|
||||||
|
},
|
||||||
|
cfg: normalizeSessionConfig(cfg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) OpenStream(streamID uint64, trafficClass TrafficClass) error {
|
||||||
|
if streamID == 0 {
|
||||||
|
return ErrInvalidStreamID
|
||||||
|
}
|
||||||
|
if !KnownTrafficClass(trafficClass) {
|
||||||
|
return ErrUnknownTraffic
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return ErrSessionClosed
|
||||||
|
}
|
||||||
|
if _, ok := s.streams[streamID]; ok {
|
||||||
|
return ErrStreamExists
|
||||||
|
}
|
||||||
|
s.streams[streamID] = &stream{
|
||||||
|
id: streamID,
|
||||||
|
trafficClass: trafficClass,
|
||||||
|
state: StreamStateOpen,
|
||||||
|
credit: s.cfg.InitialStreamCredit,
|
||||||
|
}
|
||||||
|
s.metrics.StreamsOpened++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) EnqueueData(streamID uint64, payload []byte) (Frame, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return Frame{}, ErrSessionClosed
|
||||||
|
}
|
||||||
|
st, err := s.openStreamLocked(streamID)
|
||||||
|
if err != nil {
|
||||||
|
return Frame{}, err
|
||||||
|
}
|
||||||
|
if st.credit <= 0 {
|
||||||
|
st.metrics.CreditBlocked++
|
||||||
|
s.metrics.CreditExhausted++
|
||||||
|
s.metrics.FramesDropped++
|
||||||
|
return Frame{}, ErrStreamCreditExhausted
|
||||||
|
}
|
||||||
|
if len(st.queue) >= s.queueCapacity(st.trafficClass) {
|
||||||
|
st.metrics.QueueBlocked++
|
||||||
|
st.metrics.Dropped++
|
||||||
|
s.metrics.QueueFull++
|
||||||
|
s.metrics.FramesDropped++
|
||||||
|
return Frame{}, ErrStreamQueueFull
|
||||||
|
}
|
||||||
|
st.nextSequence++
|
||||||
|
st.credit--
|
||||||
|
frame := Frame{
|
||||||
|
Type: FrameData,
|
||||||
|
TrafficClass: st.trafficClass,
|
||||||
|
StreamID: streamID,
|
||||||
|
Sequence: st.nextSequence,
|
||||||
|
Payload: append([]byte(nil), payload...),
|
||||||
|
}
|
||||||
|
st.queue = append(st.queue, frame)
|
||||||
|
st.metrics.Enqueued++
|
||||||
|
s.metrics.FramesEnqueued++
|
||||||
|
return frame, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) DequeueNext() (Frame, bool) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.closed {
|
||||||
|
return Frame{}, false
|
||||||
|
}
|
||||||
|
for _, trafficClass := range priorityOrder() {
|
||||||
|
streams := s.streamsByClassLocked(trafficClass)
|
||||||
|
for _, st := range streams {
|
||||||
|
if len(st.queue) == 0 || st.state != StreamStateOpen {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
frame := st.queue[0]
|
||||||
|
st.queue = st.queue[1:]
|
||||||
|
st.metrics.Dequeued++
|
||||||
|
s.metrics.FramesDequeued++
|
||||||
|
return frame, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Frame{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Ack(streamID uint64, sequence uint64) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
st, ok := s.streams[streamID]
|
||||||
|
if !ok {
|
||||||
|
return ErrStreamNotFound
|
||||||
|
}
|
||||||
|
if sequence > st.metrics.Acked {
|
||||||
|
delta := sequence - st.metrics.Acked
|
||||||
|
st.metrics.Acked = sequence
|
||||||
|
s.metrics.FramesAcked += delta
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) AddCredit(streamID uint64, frames int) error {
|
||||||
|
if frames <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
st, ok := s.streams[streamID]
|
||||||
|
if !ok {
|
||||||
|
return ErrStreamNotFound
|
||||||
|
}
|
||||||
|
if st.state != StreamStateOpen {
|
||||||
|
return ErrStreamClosed
|
||||||
|
}
|
||||||
|
st.credit += frames
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) CloseStream(streamID uint64) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
st, ok := s.streams[streamID]
|
||||||
|
if !ok {
|
||||||
|
return ErrStreamNotFound
|
||||||
|
}
|
||||||
|
if st.state == StreamStateClosed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
st.queue = nil
|
||||||
|
st.state = StreamStateClosed
|
||||||
|
s.metrics.StreamsClosed++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) ResetStream(streamID uint64) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
st, ok := s.streams[streamID]
|
||||||
|
if !ok {
|
||||||
|
return ErrStreamNotFound
|
||||||
|
}
|
||||||
|
if st.state == StreamStateReset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dropped := len(st.queue)
|
||||||
|
st.queue = nil
|
||||||
|
st.state = StreamStateReset
|
||||||
|
st.metrics.Dropped += uint64(dropped)
|
||||||
|
s.metrics.FramesDropped += uint64(dropped)
|
||||||
|
s.metrics.StreamsReset++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Close() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.closed = true
|
||||||
|
for _, st := range s.streams {
|
||||||
|
st.queue = nil
|
||||||
|
if st.state == StreamStateOpen {
|
||||||
|
st.state = StreamStateClosed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Snapshot() SessionMetrics {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
out := s.metrics
|
||||||
|
out.QueueDepths = map[uint64]int{}
|
||||||
|
out.Streams = map[uint64]StreamMetrics{}
|
||||||
|
for streamID, st := range s.streams {
|
||||||
|
metrics := st.metrics
|
||||||
|
metrics.StreamID = streamID
|
||||||
|
metrics.TrafficClass = st.trafficClass
|
||||||
|
metrics.State = st.state
|
||||||
|
metrics.NextSequence = st.nextSequence
|
||||||
|
metrics.Credit = st.credit
|
||||||
|
metrics.QueueDepth = len(st.queue)
|
||||||
|
out.QueueDepths[streamID] = len(st.queue)
|
||||||
|
out.Streams[streamID] = metrics
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) openStreamLocked(streamID uint64) (*stream, error) {
|
||||||
|
st, ok := s.streams[streamID]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrStreamNotFound
|
||||||
|
}
|
||||||
|
if st.state != StreamStateOpen {
|
||||||
|
return nil, ErrStreamClosed
|
||||||
|
}
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) queueCapacity(trafficClass TrafficClass) int {
|
||||||
|
if capacity := s.cfg.ClassQueueCapacity[trafficClass]; capacity > 0 {
|
||||||
|
return capacity
|
||||||
|
}
|
||||||
|
return defaultClassQueueCapacity(trafficClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) streamsByClassLocked(trafficClass TrafficClass) []*stream {
|
||||||
|
var out []*stream
|
||||||
|
for _, st := range s.streams {
|
||||||
|
if st.trafficClass == trafficClass {
|
||||||
|
out = append(out, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
return out[i].id < out[j].id
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSessionConfig(cfg SessionConfig) SessionConfig {
|
||||||
|
if cfg.InitialStreamCredit <= 0 {
|
||||||
|
cfg.InitialStreamCredit = DefaultInitialStreamCredit
|
||||||
|
}
|
||||||
|
if cfg.ClassQueueCapacity == nil {
|
||||||
|
cfg.ClassQueueCapacity = map[TrafficClass]int{}
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func priorityOrder() []TrafficClass {
|
||||||
|
return []TrafficClass{
|
||||||
|
TrafficClassControl,
|
||||||
|
TrafficClassDNS,
|
||||||
|
TrafficClassInteractive,
|
||||||
|
TrafficClassReliable,
|
||||||
|
TrafficClassBulk,
|
||||||
|
TrafficClassDroppable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultClassQueueCapacity(trafficClass TrafficClass) int {
|
||||||
|
switch trafficClass {
|
||||||
|
case TrafficClassControl, TrafficClassDNS, TrafficClassInteractive:
|
||||||
|
return 128
|
||||||
|
case TrafficClassReliable:
|
||||||
|
return 64
|
||||||
|
case TrafficClassBulk:
|
||||||
|
return 16
|
||||||
|
case TrafficClassDroppable:
|
||||||
|
return 8
|
||||||
|
default:
|
||||||
|
return 32
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package fabricproto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSessionDequeuesByTrafficPriority(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{})
|
||||||
|
mustOpenStream(t, session, 10, TrafficClassBulk)
|
||||||
|
mustOpenStream(t, session, 20, TrafficClassInteractive)
|
||||||
|
mustOpenStream(t, session, 30, TrafficClassControl)
|
||||||
|
|
||||||
|
mustEnqueue(t, session, 10, "bulk")
|
||||||
|
mustEnqueue(t, session, 20, "interactive")
|
||||||
|
mustEnqueue(t, session, 30, "control")
|
||||||
|
|
||||||
|
frame, ok := session.DequeueNext()
|
||||||
|
if !ok || frame.StreamID != 30 {
|
||||||
|
t.Fatalf("first frame = %+v ok=%v, want control stream 30", frame, ok)
|
||||||
|
}
|
||||||
|
frame, ok = session.DequeueNext()
|
||||||
|
if !ok || frame.StreamID != 20 {
|
||||||
|
t.Fatalf("second frame = %+v ok=%v, want interactive stream 20", frame, ok)
|
||||||
|
}
|
||||||
|
frame, ok = session.DequeueNext()
|
||||||
|
if !ok || frame.StreamID != 10 {
|
||||||
|
t.Fatalf("third frame = %+v ok=%v, want bulk stream 10", frame, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionBulkQueueFullDoesNotBlockControlOrInteractive(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{
|
||||||
|
ClassQueueCapacity: map[TrafficClass]int{
|
||||||
|
TrafficClassBulk: 1,
|
||||||
|
TrafficClassControl: 4,
|
||||||
|
TrafficClassInteractive: 4,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassBulk)
|
||||||
|
mustOpenStream(t, session, 2, TrafficClassControl)
|
||||||
|
mustOpenStream(t, session, 3, TrafficClassInteractive)
|
||||||
|
|
||||||
|
mustEnqueue(t, session, 1, "bulk-1")
|
||||||
|
if _, err := session.EnqueueData(1, []byte("bulk-2")); !errors.Is(err, ErrStreamQueueFull) {
|
||||||
|
t.Fatalf("bulk enqueue error = %v, want %v", err, ErrStreamQueueFull)
|
||||||
|
}
|
||||||
|
mustEnqueue(t, session, 2, "control")
|
||||||
|
mustEnqueue(t, session, 3, "interactive")
|
||||||
|
|
||||||
|
first, ok := session.DequeueNext()
|
||||||
|
if !ok || first.StreamID != 2 {
|
||||||
|
t.Fatalf("first frame = %+v ok=%v, want control", first, ok)
|
||||||
|
}
|
||||||
|
second, ok := session.DequeueNext()
|
||||||
|
if !ok || second.StreamID != 3 {
|
||||||
|
t.Fatalf("second frame = %+v ok=%v, want interactive", second, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot := session.Snapshot()
|
||||||
|
if snapshot.QueueFull != 1 || snapshot.FramesDropped != 1 {
|
||||||
|
t.Fatalf("snapshot queue full/dropped = %d/%d, want 1/1", snapshot.QueueFull, snapshot.FramesDropped)
|
||||||
|
}
|
||||||
|
if snapshot.Streams[2].Enqueued != 1 || snapshot.Streams[3].Enqueued != 1 {
|
||||||
|
t.Fatalf("control/interactive metrics = %+v / %+v", snapshot.Streams[2], snapshot.Streams[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionCreditBlocksOnlyOneStream(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{InitialStreamCredit: 1})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassReliable)
|
||||||
|
mustOpenStream(t, session, 2, TrafficClassReliable)
|
||||||
|
|
||||||
|
mustEnqueue(t, session, 1, "one")
|
||||||
|
if _, err := session.EnqueueData(1, []byte("two")); !errors.Is(err, ErrStreamCreditExhausted) {
|
||||||
|
t.Fatalf("credit error = %v, want %v", err, ErrStreamCreditExhausted)
|
||||||
|
}
|
||||||
|
mustEnqueue(t, session, 2, "other")
|
||||||
|
|
||||||
|
snapshot := session.Snapshot()
|
||||||
|
if snapshot.CreditExhausted != 1 || snapshot.Streams[1].CreditBlocked != 1 {
|
||||||
|
t.Fatalf("credit metrics = %+v stream=%+v", snapshot, snapshot.Streams[1])
|
||||||
|
}
|
||||||
|
if snapshot.Streams[2].Enqueued != 1 {
|
||||||
|
t.Fatalf("second stream metrics = %+v, want enqueued", snapshot.Streams[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionAddCreditAllowsMoreFrames(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{InitialStreamCredit: 1})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassReliable)
|
||||||
|
mustEnqueue(t, session, 1, "one")
|
||||||
|
if _, err := session.EnqueueData(1, []byte("blocked")); !errors.Is(err, ErrStreamCreditExhausted) {
|
||||||
|
t.Fatalf("credit error = %v, want %v", err, ErrStreamCreditExhausted)
|
||||||
|
}
|
||||||
|
if err := session.AddCredit(1, 2); err != nil {
|
||||||
|
t.Fatalf("add credit: %v", err)
|
||||||
|
}
|
||||||
|
mustEnqueue(t, session, 1, "two")
|
||||||
|
|
||||||
|
snapshot := session.Snapshot()
|
||||||
|
if snapshot.Streams[1].Credit != 1 || snapshot.Streams[1].Enqueued != 2 {
|
||||||
|
t.Fatalf("stream metrics = %+v, want credit 1 and enqueued 2", snapshot.Streams[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionResetDropsOnlySelectedStream(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassBulk)
|
||||||
|
mustOpenStream(t, session, 2, TrafficClassControl)
|
||||||
|
mustEnqueue(t, session, 1, "bulk")
|
||||||
|
mustEnqueue(t, session, 2, "control")
|
||||||
|
|
||||||
|
if err := session.ResetStream(1); err != nil {
|
||||||
|
t.Fatalf("reset stream: %v", err)
|
||||||
|
}
|
||||||
|
frame, ok := session.DequeueNext()
|
||||||
|
if !ok || frame.StreamID != 2 {
|
||||||
|
t.Fatalf("dequeued frame = %+v ok=%v, want control stream 2", frame, ok)
|
||||||
|
}
|
||||||
|
if _, ok := session.DequeueNext(); ok {
|
||||||
|
t.Fatal("unexpected frame after reset dropped bulk queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot := session.Snapshot()
|
||||||
|
if snapshot.StreamsReset != 1 || snapshot.Streams[1].State != StreamStateReset || snapshot.Streams[2].Dequeued != 1 {
|
||||||
|
t.Fatalf("snapshot = %+v", snapshot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionAckUpdatesMetrics(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassReliable)
|
||||||
|
mustEnqueue(t, session, 1, "one")
|
||||||
|
mustEnqueue(t, session, 1, "two")
|
||||||
|
|
||||||
|
if err := session.Ack(1, 2); err != nil {
|
||||||
|
t.Fatalf("ack: %v", err)
|
||||||
|
}
|
||||||
|
snapshot := session.Snapshot()
|
||||||
|
if snapshot.FramesAcked != 2 || snapshot.Streams[1].Acked != 2 {
|
||||||
|
t.Fatalf("ack metrics = %+v stream=%+v", snapshot, snapshot.Streams[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionCloseRejectsNewData(t *testing.T) {
|
||||||
|
session := NewSession(SessionConfig{})
|
||||||
|
mustOpenStream(t, session, 1, TrafficClassReliable)
|
||||||
|
session.Close()
|
||||||
|
if _, err := session.EnqueueData(1, []byte("after-close")); !errors.Is(err, ErrSessionClosed) {
|
||||||
|
t.Fatalf("enqueue after close err = %v, want %v", err, ErrSessionClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustOpenStream(t *testing.T, session *Session, streamID uint64, trafficClass TrafficClass) {
|
||||||
|
t.Helper()
|
||||||
|
if err := session.OpenStream(streamID, trafficClass); err != nil {
|
||||||
|
t.Fatalf("open stream %d: %v", streamID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustEnqueue(t *testing.T, session *Session, streamID uint64, payload string) Frame {
|
||||||
|
t.Helper()
|
||||||
|
frame, err := session.EnqueueData(streamID, []byte(payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("enqueue stream %d: %v", streamID, err)
|
||||||
|
}
|
||||||
|
return frame
|
||||||
|
}
|
||||||
@@ -244,6 +244,8 @@ Deliverables:
|
|||||||
|
|
||||||
### Stage FNP-2: Persistent Session Runtime Skeleton
|
### Stage FNP-2: Persistent Session Runtime Skeleton
|
||||||
|
|
||||||
|
Status: started in `agents/rap-node-agent/internal/fabricproto`.
|
||||||
|
|
||||||
Deliverables:
|
Deliverables:
|
||||||
|
|
||||||
- implement in-memory session runtime with streams, sequence numbers, ACK,
|
- implement in-memory session runtime with streams, sequence numbers, ACK,
|
||||||
|
|||||||
Reference in New Issue
Block a user