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..443b95a6 --- /dev/null +++ b/box/box.go @@ -0,0 +1,24 @@ +package box + +import ( + "github.com/tarantool/go-tarantool/v2" +) + +// Box defines an interface for interacting with a Tarantool instance. +// It includes the Info method, which retrieves instance information. +type Box interface { + Info() (Info, error) // Retrieves detailed information about the Tarantool instance. +} + +// box is a concrete implementation of the Box interface. +// It holds a connection to the Tarantool instance via the Doer interface. +type box struct { + conn tarantool.Doer // Connection interface for interacting with Tarantool. +} + +// By returns a new instance of the box structure, which implements the Box interface. +func By(conn tarantool.Doer) Box { + return &box{ + conn: conn, // Assigns the provided Tarantool connection. + } +} diff --git a/box/box_test.go b/box/box_test.go new file mode 100644 index 00000000..a93bbadf --- /dev/null +++ b/box/box_test.go @@ -0,0 +1,28 @@ +package box_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-tarantool/v2/box" +) + +func TestBy(t *testing.T) { + // 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 of the library's responsibility. + require.Panics(t, func() { + // Create a box instance with a nil connection. This should lead to a panic later. + b := box.By(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) + + // 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/info.go b/box/info.go new file mode 100644 index 00000000..ffadeaaf --- /dev/null +++ b/box/info.go @@ -0,0 +1,48 @@ +package box + +import "github.com/tarantool/go-tarantool/v2" + +// ClusterInfo represents information about the cluster. +// It contains the unique identifier (UUID) of the cluster. +type ClusterInfo struct { + UUID string `msgpack:"uuid"` +} + +// 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"` + // Cluster information, including cluster UUID. + Cluster ClusterInfo `msgpack:"cluster"` +} + +// 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 info Info + + // Call "box.info" to get instance information from Tarantool. + fut := b.conn.Do(tarantool.NewCallRequest("box.info")) + + // Parse the result into the Info structure. + err := fut.GetTyped(&[]interface{}{&info}) + if err != nil { + return Info{}, err + } + + // Return the parsed info and any potential error. + return info, err +} diff --git a/box/tarantool_test.go b/box/tarantool_test.go new file mode 100644 index 00000000..00e06e83 --- /dev/null +++ b/box/tarantool_test.go @@ -0,0 +1,65 @@ +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 TestBox_Info(t *testing.T) { + ctx := context.TODO() + + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{}) + require.NoError(t, err) + + info, err := box.By(conn).Info() + require.NoError(t, err) + + // 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 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"), +}