Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
156 lines
6.1 KiB
Go
156 lines
6.1 KiB
Go
package buscalls
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// SeatChunks is the send of one seat verb's answer file in chunks (design 47 G §681, A-64, A-139), a verb whose seat
|
|
// definition says `chunks: true`: the holder's process sends the nth chunk on mesh.seat.<seat>.chunk.<verb>.<call>.<n>,
|
|
// and the caller acknowledges it on mesh.seat.<seat>.ack.<verb>.<call>.<n> within SeatAckWithin before the next is sent.
|
|
// `call` is the 128 random bits the caller chose. The holder sends with it and the caller receives with it: one form.
|
|
type SeatChunks struct {
|
|
Seat, Verb, Call string
|
|
}
|
|
|
|
// SeatChunkBytes is at most what one chunk carries: 512 KiB.
|
|
const SeatChunkBytes = 512 << 10
|
|
|
|
// SeatAckWithin is how long the holder waits for a chunk's acknowledgement, and so how long a caller waits for the next chunk.
|
|
const SeatAckWithin = 30 * time.Second
|
|
|
|
// SeatChunk is the body of one chunk: its number from 1, how many there are, and its bytes.
|
|
type SeatChunk struct {
|
|
N int64 `json:"n"`
|
|
Of int64 `json:"of"`
|
|
Data []byte `json:"data"`
|
|
}
|
|
|
|
// Chunk is the subject of the nth chunk.
|
|
func (c SeatChunks) Chunk(n int64) string {
|
|
return "mesh.seat." + c.Seat + ".chunk." + c.Verb + "." + c.Call + "." + strconv.FormatInt(n, 10)
|
|
}
|
|
|
|
// Ack is the subject the nth chunk is acknowledged on.
|
|
func (c SeatChunks) Ack(n int64) string {
|
|
return "mesh.seat." + c.Seat + ".ack." + c.Verb + "." + c.Call + "." + strconv.FormatInt(n, 10)
|
|
}
|
|
|
|
// Chunks is every chunk subject of this call: what a caller subscribes to before it asks the verb.
|
|
func (c SeatChunks) Chunks() string {
|
|
return "mesh.seat." + c.Seat + ".chunk." + c.Verb + "." + c.Call + ".*"
|
|
}
|
|
|
|
// SeatAcks is every acknowledgement of a verb's sends: what the holder subscribes to.
|
|
func SeatAcks(seat, verb string) string {
|
|
return "mesh.seat." + seat + ".ack." + verb + ".*.*"
|
|
}
|
|
|
|
// ErrSeatChunkMissed is a chunk that did not come within its bound: the holder ended the send.
|
|
var ErrSeatChunkMissed = errors.New("a chunk did not come within its bound, so the send ended")
|
|
|
|
// Receive reads `of` chunks in order, writes each one's bytes to w and acknowledges it before the next is sent. next
|
|
// yields the subject and body of the next chunk heard on Chunks(), or ErrSeatChunkMissed at its bound (SeatAckWithin);
|
|
// ack publishes on a subject. A chunk out of order, of another count or on another subject ends the receive, and so the
|
|
// send, since it is never acknowledged. It answers the bytes written.
|
|
func (c SeatChunks) Receive(of int64, next func() (subject string, body []byte, err error), ack func(subject string) error, w io.Writer) (int64, error) {
|
|
var written int64
|
|
for n := int64(1); n <= of; n++ {
|
|
subject, body, err := next()
|
|
if err != nil {
|
|
return written, fmt.Errorf("chunk %d of %d: %w", n, of, err)
|
|
}
|
|
if subject != c.Chunk(n) {
|
|
return written, fmt.Errorf("chunk %d of %d came on %s, not %s", n, of, subject, c.Chunk(n))
|
|
}
|
|
var ch SeatChunk
|
|
if err := json.Unmarshal(body, &ch); err != nil {
|
|
return written, fmt.Errorf("chunk %d of %d does not read: %w", n, of, err)
|
|
}
|
|
if ch.N != n || ch.Of != of {
|
|
return written, fmt.Errorf("chunk %d of %d says it is %d of %d", n, of, ch.N, ch.Of)
|
|
}
|
|
k, err := w.Write(ch.Data)
|
|
written += int64(k)
|
|
if err != nil {
|
|
return written, fmt.Errorf("chunk %d of %d cannot be written: %w", n, of, err)
|
|
}
|
|
if err := ack(c.Ack(n)); err != nil {
|
|
return written, fmt.Errorf("chunk %d of %d cannot be acknowledged: %w", n, of, err)
|
|
}
|
|
}
|
|
return written, nil
|
|
}
|
|
|
|
// ChunkLine is the core connection as a send of chunks reaches it, which a *nats.Conn is: a chunk is published on its subject and
|
|
// kept on no stream, and the acknowledgements are subscribed to, since the caller asks each as a request.
|
|
type ChunkLine interface {
|
|
Publish(subject string, data []byte) error
|
|
Subscribe(subject string, cb nats.MsgHandler) (*nats.Subscription, error)
|
|
}
|
|
|
|
// Send sends body in chunks of at most SeatChunkBytes, the holder's side of Receive (design 47 G §681, A-139, A-237): it subscribes
|
|
// to the verb's acknowledgements (SeatAcks), answers each as the request it is, publishes the nth chunk on Chunk(n) only once the
|
|
// n-1th is acknowledged on Ack(n-1), and ends the subscription when the send ends. An acknowledgement that does not come within
|
|
// `within` (SeatAckWithin where the caller has no other bound) ends the send with ErrSeatChunkMissed, and logf says where. Another
|
|
// call's acknowledgements are skipped. It blocks until the send ends, so a verb answers its chunk count first and runs it after.
|
|
func (c SeatChunks) Send(line ChunkLine, body []byte, within time.Duration, logf func(string, ...any)) error {
|
|
if logf == nil {
|
|
logf = func(string, ...any) {}
|
|
}
|
|
acks := make(chan string, 1)
|
|
sub, err := line.Subscribe(SeatAcks(c.Seat, c.Verb), func(m *nats.Msg) {
|
|
if m.Reply != "" {
|
|
_ = m.Respond([]byte("{}"))
|
|
}
|
|
select {
|
|
case acks <- m.Subject:
|
|
default:
|
|
}
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("the acknowledgements cannot be heard, so no chunk is sent: %w", err)
|
|
}
|
|
defer func() { _ = sub.Unsubscribe() }()
|
|
total := int64((len(body) + SeatChunkBytes - 1) / SeatChunkBytes)
|
|
for n := int64(1); n <= total; n++ {
|
|
from := (n - 1) * SeatChunkBytes
|
|
to := min(int64(len(body)), from+SeatChunkBytes)
|
|
data, err := json.Marshal(SeatChunk{N: n, Of: total, Data: body[from:to]})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := line.Publish(c.Chunk(n), data); err != nil {
|
|
logf("the send %s of %s ends at chunk %d of %d: %v", c.Call, c.Verb, n, total, err)
|
|
return fmt.Errorf("chunk %d of %d: %w", n, total, err)
|
|
}
|
|
if !c.acknowledged(acks, c.Ack(n), within) {
|
|
logf("the send %s of %s ends at chunk %d of %d: no acknowledgement within %s", c.Call, c.Verb, n, total, within)
|
|
return fmt.Errorf("chunk %d of %d: %w", n, total, ErrSeatChunkMissed)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// acknowledged waits within for the acknowledgement on subject, skipping another call's.
|
|
func (c SeatChunks) acknowledged(acks <-chan string, subject string, within time.Duration) bool {
|
|
timer := time.NewTimer(within)
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case got := <-acks:
|
|
if got == subject {
|
|
return true
|
|
}
|
|
case <-timer.C:
|
|
return false
|
|
}
|
|
}
|
|
}
|