Skip to content

Commit e88cf42

Browse files
committed
api: add events subscription support
A user can create watcher by the Connection.NewWatcher() call: watcher = conn.NewWatcker("key", func(event WatchEvent) { // The callback code. }) After that, the watcher callback is invoked for the first time. In this case, the callback is triggered whether or not the key has already been broadcast. All subsequent invocations are triggered with box.broadcast() called on the remote host. If a watcher is subscribed for a key that has not been broadcast yet, the callback is triggered only once, after the registration of the watcher. If the key is updated while the watcher callback is running, the callback will be invoked again with the latest value as soon as it returns. Multiple watchers can be created for one key. If you don’t need the watcher anymore, you can unregister it using the Unregister method: watcher.Unregister() The api is similar to net.box implementation [1]. It also adds a BroadcastRequest to make it easier to send broadcast messages. 1. https://www.tarantool.io/en/doc/latest/reference/reference_lua/net_box/#conn-watch Closes #119
1 parent 1d69898 commit e88cf42

21 files changed

+1569
-70
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1111
### Added
1212

1313
- Support iproto feature discovery (#120).
14+
- Event subscription support (#119)
1415

1516
### Changed
1617

connection.go

+291-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ const (
5454
// LogUnexpectedResultId is logged when response with unknown id was received.
5555
// Most probably it is due to request timeout.
5656
LogUnexpectedResultId
57+
// LogReadWatchEventFailed is logged when failed to read a watch event.
58+
LogReadWatchEventFailed
5759
)
5860

5961
// ConnEvent is sent throw Notify channel specified in Opts.
@@ -63,6 +65,12 @@ type ConnEvent struct {
6365
When time.Time
6466
}
6567

68+
// A raw watch event.
69+
type connWatchEvent struct {
70+
key string
71+
value interface{}
72+
}
73+
6674
var epoch = time.Now()
6775

6876
// Logger is logger type expected to be passed in options.
@@ -84,6 +92,9 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac
8492
case LogUnexpectedResultId:
8593
resp := v[0].(*Response)
8694
log.Printf("tarantool: connection %s got unexpected resultId (%d) in response", conn.addr, resp.RequestId)
95+
case LogReadWatchEventFailed:
96+
err := v[0].(error)
97+
log.Printf("tarantool: unable to parse watch event: %s", err)
8798
default:
8899
args := append([]interface{}{"tarantool: unexpected event ", event, conn}, v...)
89100
log.Print(args...)
@@ -149,6 +160,8 @@ type Connection struct {
149160
lastStreamId uint64
150161

151162
serverProtocolInfo ProtocolInfo
163+
// watchMap is a map of key -> watchSharedData.
164+
watchMap sync.Map
152165
}
153166

154167
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
@@ -531,7 +544,7 @@ func (conn *Connection) dial() (err error) {
531544
return fmt.Errorf("identify: %w", err)
532545
}
533546

534-
// Auth
547+
// Auth.
535548
if opts.User != "" {
536549
scr, err := scramble(conn.Greeting.auth, opts.Pass)
537550
if err != nil {
@@ -549,7 +562,34 @@ func (conn *Connection) dial() (err error) {
549562
}
550563
}
551564

552-
// Only if connected and authenticated.
565+
// Watchers.
566+
watchersChecked := false
567+
conn.watchMap.Range(func(key, value interface{}) bool {
568+
if !watchersChecked {
569+
570+
watchersChecked = true
571+
}
572+
573+
st := value.(chan watchState)
574+
state := <-st
575+
if state.cnt > 0 {
576+
req := newWatchRequest(key.(string))
577+
if err = conn.writeRequest(w, req); err != nil {
578+
st <- state
579+
return false
580+
}
581+
state.init = true
582+
state.ack = true
583+
}
584+
st <- state
585+
return true
586+
})
587+
588+
if err != nil {
589+
return fmt.Errorf("unable to register watch: %w", err)
590+
}
591+
592+
// Only if connected and fully initialized.
553593
conn.lockShards()
554594
conn.c = connection
555595
atomic.StoreUint32(&conn.state, connConnected)
@@ -843,7 +883,50 @@ func (conn *Connection) writer(w *bufio.Writer, c net.Conn) {
843883
}
844884
}
845885

886+
func readWatchEvent(reader io.Reader) (connWatchEvent, error) {
887+
keyExist := false
888+
event := connWatchEvent{}
889+
d := newDecoder(reader)
890+
891+
if l, err := d.DecodeMapLen(); err == nil {
892+
for ; l > 0; l-- {
893+
if cd, err := d.DecodeInt(); err == nil {
894+
switch cd {
895+
case KeyEvent:
896+
if event.key, err = d.DecodeString(); err != nil {
897+
return event, err
898+
}
899+
keyExist = true
900+
case KeyEventData:
901+
if event.value, err = d.DecodeInterface(); err != nil {
902+
return event, err
903+
}
904+
default:
905+
if err = d.Skip(); err != nil {
906+
return event, err
907+
}
908+
}
909+
} else {
910+
return event, err
911+
}
912+
}
913+
} else {
914+
return event, err
915+
}
916+
917+
if !keyExist {
918+
return event, errors.New("watch event does not have a key")
919+
}
920+
921+
return event, nil
922+
}
923+
846924
func (conn *Connection) reader(r *bufio.Reader, c net.Conn) {
925+
events := make(chan connWatchEvent, 1024)
926+
defer close(events)
927+
928+
go conn.eventer(events)
929+
847930
for atomic.LoadUint32(&conn.state) != connClosed {
848931
respBytes, err := conn.read(r)
849932
if err != nil {
@@ -858,7 +941,14 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) {
858941
}
859942

860943
var fut *Future = nil
861-
if resp.Code == PushCode {
944+
if resp.Code == EventCode {
945+
if event, err := readWatchEvent(&resp.buf); err == nil {
946+
events <- event
947+
} else {
948+
conn.opts.Logger.Report(LogReadWatchEventFailed, conn, err)
949+
}
950+
continue
951+
} else if resp.Code == PushCode {
862952
if fut = conn.peekFuture(resp.RequestId); fut != nil {
863953
fut.AppendPush(resp)
864954
}
@@ -868,12 +958,37 @@ func (conn *Connection) reader(r *bufio.Reader, c net.Conn) {
868958
conn.markDone(fut)
869959
}
870960
}
961+
871962
if fut == nil {
872963
conn.opts.Logger.Report(LogUnexpectedResultId, conn, resp)
873964
}
874965
}
875966
}
876967

968+
// eventer goroutine gets watch events and updates values for watchers.
969+
func (conn *Connection) eventer(events <-chan connWatchEvent) {
970+
for {
971+
event, ok := <-events
972+
if !ok {
973+
// The channel is closed.
974+
break
975+
}
976+
977+
if value, ok := conn.watchMap.Load(event.key); ok {
978+
st := value.(chan watchState)
979+
state := <-st
980+
state.value = event.value
981+
state.init = false
982+
state.ack = false
983+
if state.changed != nil {
984+
close(state.changed)
985+
state.changed = nil
986+
}
987+
st <- state
988+
}
989+
}
990+
}
991+
877992
func (conn *Connection) newFuture(ctx context.Context) (fut *Future) {
878993
fut = NewFuture()
879994
if conn.rlimit != nil && conn.opts.RLimitAction == RLimitDrop {
@@ -1029,6 +1144,18 @@ func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) {
10291144
return
10301145
}
10311146
shard.bufmut.Unlock()
1147+
1148+
if req.Async() {
1149+
if fut = conn.fetchFuture(reqid); fut != nil {
1150+
resp := &Response{
1151+
RequestId: reqid,
1152+
Code: OkCode,
1153+
}
1154+
fut.SetResponse(resp)
1155+
conn.markDone(fut)
1156+
}
1157+
}
1158+
10321159
if firstWritten {
10331160
conn.dirtyShard <- shardn
10341161
}
@@ -1233,6 +1360,167 @@ func (conn *Connection) NewStream() (*Stream, error) {
12331360
}, nil
12341361
}
12351362

1363+
// watchState is the current state of the watcher. See the idea at p. 70, 105:
1364+
// https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view
1365+
type watchState struct {
1366+
// value is a current value.
1367+
value interface{}
1368+
// init is true if it is an initial state (no events received).
1369+
init bool
1370+
// ack true if the acknowledge is already sended.
1371+
ack bool
1372+
// cnt is a count of active watchers for the key.
1373+
cnt int
1374+
// changed is a channel for broadcast the value changes.
1375+
changed chan struct{}
1376+
}
1377+
1378+
// connWatcher is an internal implementation of the Watcher interface.
1379+
type connWatcher struct {
1380+
unregister sync.Once
1381+
done chan struct{}
1382+
finished chan struct{}
1383+
}
1384+
1385+
// Unregister unregisters the connection watcher.
1386+
func (w *connWatcher) Unregister() {
1387+
w.unregister.Do(func() {
1388+
close(w.done)
1389+
})
1390+
<-w.finished
1391+
}
1392+
1393+
// NewWatcher creates a new Watcher object for the connection.
1394+
//
1395+
// After watcher creation, the watcher callback is invoked for the first time.
1396+
// In this case, the callback is triggered whether or not the key has already
1397+
// been broadcast. All subsequent invocations are triggered with
1398+
// box.broadcast() called on the remote host. If a watcher is subscribed for a
1399+
// key that has not been broadcast yet, the callback is triggered only once,
1400+
// after the registration of the watcher.
1401+
//
1402+
// The watcher callbacks are always invoked in a separate goroutine. A watcher
1403+
// callback is never executed in parallel with itself, but they can be executed
1404+
// in parallel to other watchers.
1405+
//
1406+
// If the key is updated while the watcher callback is running, the callback
1407+
// will be invoked again with the latest value as soon as it returns.
1408+
//
1409+
// Watchers survive reconnection. All registered watchers are automatically
1410+
// resubscribed when the connection is reestablished.
1411+
//
1412+
// Keep in mind that garbage collection of a watcher handle doesn’t lead to the
1413+
// watcher’s destruction. In this case, the watcher remains registered. You
1414+
// need to call Unregister() directly.
1415+
//
1416+
// Unregister() guarantees that there will be no the watcher's callback calls
1417+
// after it, but Unregister() call from the callback leads to a deadlock.
1418+
//
1419+
// See:
1420+
// https://www.tarantool.io/en/doc/latest/reference/reference_lua/box_events/#box-watchers
1421+
//
1422+
// Since 1.10.0
1423+
func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher, error) {
1424+
watchersSupported := false
1425+
for _, feature := range conn.ServerProtocolInfo().Features {
1426+
if feature == WatchersFeature {
1427+
watchersSupported = true
1428+
break
1429+
}
1430+
}
1431+
if !watchersSupported {
1432+
return nil, errors.New("watchers does not supported by a server")
1433+
}
1434+
1435+
var st chan watchState
1436+
// Get or create a shared data for the key.
1437+
if val, ok := conn.watchMap.Load(key); !ok {
1438+
st = make(chan watchState, 1)
1439+
st <- watchState{
1440+
value: nil,
1441+
init: true,
1442+
ack: false,
1443+
cnt: 0,
1444+
changed: nil,
1445+
}
1446+
1447+
if val, ok := conn.watchMap.LoadOrStore(key, st); ok {
1448+
close(st)
1449+
st = val.(chan watchState)
1450+
}
1451+
} else {
1452+
st = val.(chan watchState)
1453+
}
1454+
1455+
state := <-st
1456+
// Send an initial watch request if needed.
1457+
if state.cnt == 0 {
1458+
if _, err := conn.Do(newWatchRequest(key)).Get(); err != nil {
1459+
st <- state
1460+
return nil, err
1461+
}
1462+
state.init = true
1463+
state.ack = true
1464+
}
1465+
state.cnt += 1
1466+
st <- state
1467+
1468+
// Start the watcher goroutine.
1469+
done := make(chan struct{})
1470+
finished := make(chan struct{})
1471+
1472+
go func() {
1473+
for {
1474+
state := <-st
1475+
if state.changed == nil {
1476+
state.changed = make(chan struct{})
1477+
}
1478+
st <- state
1479+
1480+
if !state.init {
1481+
callback(WatchEvent{
1482+
Conn: conn,
1483+
Key: key,
1484+
Value: state.value,
1485+
})
1486+
1487+
// Acknowledge the notification.
1488+
state = <-st
1489+
ack := state.ack
1490+
state.ack = true
1491+
st <- state
1492+
1493+
if !ack {
1494+
conn.Do(newWatchRequest(key)).Get()
1495+
// We expect a reconnect and re-subscribe if it fails to
1496+
// send the watch request. So it looks ok do not check a
1497+
// result.
1498+
}
1499+
}
1500+
1501+
select {
1502+
case <-done:
1503+
state := <-st
1504+
state.cnt -= 1
1505+
if state.cnt == 0 {
1506+
// The last one sends IPROTO_UNWATCH.
1507+
conn.Do(newUnwatchRequest(key)).Get()
1508+
}
1509+
st <- state
1510+
1511+
close(finished)
1512+
return
1513+
case <-state.changed:
1514+
}
1515+
}
1516+
}()
1517+
1518+
return &connWatcher{
1519+
done: done,
1520+
finished: finished,
1521+
}, nil
1522+
}
1523+
12361524
// checkProtocolInfo checks that expected protocol version is
12371525
// and protocol features are supported.
12381526
func checkProtocolInfo(expected ProtocolInfo, actual ProtocolInfo) error {

0 commit comments

Comments
 (0)