From e50696688ec86d9d6474d0ca1c46ce4a355f9cf8 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 12:49:11 +0300 Subject: [PATCH 1/2] api: meaningful read/write networt error We need to add meaningful error descriptions for a read/write socket errors. Part of #129 --- CHANGELOG.md | 1 + connection.go | 20 ++++++++++++++++++++ errors.go | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f025e122..c443a4ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Enumeration types for RLimitAction/iterators (#158) - IsNullable flag for Field (#302) - More linters on CI (#310) +- Meaningful description for read/write socket errors (#129) ### Changed diff --git a/connection.go b/connection.go index 367cf9640..37de22e82 100644 --- a/connection.go +++ b/connection.go @@ -789,6 +789,10 @@ 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) return } @@ -812,6 +816,10 @@ 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) return } @@ -868,12 +876,20 @@ func (conn *Connection) reader(r io.Reader, c Conn) { for atomic.LoadUint32(&conn.state) != connClosed { respBytes, err := read(r, conn.lenbuf[:]) if err != nil { + err = ClientError{ + ErrIoError, + fmt.Sprintf("failed to read data from the connection: %s", err), + } conn.reconnect(err, c) return } resp := &Response{buf: smallBuf{b: respBytes}} err = resp.decodeHeader(conn.dec) if err != nil { + err = ClientError{ + ErrProtocolError, + fmt.Sprintf("failed to decode IPROTO header: %s", err), + } conn.reconnect(err, c) return } @@ -883,6 +899,10 @@ func (conn *Connection) reader(r io.Reader, c Conn) { if event, err := readWatchEvent(&resp.buf); err == nil { events <- event } else { + err = ClientError{ + ErrProtocolError, + fmt.Sprintf("failed to decode IPROTO_EVENT: %s", err), + } conn.opts.Logger.Report(LogWatchEventReadFailed, conn, err) } continue diff --git a/errors.go b/errors.go index ab40e4f63..60f71a2b1 100644 --- a/errors.go +++ b/errors.go @@ -45,7 +45,7 @@ func (clierr ClientError) Error() string { // - request is aborted due to rate limit func (clierr ClientError) Temporary() bool { switch clierr.Code { - case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited: + case ErrConnectionNotReady, ErrTimeouted, ErrRateLimited, ErrIoError: return true default: return false @@ -60,4 +60,5 @@ const ( ErrTimeouted = 0x4000 + iota ErrRateLimited = 0x4000 + iota ErrConnectionShutdown = 0x4000 + iota + ErrIoError = 0x4000 + iota ) From aa71a64c97b1897abf8cb63bedc97ba876777d36 Mon Sep 17 00:00:00 2001 From: Oleg Jukovec Date: Tue, 27 Jun 2023 13:27:50 +0300 Subject: [PATCH 2/2] doc: add Connection.Do result processing examples - ExampleConnection_Do demonstrates how to process a result. - ExampleConnection_Do_failure demonstrates how to process a request failure. Closes #128 --- example_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/example_test.go b/example_test.go index e2a645937..d871d578a 100644 --- a/example_test.go +++ b/example_test.go @@ -998,6 +998,75 @@ func ExampleSpace() { // SpaceField 2 name3 unsigned } +// ExampleConnection_Do demonstrates how to send a request and process +// a response. +func ExampleConnection_Do() { + conn := exampleConnect(opts) + defer conn.Close() + + // It could be any request. + req := tarantool.NewReplaceRequest("test"). + Tuple([]interface{}{int(1111), "foo", "bar"}) + + // We got a future, the request actually not performed yet. + future := conn.Do(req) + + // When the future receives the response, the result of the Future is set + // and becomes available. We could wait for that moment with Future.Get() + // or Future.GetTyped() methods. + resp, err := future.Get() + if err != nil { + fmt.Printf("Failed to execute the request: %s\n", err) + } else { + fmt.Println(resp.Data) + } + + // Output: + // [[1111 foo bar]] +} + +// ExampleConnection_Do_failure demonstrates how to send a request and process +// failure. +func ExampleConnection_Do_failure() { + conn := exampleConnect(opts) + defer conn.Close() + + // It could be any request. + req := tarantool.NewCallRequest("not_exist") + + // We got a future, the request actually not performed yet. + future := conn.Do(req) + + // When the future receives the response, the result of the Future is set + // and becomes available. We could wait for that moment with Future.Get() + // or Future.GetTyped() methods. + resp, err := future.Get() + if err != nil { + // We don't print the error here to keep the example reproducible. + // fmt.Printf("Failed to execute the request: %s\n", err) + if resp == nil { + // Something happens in a client process (timeout, IO error etc). + fmt.Printf("Resp == nil, ClientErr = %s\n", err.(tarantool.ClientError)) + } else { + // Response exist. So it could be a Tarantool error or a decode + // error. We need to check the error code. + fmt.Printf("Error code from the response: %d\n", resp.Code) + if resp.Code == tarantool.OkCode { + fmt.Printf("Decode error: %s\n", err) + } else { + code := err.(tarantool.Error).Code + fmt.Printf("Error code from the error: %d\n", code) + fmt.Printf("Error short from the error: %s\n", code) + } + } + } + + // Output: + // Error code from the response: 33 + // Error code from the error: 33 + // Error short from the error: ER_NO_SUCH_PROC +} + // To use prepared statements to query a tarantool instance, call NewPrepared. func ExampleConnection_NewPrepared() { // Tarantool supports SQL since version 2.0.0