Skip to content

Commit 637be3b

Browse files
feature: support graceful shutdown
If connected to Tarantool 2.10 or newer and WatchersFeature is required, after this patch connection supports server graceful shutdown [1]. In this case, server will wait until all client requests will be finished and client disconnects before going down (server also may go down by timeout). Client reconnect will happen if connection options enable reconnect. 1. https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/ Closes #214
1 parent b93df00 commit 637be3b

File tree

3 files changed

+403
-9
lines changed

3 files changed

+403
-9
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1515
- Error type support in MessagePack (#209)
1616
- Event subscription support (#119)
1717
- Session settings support (#215)
18+
- Support graceful shutdown (#214)
1819

1920
### Changed
2021

connection.go

+102-9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ const (
3333
connTransportSsl = "ssl"
3434
)
3535

36+
const (
37+
shutdownNotInProgress = 0
38+
shutdownInProgress = 1
39+
)
40+
41+
const shutdownEventKey = "box.shutdown"
42+
3643
type ConnEventKind int
3744
type ConnLogKind int
3845

@@ -134,6 +141,14 @@ func (d defaultLogger) Report(event ConnLogKind, conn *Connection, v ...interfac
134141
// always returns array of array (array of tuples for space related methods).
135142
// For Eval* and Call* Tarantool always returns array, but does not forces
136143
// array of arrays.
144+
//
145+
// If connected to Tarantool 2.10 or newer and WatchersFeature is required,
146+
// connection supports server graceful shutdown. In this case, server will
147+
// wait until all client requests will be finished and client disconnects
148+
// before going down (server also may go down by timeout). Client reconnect will
149+
// happen if connection options enable reconnect.
150+
//
151+
// More on graceful shutdown: https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/
137152
type Connection struct {
138153
addr string
139154
c net.Conn
@@ -162,6 +177,13 @@ type Connection struct {
162177
serverProtocolInfo ProtocolInfo
163178
// watchMap is a map of key -> chan watchState.
164179
watchMap sync.Map
180+
181+
// shutdownInProgress defined whether shutdown now in progress.
182+
shutdownInProgress uint32
183+
// shutdownWatcher is the "box.shutdown" event watcher.
184+
shutdownWatcher Watcher
185+
// shutdownWg is the wait group to finish all tasks on shutdown.
186+
shutdownWg sync.WaitGroup
165187
}
166188

167189
var _ = Connector(&Connection{}) // Check compatibility with connector interface.
@@ -373,6 +395,7 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) {
373395
}
374396
}
375397
}
398+
conn.shutdownInProgress = shutdownNotInProgress
376399

377400
if conn.opts.RateLimit > 0 {
378401
conn.rlimit = make(chan struct{}, conn.opts.RateLimit)
@@ -421,6 +444,16 @@ func Connect(addr string, opts Opts) (conn *Connection, err error) {
421444
}
422445
}
423446

447+
// Subscribe shutdown event to process graceful shutdown.
448+
if conn.isWatchersRequired() {
449+
watcher, werr := conn.NewWatcher(shutdownEventKey, shutdownEventCallback)
450+
if werr != nil {
451+
conn.closeConnection(werr, true)
452+
return nil, werr
453+
}
454+
conn.shutdownWatcher = watcher
455+
}
456+
424457
return conn, err
425458
}
426459

@@ -1086,6 +1119,7 @@ func (conn *Connection) send(req Request, streamId uint64) *Future {
10861119
if fut.ready == nil {
10871120
return fut
10881121
}
1122+
10891123
if req.Ctx() != nil {
10901124
select {
10911125
case <-req.Ctx().Done():
@@ -1094,13 +1128,31 @@ func (conn *Connection) send(req Request, streamId uint64) *Future {
10941128
default:
10951129
}
10961130
}
1131+
1132+
if atomic.LoadUint32(&(conn.shutdownInProgress)) == shutdownInProgress {
1133+
conn.cancelFuture(fut, fmt.Errorf("server shutdown in progress"))
1134+
return fut
1135+
}
1136+
10971137
conn.putFuture(fut, req, streamId)
1138+
10981139
if req.Ctx() != nil {
10991140
go conn.contextWatchdog(fut, req.Ctx())
11001141
}
1142+
1143+
if conn.shutdownWatcher != nil {
1144+
go conn.gracefulWait(fut)
1145+
}
1146+
11011147
return fut
11021148
}
11031149

1150+
func (conn *Connection) gracefulWait(fut *Future) {
1151+
conn.shutdownWg.Add(1)
1152+
<-fut.done
1153+
conn.shutdownWg.Done()
1154+
}
1155+
11041156
func (conn *Connection) putFuture(fut *Future, req Request, streamId uint64) {
11051157
shardn := fut.requestId & (conn.opts.Concurrency - 1)
11061158
shard := &conn.shard[shardn]
@@ -1458,6 +1510,15 @@ func subscribeWatchChannel(conn *Connection, key string) (chan watchState, error
14581510
return st, nil
14591511
}
14601512

1513+
func (conn *Connection) isWatchersRequired() bool {
1514+
for _, feature := range conn.opts.RequiredProtocolInfo.Features {
1515+
if feature == WatchersFeature {
1516+
return true
1517+
}
1518+
}
1519+
return false
1520+
}
1521+
14611522
// NewWatcher creates a new Watcher object for the connection.
14621523
//
14631524
// You need to require WatchersFeature to use watchers, see examples for the
@@ -1496,15 +1557,7 @@ func (conn *Connection) NewWatcher(key string, callback WatchCallback) (Watcher,
14961557
// asynchronous. We do not expect any response from a Tarantool instance
14971558
// That's why we can't just check the Tarantool response for an unsupported
14981559
// request error.
1499-
watchersRequired := false
1500-
for _, feature := range conn.opts.RequiredProtocolInfo.Features {
1501-
if feature == WatchersFeature {
1502-
watchersRequired = true
1503-
break
1504-
}
1505-
}
1506-
1507-
if !watchersRequired {
1560+
if !conn.isWatchersRequired() {
15081561
err := fmt.Errorf("the feature %s must be required by connection "+
15091562
"options to create a watcher", WatchersFeature)
15101563
return nil, err
@@ -1666,3 +1719,43 @@ func (conn *Connection) ServerProtocolInfo() ProtocolInfo {
16661719
func (conn *Connection) ClientProtocolInfo() ProtocolInfo {
16671720
return clientProtocolInfo.Clone()
16681721
}
1722+
1723+
func shutdownEventCallback(event WatchEvent) {
1724+
// Receives "true" on server shutdown.
1725+
// See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/
1726+
// step 2.
1727+
val, ok := event.Value.(bool)
1728+
if ok && val {
1729+
// defer cause otherwise we'll block ourselves on ack wait.
1730+
defer event.Conn.processShutdown()
1731+
}
1732+
}
1733+
1734+
func (conn *Connection) processShutdown() {
1735+
// Forbid starting new tasks.
1736+
atomic.StoreUint32(&(conn.shutdownInProgress), shutdownInProgress)
1737+
1738+
// After finish, allow starting new tasks, they will fail with
1739+
// "not connected" instead.
1740+
defer atomic.StoreUint32(&(conn.shutdownInProgress), shutdownNotInProgress)
1741+
1742+
// Wait for tasks to finish.
1743+
conn.shutdownWg.Wait()
1744+
1745+
// Do not unregister task explicitly since connection teardown
1746+
// has the same effect.
1747+
1748+
if !conn.ClosedNow() {
1749+
err := ClientError{
1750+
ErrConnectionClosed,
1751+
"connection closed after server shutdown",
1752+
}
1753+
1754+
// Start to reconnect based on common rules, same as in net.box.
1755+
// Reconnect also closes the connection: server waits until all
1756+
// subscribed connections are terminated.
1757+
// See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/
1758+
// step 3.
1759+
defer conn.reconnect(err, conn.c)
1760+
}
1761+
}

0 commit comments

Comments
 (0)