diff --git a/CHANGELOG.md b/CHANGELOG.md index b0eb0f9c..4ae4da13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Methods that are implemented but not included in the pooler interface (#395). - Implemented stringer methods for pool.Role (#405). - Support the IPROTO_INSERT_ARROW request (#399). +- A simple implementation of using the box interface was written (#410). ### Changed diff --git a/box/box.go b/box/box.go new file mode 100644 index 00000000..be7f288a --- /dev/null +++ b/box/box.go @@ -0,0 +1,36 @@ +package box + +import ( + "github.com/tarantool/go-tarantool/v2" +) + +// Box is a helper that wraps box.* requests. +// It holds a connection to the Tarantool instance via the Doer interface. +type Box struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// New returns a new instance of the box structure, which implements the Box interface. +func New(conn tarantool.Doer) *Box { + return &Box{ + conn: conn, // Assigns the provided Tarantool connection. + } +} + +// Info retrieves the current information of the Tarantool instance. +// It calls the "box.info" function and parses the result into the Info structure. +func (b *Box) Info() (Info, error) { + var infoResp InfoResponse + + // Call "box.info" to get instance information from Tarantool. + fut := b.conn.Do(NewInfoRequest()) + + // Parse the result into the Info structure. + err := fut.GetTyped(&infoResp) + if err != nil { + return Info{}, err + } + + // Return the parsed info and any potential error. + return infoResp.Info, err +} diff --git a/box/box_test.go b/box/box_test.go new file mode 100644 index 00000000..31e614c1 --- /dev/null +++ b/box/box_test.go @@ -0,0 +1,29 @@ +package box_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2/box" +) + +func TestNew(t *testing.T) { + // Create a box instance with a nil connection. This should lead to a panic later. + b := box.New(nil) + + // Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful + // since we will panic when we call the Info method with the nil connection. + require.NotNil(t, b) + + // We expect a panic because we are passing a nil connection (nil Doer) to the By function. + // The library does not control this zone, and the nil connection would cause a runtime error + // when we attempt to call methods (like Info) on it. + // This test ensures that such an invalid state is correctly handled by causing a panic, + // as it's outside the library's responsibility. + require.Panics(t, func() { + + // Calling Info on a box with a nil connection will result in a panic, since the underlying + // connection (Doer) cannot perform the requested action (it's nil). + _, _ = b.Info() + }) +} diff --git a/box/example_test.go b/box/example_test.go new file mode 100644 index 00000000..46194976 --- /dev/null +++ b/box/example_test.go @@ -0,0 +1,60 @@ +// Run Tarantool Common Edition before example execution: +// +// Terminal 1: +// $ cd box +// $ TEST_TNT_LISTEN=127.0.0.1:3013 tarantool testdata/config.lua +// +// Terminal 2: +// $ go test -v example_test.go +package box_test + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/box" +) + +func Example() { + dialer := tarantool.NetDialer{ + Address: "127.0.0.1:3013", + User: "test", + Password: "test", + } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + cancel() + if err != nil { + log.Fatalf("Failed to connect: %s", err) + } + + // You can use Info Request type. + + fut := client.Do(box.NewInfoRequest()) + + resp := &box.InfoResponse{} + + err = fut.GetTyped(resp) + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } + + // Or use simple Box implementation. + + b := box.New(client) + + info, err := b.Info() + if err != nil { + log.Fatalf("Failed get box info: %s", err) + } + + if info.UUID != resp.Info.UUID { + log.Fatalf("Box info uuids are not equal") + } + + fmt.Printf("Box info uuids are equal") + fmt.Printf("Current box info: %+v\n", resp.Info) +} diff --git a/box/info.go b/box/info.go new file mode 100644 index 00000000..6e5ed1c9 --- /dev/null +++ b/box/info.go @@ -0,0 +1,76 @@ +package box + +import ( + "fmt" + + "github.com/tarantool/go-tarantool/v2" + "github.com/vmihailenco/msgpack/v5" +) + +var _ tarantool.Request = (*InfoRequest)(nil) + +// Info represents detailed information about the Tarantool instance. +// It includes version, node ID, read-only status, process ID, cluster information, and more. +type Info struct { + // The Version of the Tarantool instance. + Version string `msgpack:"version"` + // The node ID (nullable). + ID *int `msgpack:"id"` + // Read-only (RO) status of the instance. + RO bool `msgpack:"ro"` + // UUID - Unique identifier of the instance. + UUID string `msgpack:"uuid"` + // Process ID of the instance. + PID int `msgpack:"pid"` + // Status - Current status of the instance (e.g., running, unconfigured). + Status string `msgpack:"status"` + // LSN - Log sequence number of the instance. + LSN uint64 `msgpack:"lsn"` +} + +// InfoResponse represents the response structure +// that holds the information of the Tarantool instance. +// It contains a single field: Info, which holds the instance details (version, UUID, PID, etc.). +type InfoResponse struct { + Info Info +} + +func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error { + arrayLen, err := d.DecodeArrayLen() + if err != nil { + return err + } + + if arrayLen != 1 { + return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen) + } + + i := Info{} + err = d.Decode(&i) + if err != nil { + return err + } + + ir.Info = i + + return nil +} + +// InfoRequest represents a request to retrieve information about the Tarantool instance. +// It implements the tarantool.Request interface. +type InfoRequest struct { + baseRequest +} + +// Body method is used to serialize the request's body. +// It is part of the tarantool.Request interface implementation. +func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error { + return i.impl.Body(res, enc) +} + +// NewInfoRequest returns a new empty info request. +func NewInfoRequest() InfoRequest { + req := InfoRequest{} + req.impl = newCall("box.info") + return req +} diff --git a/box/request.go b/box/request.go new file mode 100644 index 00000000..bf51a72f --- /dev/null +++ b/box/request.go @@ -0,0 +1,38 @@ +package box + +import ( + "context" + "io" + + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-tarantool/v2" +) + +type baseRequest struct { + impl *tarantool.CallRequest +} + +func newCall(method string) *tarantool.CallRequest { + return tarantool.NewCallRequest(method) +} + +// Type returns IPROTO type for request. +func (req baseRequest) Type() iproto.Type { + return req.impl.Type() +} + +// Ctx returns a context of request. +func (req baseRequest) Ctx() context.Context { + return req.impl.Ctx() +} + +// Async returns request expects a response. +func (req baseRequest) Async() bool { + return req.impl.Async() +} + +// Response creates a response for the baseRequest. +func (req baseRequest) Response(header tarantool.Header, + body io.Reader) (tarantool.Response, error) { + return req.impl.Response(header, body) +} diff --git a/box/tarantool_test.go b/box/tarantool_test.go new file mode 100644 index 00000000..3d638b5b --- /dev/null +++ b/box/tarantool_test.go @@ -0,0 +1,86 @@ +package box_test + +import ( + "context" + "log" + "os" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/box" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +var server = "127.0.0.1:3013" +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} + +func validateInfo(t testing.TB, info box.Info) { + var err error + + // Check all fields run correctly. + _, err = uuid.Parse(info.UUID) + require.NoErrorf(t, err, "validate instance uuid is valid") + + require.NotEmpty(t, info.Version) + // Check that pid parsed correctly. + require.NotEqual(t, info.PID, 0) +} + +func TestBox_Sugar_Info(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + info, err := box.New(conn).Info() + require.NoError(t, err) + + validateInfo(t, info) +} + +func TestBox_Info(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + fut := conn.Do(box.NewInfoRequest()) + require.NotNil(t, fut) + + resp := &box.InfoResponse{} + err = fut.GetTyped(resp) + require.NoError(t, err) + + validateInfo(t, resp.Info) +} + +func runTestMain(m *testing.M) int { + instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{ + Dialer: dialer, + InitScript: "testdata/config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }) + defer test_helpers.StopTarantoolWithCleanup(instance) + + if err != nil { + log.Printf("Failed to prepare test Tarantool: %s", err) + return 1 + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/box/testdata/config.lua b/box/testdata/config.lua new file mode 100644 index 00000000..f3ee1a7b --- /dev/null +++ b/box/testdata/config.lua @@ -0,0 +1,13 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +box.cfg{ + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.schema.user.create('test', { password = 'test' , if_not_exists = true }) +box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true }) + +-- Set listen only when every other thing is configured. +box.cfg{ + listen = os.getenv("TEST_TNT_LISTEN"), +}