744 lines
20 KiB
Go
744 lines
20 KiB
Go
package ca
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/uopi/goca/proto"
|
|
)
|
|
|
|
const (
|
|
writeQueueDepth = 256
|
|
echoInterval = 15 * time.Second
|
|
connectTimeout = 5 * time.Second
|
|
maxReconnDelay = 60 * time.Second
|
|
)
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// reply — async GET callback payload //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// reply is delivered to a pending GET waiter via a buffered channel.
|
|
// If ok is false the circuit died before the reply arrived.
|
|
type reply struct {
|
|
hdr proto.Header
|
|
payload []byte
|
|
ok bool
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// monState — one active EVENT_ADD subscription //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// monState survives reconnections; the circuit re-sends EVENT_ADD with the
|
|
// new SID each time a CREATE_CHAN reply is received.
|
|
type monState struct {
|
|
subID uint32
|
|
dbrType uint16
|
|
count uint32
|
|
ch chan<- proto.TimeValue // caller-owned, never closed here
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// chanState — one CA channel (persists across reconnections) //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// chanState field invariants:
|
|
// - cid, pvName: immutable after construction.
|
|
// - sid, dbfType, count, access: valid only while readyC is closed.
|
|
// - readyC: closed when CREATE_CHAN reply received; replaced on reconnect.
|
|
// - monitors: append-only (except removeMonitor); never cleared on reconnect.
|
|
// - pending: ioid → GET reply channel; drained (channels closed) on reconnect.
|
|
type chanState struct {
|
|
cid uint32
|
|
pvName string
|
|
|
|
mu sync.RWMutex
|
|
sid uint32 // server-assigned channel ID (0 until CREATE_CHAN reply)
|
|
dbfType int // native DBF field type
|
|
count uint32 // element count
|
|
access uint32 // AccessRead | AccessWrite bitmask
|
|
readyC chan struct{} // closed once CREATE_CHAN reply is received
|
|
monitors []*monState // active subscriptions
|
|
pending map[uint32]chan reply // ioid → one-shot GET callback
|
|
}
|
|
|
|
func newChanState(cid uint32, pvName string) *chanState {
|
|
return &chanState{
|
|
cid: cid,
|
|
pvName: pvName,
|
|
readyC: make(chan struct{}),
|
|
pending: make(map[uint32]chan reply),
|
|
}
|
|
}
|
|
|
|
// resetForReconnect prepares cs for a new TCP connection.
|
|
// It closes the current readyC (waking any waitReady callers so they can
|
|
// re-wait on the fresh channel) and then replaces it.
|
|
// Must be called before the circuit sends CREATE_CHAN on the new conn.
|
|
func (cs *chanState) resetForReconnect() {
|
|
cs.mu.Lock()
|
|
cs.sid = 0
|
|
old := cs.readyC
|
|
cs.readyC = make(chan struct{})
|
|
// Drain pending GETs — they will receive zero reply (ok=false).
|
|
for id, ch := range cs.pending {
|
|
close(ch)
|
|
delete(cs.pending, id)
|
|
}
|
|
cs.mu.Unlock()
|
|
// Close the old readyC outside the lock to avoid deadlock with waitReady.
|
|
select {
|
|
case <-old:
|
|
// Already closed (CmdCreateFail path or double-reset guard).
|
|
default:
|
|
close(old)
|
|
}
|
|
}
|
|
|
|
// waitReady blocks until sid != 0 (CREATE_CHAN reply received for the current
|
|
// connection) or ctx expires. It loops through reconnections automatically:
|
|
// when resetForReconnect closes the current readyC the goroutine wakes, sees
|
|
// sid == 0, and waits on the freshly created channel.
|
|
func (cs *chanState) waitReady(ctx context.Context) error {
|
|
for {
|
|
cs.mu.RLock()
|
|
sid := cs.sid
|
|
ready := cs.readyC
|
|
cs.mu.RUnlock()
|
|
|
|
if sid != 0 {
|
|
return nil
|
|
}
|
|
|
|
select {
|
|
case <-ready:
|
|
// State changed — either CREATE_CHAN reply (sid set) or reconnect
|
|
// started (sid cleared, new readyC installed). Loop to check.
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// circuit — persistent TCP connection to one CA server //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// circuit manages a persistent, auto-reconnecting TCP connection to a single
|
|
// CA server. All channels and monitors created on a circuit survive
|
|
// reconnections; the circuit re-creates them transparently.
|
|
//
|
|
// Goroutine model:
|
|
// - run() goroutine: reconnect loop; drives the write loop for each conn.
|
|
// - readLoop() goroutine: one per active conn; dispatches inbound messages.
|
|
//
|
|
// Lock ordering: circuit.mu → chanState.mu (never the reverse).
|
|
type circuit struct {
|
|
addr string
|
|
clientName string
|
|
hostName string
|
|
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
mu sync.RWMutex
|
|
channels []*chanState // append-only; survive reconnect
|
|
byCID map[uint32]*chanState // cid → chanState (fast lookup)
|
|
bySubID map[uint32]*monState // subID → monState (fast event dispatch)
|
|
|
|
writeQ chan []byte // serialised outbound message queue
|
|
seq atomic.Uint32 // shared ID sequence (cid, ioid, subID)
|
|
}
|
|
|
|
func newCircuit(ctx context.Context, addr, clientName, hostName string) *circuit {
|
|
cctx, cancel := context.WithCancel(ctx)
|
|
c := &circuit{
|
|
addr: addr,
|
|
clientName: clientName,
|
|
hostName: hostName,
|
|
ctx: cctx,
|
|
cancel: cancel,
|
|
byCID: make(map[uint32]*chanState),
|
|
bySubID: make(map[uint32]*monState),
|
|
writeQ: make(chan []byte, writeQueueDepth),
|
|
}
|
|
go c.run()
|
|
return c
|
|
}
|
|
|
|
func (c *circuit) close() { c.cancel() }
|
|
|
|
func (c *circuit) nextID() uint32 { return c.seq.Add(1) }
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Reconnect loop //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func (c *circuit) run() {
|
|
delay := time.Second
|
|
for {
|
|
if c.ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
dbg("CA circuit connecting", "addr", c.addr)
|
|
conn, err := c.dial()
|
|
if err != nil {
|
|
dbg("CA circuit dial failed", "addr", c.addr, "err", err)
|
|
select {
|
|
case <-time.After(delay):
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
delay = min(delay*2, maxReconnDelay)
|
|
continue
|
|
}
|
|
dbg("CA circuit connected", "addr", c.addr)
|
|
delay = time.Second // reset back-off on successful connect
|
|
|
|
// Snapshot channels and reset their per-connection state.
|
|
c.mu.Lock()
|
|
for _, cs := range c.channels {
|
|
cs.resetForReconnect()
|
|
}
|
|
chs := make([]*chanState, len(c.channels))
|
|
copy(chs, c.channels)
|
|
c.mu.Unlock()
|
|
|
|
// Drain stale messages from the write queue (they carry old SIDs).
|
|
for len(c.writeQ) > 0 {
|
|
<-c.writeQ
|
|
}
|
|
|
|
// Send VERSION + HOST_NAME + CLIENT_NAME.
|
|
if err := c.sendHandshake(conn); err != nil {
|
|
conn.Close()
|
|
continue
|
|
}
|
|
|
|
// Re-create all known channels on the new connection.
|
|
setupOK := true
|
|
for _, cs := range chs {
|
|
if _, err := conn.Write(buildCreateChanMsg(cs.cid, cs.pvName)); err != nil {
|
|
setupOK = false
|
|
break
|
|
}
|
|
}
|
|
if !setupOK {
|
|
conn.Close()
|
|
continue
|
|
}
|
|
|
|
dbg("CA handshake sent", "addr", c.addr, "channels", len(chs))
|
|
|
|
// Start read loop.
|
|
readDone := make(chan struct{})
|
|
go func() {
|
|
c.readLoop(conn)
|
|
dbg("CA read loop exited", "addr", c.addr)
|
|
close(readDone)
|
|
}()
|
|
|
|
// Write loop (run goroutine acts as the write pump).
|
|
c.writeLoop(conn, readDone)
|
|
|
|
conn.Close()
|
|
<-readDone // drain read goroutine before reconnecting
|
|
|
|
if c.ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-time.After(delay):
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
delay = min(delay*2, maxReconnDelay)
|
|
}
|
|
}
|
|
|
|
func (c *circuit) writeLoop(conn net.Conn, readDone <-chan struct{}) {
|
|
ticker := time.NewTicker(echoInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case msg := <-c.writeQ:
|
|
if _, err := conn.Write(msg); err != nil {
|
|
return
|
|
}
|
|
case <-ticker.C:
|
|
// Periodic heartbeat; server echoes it back.
|
|
hb := proto.BuildMessage(proto.Header{Command: proto.CmdEcho}, nil)
|
|
if _, err := conn.Write(hb); err != nil {
|
|
return
|
|
}
|
|
case <-readDone:
|
|
return
|
|
case <-c.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Dial + handshake //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func (c *circuit) dial() (net.Conn, error) {
|
|
d := net.Dialer{Timeout: connectTimeout}
|
|
conn, err := d.DialContext(c.ctx, "tcp4", c.addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ca: dial %s: %w", c.addr, err)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (c *circuit) sendHandshake(conn net.Conn) error {
|
|
ver := proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdVersion,
|
|
DataCount: proto.MinorVersion,
|
|
}, nil)
|
|
host := proto.BuildMessage(
|
|
proto.Header{Command: proto.CmdHostName},
|
|
proto.BuildStringPayload(c.hostName),
|
|
)
|
|
client := proto.BuildMessage(
|
|
proto.Header{Command: proto.CmdClientName},
|
|
proto.BuildStringPayload(c.clientName),
|
|
)
|
|
out := make([]byte, 0, len(ver)+len(host)+len(client))
|
|
out = append(out, ver...)
|
|
out = append(out, host...)
|
|
out = append(out, client...)
|
|
_, err := conn.Write(out)
|
|
return err
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Read loop + message dispatch //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func (c *circuit) readLoop(conn net.Conn) {
|
|
for {
|
|
hdr, _, err := proto.DecodeHeader(conn)
|
|
if err != nil {
|
|
if c.ctx.Err() == nil {
|
|
dbg("CA read loop error (header)", "addr", c.addr, "err", err)
|
|
}
|
|
return
|
|
}
|
|
dbg("CA recv", "addr", c.addr, "cmd", hdr.Command, "dataType", hdr.DataType,
|
|
"dataCount", hdr.DataCount, "payloadSize", hdr.PayloadSize,
|
|
"p1", hdr.Parameter1, "p2", hdr.Parameter2)
|
|
var payload []byte
|
|
if hdr.PayloadSize > 0 {
|
|
payload = make([]byte, hdr.PayloadSize)
|
|
if _, err = io.ReadFull(conn, payload); err != nil {
|
|
if c.ctx.Err() == nil {
|
|
dbg("CA read loop error (payload)", "addr", c.addr,
|
|
"cmd", hdr.Command, "wantBytes", hdr.PayloadSize, "err", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
c.dispatch(conn, hdr, payload)
|
|
}
|
|
}
|
|
|
|
// dispatch handles one inbound CA message.
|
|
func (c *circuit) dispatch(conn net.Conn, hdr proto.Header, payload []byte) {
|
|
switch hdr.Command {
|
|
|
|
case proto.CmdVersion:
|
|
// Server version negotiation reply — nothing to do.
|
|
|
|
case proto.CmdCreateChan:
|
|
// Parameter1 = cid (echoed), Parameter2 = SID assigned by server.
|
|
c.mu.RLock()
|
|
cs, ok := c.byCID[hdr.Parameter1]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return
|
|
}
|
|
cs.mu.Lock()
|
|
cs.sid = hdr.Parameter2
|
|
cs.dbfType = int(hdr.DataType)
|
|
cs.count = hdr.DataCount
|
|
ready := cs.readyC
|
|
// Snapshot monitors for EVENT_ADD re-subscription.
|
|
mons := make([]*monState, len(cs.monitors))
|
|
copy(mons, cs.monitors)
|
|
cs.mu.Unlock()
|
|
|
|
dbg("CA CREATE_CHAN reply", "pv", cs.pvName, "sid", hdr.Parameter2,
|
|
"dbfType", hdr.DataType, "count", hdr.DataCount, "monitors", len(mons))
|
|
|
|
// Re-subscribe all monitors with the new SID.
|
|
for _, ms := range mons {
|
|
msg := buildEventAddMsg(ms.subID, hdr.Parameter2, ms.dbrType, ms.count)
|
|
dbg("CA EVENT_ADD sent", "pv", cs.pvName, "subID", ms.subID,
|
|
"sid", hdr.Parameter2, "dbrType", ms.dbrType, "count", ms.count)
|
|
select {
|
|
case c.writeQ <- msg:
|
|
default:
|
|
}
|
|
}
|
|
close(ready) // unblock waitReady callers
|
|
|
|
case proto.CmdCreateFail:
|
|
// Server does not host this PV (or quota exceeded).
|
|
c.mu.RLock()
|
|
cs, ok := c.byCID[hdr.Parameter1]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return
|
|
}
|
|
dbg("CA CREATE_FAIL", "pv", cs.pvName, "cid", hdr.Parameter1)
|
|
cs.mu.RLock()
|
|
ready := cs.readyC
|
|
cs.mu.RUnlock()
|
|
// readyC is only closed once per connection cycle by this goroutine,
|
|
// so the select here is safe (no concurrent close).
|
|
select {
|
|
case <-ready:
|
|
// Already closed (shouldn't happen, but guard against it).
|
|
default:
|
|
close(ready)
|
|
}
|
|
|
|
case proto.CmdAccessRights:
|
|
// Parameter1 = cid, Parameter2 = access bitmask.
|
|
c.mu.RLock()
|
|
cs, ok := c.byCID[hdr.Parameter1]
|
|
c.mu.RUnlock()
|
|
if ok {
|
|
cs.mu.Lock()
|
|
cs.access = hdr.Parameter2
|
|
cs.mu.Unlock()
|
|
}
|
|
|
|
case proto.CmdEventAdd:
|
|
// Monitor update: Parameter1 = subscriptionID.
|
|
c.mu.RLock()
|
|
ms, ok := c.bySubID[hdr.Parameter1]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
dbg("CA EVENT_ADD for unknown subID", "subID", hdr.Parameter1)
|
|
return
|
|
}
|
|
tv, ok := proto.DecodeTimeValue(hdr.DataType, hdr.DataCount, payload)
|
|
if !ok {
|
|
dbg("CA EVENT_ADD decode failed", "subID", hdr.Parameter1,
|
|
"dbrType", hdr.DataType, "count", hdr.DataCount, "payloadLen", len(payload))
|
|
return
|
|
}
|
|
dbg("CA EVENT_ADD value", "subID", hdr.Parameter1, "dbrType", hdr.DataType,
|
|
"double", tv.Double, "severity", tv.Severity)
|
|
select {
|
|
case ms.ch <- tv:
|
|
default: // drop if consumer is slow
|
|
}
|
|
|
|
case proto.CmdReadNotify:
|
|
// GET reply: Parameter1 = ioid.
|
|
ioid := hdr.Parameter1
|
|
c.mu.RLock()
|
|
var replyCh chan reply
|
|
var found bool
|
|
for _, cs := range c.channels {
|
|
cs.mu.Lock()
|
|
if ch, exists := cs.pending[ioid]; exists {
|
|
delete(cs.pending, ioid)
|
|
replyCh = ch
|
|
found = true
|
|
}
|
|
cs.mu.Unlock()
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
c.mu.RUnlock()
|
|
if found {
|
|
select {
|
|
case replyCh <- reply{hdr: hdr, payload: payload, ok: true}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
case proto.CmdEcho:
|
|
// Server heartbeat — no response needed (client sends its own echoes).
|
|
|
|
case proto.CmdServerDisc:
|
|
// Server is shutting down — close conn to trigger reconnect.
|
|
conn.Close()
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Message builders //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
func buildCreateChanMsg(cid uint32, pvName string) []byte {
|
|
return proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdCreateChan,
|
|
Parameter1: cid,
|
|
Parameter2: proto.MinorVersion,
|
|
}, proto.BuildStringPayload(pvName))
|
|
}
|
|
|
|
func buildEventAddMsg(subID, sid uint32, dbrType uint16, count uint32) []byte {
|
|
return proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdEventAdd,
|
|
DataType: dbrType,
|
|
DataCount: count,
|
|
Parameter1: sid, // m_cid = channel SID (per CA spec)
|
|
Parameter2: subID, // m_available = client subscription ID
|
|
}, proto.EncodeEventMask(proto.DBEDefault))
|
|
}
|
|
|
|
func buildEventCancelMsg(subID, sid uint32, dbrType uint16) []byte {
|
|
return proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdEventCancel,
|
|
DataType: dbrType,
|
|
Parameter1: sid, // m_cid = channel SID
|
|
Parameter2: subID, // m_available = client subscription ID
|
|
}, nil)
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Channel and monitor management //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// getOrCreateChannel returns the chanState for pvName, creating it if needed.
|
|
// If the circuit is currently connected, CREATE_CHAN is queued immediately;
|
|
// otherwise the reconnect loop will send it when it next connects.
|
|
func (c *circuit) getOrCreateChannel(pvName string) *chanState {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for _, cs := range c.channels {
|
|
if cs.pvName == pvName {
|
|
return cs
|
|
}
|
|
}
|
|
cid := c.nextID()
|
|
cs := newChanState(cid, pvName)
|
|
c.channels = append(c.channels, cs)
|
|
c.byCID[cid] = cs
|
|
// Best-effort: deliver CREATE_CHAN if circuit is up.
|
|
msg := buildCreateChanMsg(cid, pvName)
|
|
select {
|
|
case c.writeQ <- msg:
|
|
default:
|
|
}
|
|
return cs
|
|
}
|
|
|
|
// addMonitor registers ms on cs and queues EVENT_ADD if the channel is ready.
|
|
// If the channel is not yet ready the CREATE_CHAN reply handler will send
|
|
// EVENT_ADD when the connection is established.
|
|
func (c *circuit) addMonitor(cs *chanState, ms *monState) {
|
|
c.mu.Lock()
|
|
c.bySubID[ms.subID] = ms
|
|
c.mu.Unlock()
|
|
|
|
cs.mu.Lock()
|
|
cs.monitors = append(cs.monitors, ms)
|
|
sid := cs.sid
|
|
cs.mu.Unlock()
|
|
|
|
if sid != 0 {
|
|
dbg("CA EVENT_ADD sent (addMonitor)", "pv", cs.pvName, "subID", ms.subID,
|
|
"sid", sid, "dbrType", ms.dbrType, "count", ms.count)
|
|
msg := buildEventAddMsg(ms.subID, sid, ms.dbrType, ms.count)
|
|
select {
|
|
case c.writeQ <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// removeMonitor unregisters a monitor and queues EVENT_CANCEL if connected.
|
|
func (c *circuit) removeMonitor(cs *chanState, subID uint32) {
|
|
c.mu.Lock()
|
|
delete(c.bySubID, subID)
|
|
c.mu.Unlock()
|
|
|
|
cs.mu.Lock()
|
|
var dbrType uint16
|
|
remaining := cs.monitors[:0:0] // fresh backing array
|
|
for _, ms := range cs.monitors {
|
|
if ms.subID == subID {
|
|
dbrType = ms.dbrType
|
|
} else {
|
|
remaining = append(remaining, ms)
|
|
}
|
|
}
|
|
cs.monitors = remaining
|
|
sid := cs.sid
|
|
cs.mu.Unlock()
|
|
|
|
if sid != 0 {
|
|
msg := buildEventCancelMsg(subID, sid, dbrType)
|
|
select {
|
|
case c.writeQ <- msg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------- //
|
|
// Channel operations: get and put //
|
|
// -------------------------------------------------------------------------- //
|
|
|
|
// getRaw performs a READ_NOTIFY and returns the undecoded header + payload.
|
|
// Use this when you need to decode a type that get() does not handle
|
|
// (e.g. DBR_CTRL_* for metadata retrieval).
|
|
func (c *circuit) getRaw(ctx context.Context, cs *chanState, dbrType uint16, count uint32) (proto.Header, []byte, error) {
|
|
if err := cs.waitReady(ctx); err != nil {
|
|
return proto.Header{}, nil, fmt.Errorf("ca: %q: %w", cs.pvName, err)
|
|
}
|
|
|
|
cs.mu.RLock()
|
|
sid := cs.sid
|
|
cs.mu.RUnlock()
|
|
if sid == 0 {
|
|
return proto.Header{}, nil, fmt.Errorf("ca: %q: channel not connected", cs.pvName)
|
|
}
|
|
|
|
ioid := c.nextID()
|
|
replyCh := make(chan reply, 1)
|
|
|
|
cs.mu.Lock()
|
|
cs.pending[ioid] = replyCh
|
|
cs.mu.Unlock()
|
|
|
|
msg := proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdReadNotify,
|
|
DataType: dbrType,
|
|
DataCount: count,
|
|
Parameter1: sid, // m_cid = channel SID (per CA spec)
|
|
Parameter2: ioid, // m_available = request IOId
|
|
}, nil)
|
|
|
|
select {
|
|
case c.writeQ <- msg:
|
|
case <-ctx.Done():
|
|
cs.mu.Lock()
|
|
delete(cs.pending, ioid)
|
|
cs.mu.Unlock()
|
|
return proto.Header{}, nil, ctx.Err()
|
|
}
|
|
|
|
select {
|
|
case r := <-replyCh:
|
|
if !r.ok {
|
|
return proto.Header{}, nil, fmt.Errorf("ca: %q: circuit disconnected", cs.pvName)
|
|
}
|
|
return r.hdr, r.payload, nil
|
|
case <-ctx.Done():
|
|
cs.mu.Lock()
|
|
delete(cs.pending, ioid)
|
|
cs.mu.Unlock()
|
|
return proto.Header{}, nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// get performs a READ_NOTIFY (async GET) and returns the decoded value.
|
|
// It blocks until the reply arrives, the channel is not yet ready, or ctx expires.
|
|
func (c *circuit) get(ctx context.Context, cs *chanState, dbrType uint16, count uint32) (proto.TimeValue, error) {
|
|
if err := cs.waitReady(ctx); err != nil {
|
|
return proto.TimeValue{}, fmt.Errorf("ca: %q: %w", cs.pvName, err)
|
|
}
|
|
|
|
cs.mu.RLock()
|
|
sid := cs.sid
|
|
cs.mu.RUnlock()
|
|
if sid == 0 {
|
|
return proto.TimeValue{}, fmt.Errorf("ca: %q: channel not connected", cs.pvName)
|
|
}
|
|
|
|
ioid := c.nextID()
|
|
replyCh := make(chan reply, 1)
|
|
|
|
cs.mu.Lock()
|
|
cs.pending[ioid] = replyCh
|
|
cs.mu.Unlock()
|
|
|
|
msg := proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdReadNotify,
|
|
DataType: dbrType,
|
|
DataCount: count,
|
|
Parameter1: sid, // m_cid = channel SID (per CA spec)
|
|
Parameter2: ioid, // m_available = request IOId
|
|
}, nil)
|
|
|
|
select {
|
|
case c.writeQ <- msg:
|
|
case <-ctx.Done():
|
|
cs.mu.Lock()
|
|
delete(cs.pending, ioid)
|
|
cs.mu.Unlock()
|
|
return proto.TimeValue{}, ctx.Err()
|
|
}
|
|
|
|
select {
|
|
case r := <-replyCh:
|
|
if !r.ok {
|
|
return proto.TimeValue{}, fmt.Errorf("ca: %q: circuit disconnected during GET", cs.pvName)
|
|
}
|
|
tv, ok := proto.DecodeTimeValue(r.hdr.DataType, r.hdr.DataCount, r.payload)
|
|
if !ok {
|
|
return proto.TimeValue{}, fmt.Errorf("ca: %q: failed to decode GET reply", cs.pvName)
|
|
}
|
|
return tv, nil
|
|
case <-ctx.Done():
|
|
cs.mu.Lock()
|
|
delete(cs.pending, ioid)
|
|
cs.mu.Unlock()
|
|
return proto.TimeValue{}, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// put queues a CA_PROTO_WRITE (fire-and-forget put).
|
|
// It waits for the channel to be ready and access rights to be confirmed.
|
|
func (c *circuit) put(ctx context.Context, cs *chanState, dbrType uint16, payload []byte) error {
|
|
if err := cs.waitReady(ctx); err != nil {
|
|
return fmt.Errorf("ca: %q: %w", cs.pvName, err)
|
|
}
|
|
|
|
cs.mu.RLock()
|
|
sid := cs.sid
|
|
access := cs.access
|
|
cs.mu.RUnlock()
|
|
|
|
if sid == 0 {
|
|
return fmt.Errorf("ca: %q: channel not connected", cs.pvName)
|
|
}
|
|
if access&proto.AccessWrite == 0 {
|
|
return fmt.Errorf("ca: %q: read-only", cs.pvName)
|
|
}
|
|
|
|
// CA_PROTO_WRITE: DataType=dbrType, DataCount=1, Parameter1=SID, Parameter2=0 (no ioid).
|
|
msg := proto.BuildMessage(proto.Header{
|
|
Command: proto.CmdWrite,
|
|
DataType: dbrType,
|
|
DataCount: 1,
|
|
Parameter1: sid,
|
|
Parameter2: 0,
|
|
}, payload)
|
|
|
|
select {
|
|
case c.writeQ <- msg:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|