Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
### Changed

* Required Go version is `1.24` now (#456).
* Error types redesigned around `errors.Is` / `errors.As` (#469):
`tarantool.Error` renamed to `tarantool.ServerError`; the seven legacy
client error code constants are now package-level `error` sentinels
(`ErrConnectionClosed`, `ErrTimeouted`, ...) with numeric forms exposed
as `tarantool.Code*`; `ClientError` gained a `Cause` field that wraps
the underlying I/O error; `ClientError.Temporary()` removed in favour
of `tarantool.IsRetryable(err)` / `errors.Is(err, ErrRetryable)`.
See MIGRATION.md.
* `test_helpers.MockDoer` is now an interface instead of a struct. The
`Requests` field became a method `Requests()`. The `NewMockDoer()`
constructor now returns the interface and uses a builder pattern.
Expand Down
52 changes: 52 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,58 @@ TODO
`ModeRW`, `RO` → `ModeRO`, `PreferRW` → `ModePreferRW`, `PreferRO` →
`ModePreferRO`, `UnknownRole` → `RoleUnknown`, `MasterRole` → `RoleMaster`,
`ReplicaRole` → `RoleReplica`.
* Error types redesigned around Go's `errors.Is` / `errors.As` (#469):

* `tarantool.Error` (server-side error wrapper) renamed to
`tarantool.ServerError`.
* `ClientError.Code` is now a typed `tarantool.ClientErrorCode`
(underlying `uint32`, same numeric values).
* `ClientError.Temporary()` removed. Use `tarantool.IsRetryable(err)`
or `errors.Is(err, tarantool.ErrRetryable)`.
* The seven legacy `uint32` constants are now package-level `error`
sentinels matched via `errors.Is`. Numeric forms remain available as
`tarantool.Code*` constants:

| Old (`uint32` constant) | New sentinel | Numeric (`ClientErrorCode`) |
|-------------------------|---------------------------|-----------------------------|
| `ErrConnectionNotReady` | `ErrConnectionNotReady` | `CodeConnectionNotReady` |
| `ErrConnectionClosed` | `ErrConnectionClosed` | `CodeConnectionClosed` |
| `ErrProtocolError` | `ErrProtocolError` | `CodeProtocolError` |
| `ErrTimeouted` | `ErrTimeouted` | `CodeTimeouted` |
| `ErrRateLimited` | `ErrRateLimited` | `CodeRateLimited` |
| `ErrConnectionShutdown` | `ErrConnectionShutdown` | `CodeConnectionShutdown` |
| `ErrIoError` | `ErrIoError` | `CodeIoError` |

* `ClientError` gained a `Cause error` field; I/O failures wrap their
underlying `net`-layer error and can be inspected with `errors.As`.
* `ClientError.Error()` now formats as
`"<sentinel-message>: <Msg>: <cause>"` (omitting empty parts).

Before:
```Go
if clientErr, ok := err.(tarantool.ClientError); ok {
if clientErr.Code == tarantool.ErrConnectionClosed {
// ...
}
if clientErr.Temporary() {
// retry
}
}
var tntErr tarantool.Error
if errors.As(err, &tntErr) { /* ... */ }
```

After:
```Go
if errors.Is(err, tarantool.ErrConnectionClosed) {
// ...
}
if tarantool.IsRetryable(err) {
// retry
}
var tntErr tarantool.ServerError
if errors.As(err, &tntErr) { /* ... */ }
```
* `Future.Release()` call could be used to free resources allocated for the
`Future` object created by a `Connection` object.
* Removed deprecated `NewCall16Request` and `NewCall17Request` constructors.
Expand Down
2 changes: 1 addition & 1 deletion arrow/tarantool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestInsert_invalid(t *testing.T) {

req := arrow.NewInsertRequest(space, arr)
_, err = conn.Do(req).Get()
ttErr := err.(tarantool.Error)
ttErr := err.(tarantool.ServerError)

require.Contains(t, a.expected, ttErr.Code)
})
Expand Down
12 changes: 6 additions & 6 deletions box/tarantool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ func TestBox_Sugar_Schema_UserCreate_AlreadyExists(t *testing.T) {
require.Error(t, err)

// Require that error code is ER_USER_EXISTS.
var boxErr tarantool.Error
var boxErr tarantool.ServerError
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_USER_EXISTS, boxErr.Code)
}
Expand Down Expand Up @@ -295,7 +295,7 @@ func TestBox_Sugar_Schema_UserDrop_UnknownUser(t *testing.T) {
err = b.Schema().User().Drop(ctx, "some_strange_not_existing_name", box.UserDropOptions{})
require.Error(t, err)

var boxErr tarantool.Error
var boxErr tarantool.ServerError

// Require that error code is ER_NO_SUCH_USER
errors.As(err, &boxErr)
Expand All @@ -313,7 +313,7 @@ func TestSchemaUser_Passwd_NotFound(t *testing.T) {
err = b.Schema().User().Passwd(ctx, "not-exists-passwd", "new_password")
require.Error(t, err)
// Require that error code is ER_USER_EXISTS.
var boxErr tarantool.Error
var boxErr tarantool.ServerError
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_NO_SUCH_USER, boxErr.Code)
}
Expand Down Expand Up @@ -391,7 +391,7 @@ func TestSchemaUser_Passwd_WithoutGrants(t *testing.T) {
require.Error(t, err)

// Require that error code is AccessDeniedError,
var boxErr tarantool.Error
var boxErr tarantool.ServerError
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code)
}
Expand Down Expand Up @@ -456,7 +456,7 @@ func TestBox_Sugar_Schema_UserGrant_NoSu(t *testing.T) {
require.Error(t, err)

// Require that error code is ER_ACCESS_DENIED.
var boxErr tarantool.Error
var boxErr tarantool.ServerError
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code)
}
Expand Down Expand Up @@ -546,7 +546,7 @@ func TestSchemaUser_Revoke_WithoutSu(t *testing.T) {
require.Error(t, err)

// Require that error code is ER_ACCESS_DENIED.
var boxErr tarantool.Error
var boxErr tarantool.ServerError
errors.As(err, &boxErr)
require.Equal(t, iproto.ER_ACCESS_DENIED, boxErr.Code)
}
Expand Down
104 changes: 49 additions & 55 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ func (conn *Connection) ClosedNow() bool {
// Close closes Connection.
// After this method called, there is no way to reopen this Connection.
func (conn *Connection) Close() error {
err := ClientError{ErrConnectionClosed, "connection closed by client"}
err := newClientError(CodeConnectionClosed, "connection closed by client", nil)
conn.mutex.Lock()
defer conn.mutex.Unlock()
return conn.closeConnection(err, true)
Expand Down Expand Up @@ -552,7 +552,7 @@ func (conn *Connection) connect(ctx context.Context) error {
}
}
if conn.state == connClosed {
err = ClientError{ErrConnectionClosed, "using closed connection"}
err = newClientError(CodeConnectionClosed, "using closed connection", nil)
}
return err
}
Expand Down Expand Up @@ -631,8 +631,7 @@ func (conn *Connection) runReconnects(ctx context.Context) error {
if ctx.Err() != nil {
return err
}
if clientErr, ok := err.(ClientError); ok &&
clientErr.Code == ErrConnectionClosed {
if errors.Is(err, ErrConnectionClosed) {
return err
}
} else {
Expand Down Expand Up @@ -664,7 +663,7 @@ func (conn *Connection) runReconnects(ctx context.Context) error {
slog.String(LogKeyAddress, conn.addrString()),
)
// mark connection as closed to avoid reopening by another goroutine
return ClientError{ErrConnectionClosed, "last reconnect failed"}
return newClientError(CodeConnectionClosed, "last reconnect failed", nil)
}

func (conn *Connection) reconnectImpl(neterr error, c Conn) {
Expand Down Expand Up @@ -737,11 +736,11 @@ func (conn *Connection) writer(w writeFlusher, c Conn) {
runtime.Gosched()
if len(conn.dirtyShard) == 0 {
if err := w.Flush(); err != nil {
err = ClientError{
ErrIoError,
fmt.Sprintf("failed to flush data to the connection: %s", err),
}
conn.reconnect(err, c)
conn.reconnect(newClientError(
CodeIoError,
"failed to flush data to the connection",
err,
), c)
return
}
}
Expand All @@ -764,11 +763,11 @@ func (conn *Connection) writer(w writeFlusher, c Conn) {
continue
}
if _, err := w.Write(packet.b); err != nil {
err = ClientError{
ErrIoError,
fmt.Sprintf("failed to write data to the connection: %s", err),
}
conn.reconnect(err, c)
conn.reconnect(newClientError(
CodeIoError,
"failed to write data to the connection",
err,
), c)
return
}
packet.Reset()
Expand Down Expand Up @@ -861,23 +860,23 @@ func (conn *Connection) reader(r io.Reader, c Conn) {
buf, err = read(r, conn.lenbuf[:], conn.alloc)

if err != nil {
err = ClientError{
ErrIoError,
fmt.Sprintf("failed to read data from the connection: %s", err),
}
conn.reconnect(err, c)
conn.reconnect(newClientError(
CodeIoError,
"failed to read data from the connection",
err,
), c)
return
}

header, code, err := decodeHeader(conn.dec, &buf)

if err != nil {
err = ClientError{
ErrProtocolError,
fmt.Sprintf("failed to decode IPROTO header: %s", err),
}
buf.Release()
conn.reconnect(err, c)
conn.reconnect(newClientError(
CodeProtocolError,
"failed to decode IPROTO header",
err,
), c)
return
}

Expand All @@ -886,12 +885,13 @@ func (conn *Connection) reader(r io.Reader, c Conn) {
if event, err := readWatchEvent(&buf); err == nil {
events <- event
} else {
err = ClientError{
ErrProtocolError,
fmt.Sprintf("failed to decode IPROTO_EVENT: %s", err),
}
wrapped := newClientError(
CodeProtocolError,
"failed to decode IPROTO_EVENT",
err,
)
conn.logger.Warn(LogMsgWatchEventReadFailed,
slog.Any(LogKeyError, err),
slog.Any(LogKeyError, wrapped),
)
}
buf.Release()
Expand Down Expand Up @@ -938,10 +938,11 @@ func (conn *Connection) newFuture(req Request) *future {
select {
case conn.rlimit <- struct{}{}:
default:
fut.err = ClientError{
ErrRateLimited,
"Request is rate limited on client",
}
fut.err = newClientError(
CodeRateLimited,
"request is rate limited on client",
nil,
)
fut.finish()
return fut
}
Expand All @@ -952,26 +953,17 @@ func (conn *Connection) newFuture(req Request) *future {
shard.rmut.Lock()
switch atomic.LoadUint32(&conn.state) {
case connClosed:
fut.err = ClientError{
ErrConnectionClosed,
"using closed connection",
}
fut.err = newClientError(CodeConnectionClosed, "using closed connection", nil)
fut.finish()
shard.rmut.Unlock()
return fut
case connDisconnected:
fut.err = ClientError{
ErrConnectionNotReady,
"client connection is not ready",
}
fut.err = newClientError(CodeConnectionNotReady, "client connection is not ready", nil)
fut.finish()
shard.rmut.Unlock()
return fut
case connShutdown:
fut.err = ClientError{
ErrConnectionShutdown,
"server shutdown in progress",
}
fut.err = newClientError(CodeConnectionShutdown, "server shutdown in progress", nil)
fut.finish()
shard.rmut.Unlock()
return fut
Expand Down Expand Up @@ -1174,10 +1166,11 @@ func (conn *Connection) timeouts() {
} else {
fut.next = nil
}
fut.setError(ClientError{
Code: ErrTimeouted,
Msg: fmt.Sprintf("client timeout for request %d", fut.requestId),
})
fut.setError(newClientError(
CodeTimeouted,
fmt.Sprintf("client timeout for request %d", fut.requestId),
nil,
))
conn.markDone()
shard.bufmut.Unlock()
}
Expand Down Expand Up @@ -1562,7 +1555,7 @@ func (conn *Connection) shutdown(forever bool) error {

if !atomic.CompareAndSwapUint32(&conn.state, connConnected, connShutdown) {
if forever {
err := ClientError{ErrConnectionClosed, "connection closed by client"}
err := newClientError(CodeConnectionClosed, "connection closed by client", nil)
return conn.closeConnection(err, true)
}
return nil
Expand Down Expand Up @@ -1593,18 +1586,19 @@ func (conn *Connection) shutdown(forever bool) error {
}

if forever {
err := ClientError{ErrConnectionClosed, "connection closed by client"}
err := newClientError(CodeConnectionClosed, "connection closed by client", nil)
return conn.closeConnection(err, true)
} else {
// Start to reconnect based on common rules, same as in net.box.
// Reconnect also closes the connection: server waits until all
// subscribed connections are terminated.
// See https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/graceful_shutdown/
// step 3.
conn.reconnectImpl(ClientError{
ErrConnectionClosed,
conn.reconnectImpl(newClientError(
CodeConnectionClosed,
"connection closed after server shutdown",
}, conn.c)
nil,
), conn.c)
return nil
}
}
9 changes: 4 additions & 5 deletions dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -642,13 +642,12 @@ func readResponse(ctx context.Context, conn Conn, req Request) (Response, error)

_, err = resp.Decode()
if err != nil {
switch err.(type) {
case Error:
var serverErr ServerError
if errors.As(err, &serverErr) {
return resp, err
default:
resp.Release()
return nil, fmt.Errorf("decode response body error: %w", err)
}
resp.Release()
return nil, fmt.Errorf("decode response body error: %w", err)
}

return resp, nil
Expand Down
Loading
Loading