Skip to content

Commit ebdf986

Browse files
maksim.konovalovoleg-jukovec
maksim.konovalov
authored andcommitted
box: first box and box.info implementation
Implemented the box interface for tarantool with a small number of fields, which in the future can be supplemented.
1 parent fbc69d4 commit ebdf986

File tree

8 files changed

+339
-0
lines changed

8 files changed

+339
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1616
- Methods that are implemented but not included in the pooler interface (#395).
1717
- Implemented stringer methods for pool.Role (#405).
1818
- Support the IPROTO_INSERT_ARROW request (#399).
19+
- A simple implementation of using the box interface was written (#410).
1920

2021
### Changed
2122

box/box.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package box
2+
3+
import (
4+
"github.com/tarantool/go-tarantool/v2"
5+
)
6+
7+
// Box is a helper that wraps box.* requests.
8+
// It holds a connection to the Tarantool instance via the Doer interface.
9+
type Box struct {
10+
conn tarantool.Doer // Connection interface for interacting with Tarantool.
11+
}
12+
13+
// New returns a new instance of the box structure, which implements the Box interface.
14+
func New(conn tarantool.Doer) *Box {
15+
return &Box{
16+
conn: conn, // Assigns the provided Tarantool connection.
17+
}
18+
}
19+
20+
// Info retrieves the current information of the Tarantool instance.
21+
// It calls the "box.info" function and parses the result into the Info structure.
22+
func (b *Box) Info() (Info, error) {
23+
var infoResp InfoResponse
24+
25+
// Call "box.info" to get instance information from Tarantool.
26+
fut := b.conn.Do(NewInfoRequest())
27+
28+
// Parse the result into the Info structure.
29+
err := fut.GetTyped(&infoResp)
30+
if err != nil {
31+
return Info{}, err
32+
}
33+
34+
// Return the parsed info and any potential error.
35+
return infoResp.Info, err
36+
}

box/box_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package box_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"github.com/tarantool/go-tarantool/v2/box"
8+
)
9+
10+
func TestNew(t *testing.T) {
11+
// Create a box instance with a nil connection. This should lead to a panic later.
12+
b := box.New(nil)
13+
14+
// Ensure the box instance is not nil (which it shouldn't be), but this is not meaningful
15+
// since we will panic when we call the Info method with the nil connection.
16+
require.NotNil(t, b)
17+
18+
// We expect a panic because we are passing a nil connection (nil Doer) to the By function.
19+
// The library does not control this zone, and the nil connection would cause a runtime error
20+
// when we attempt to call methods (like Info) on it.
21+
// This test ensures that such an invalid state is correctly handled by causing a panic,
22+
// as it's outside the library's responsibility.
23+
require.Panics(t, func() {
24+
25+
// Calling Info on a box with a nil connection will result in a panic, since the underlying
26+
// connection (Doer) cannot perform the requested action (it's nil).
27+
_, _ = b.Info()
28+
})
29+
}

box/example_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Run Tarantool Common Edition before example execution:
2+
//
3+
// Terminal 1:
4+
// $ cd box
5+
// $ TEST_TNT_LISTEN=127.0.0.1:3013 tarantool testdata/config.lua
6+
//
7+
// Terminal 2:
8+
// $ go test -v example_test.go
9+
package box_test
10+
11+
import (
12+
"context"
13+
"fmt"
14+
"log"
15+
"time"
16+
17+
"github.com/tarantool/go-tarantool/v2"
18+
"github.com/tarantool/go-tarantool/v2/box"
19+
)
20+
21+
func Example() {
22+
dialer := tarantool.NetDialer{
23+
Address: "127.0.0.1:3013",
24+
User: "test",
25+
Password: "test",
26+
}
27+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
28+
client, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
29+
cancel()
30+
if err != nil {
31+
log.Fatalf("Failed to connect: %s", err)
32+
}
33+
34+
// You can use Info Request type.
35+
36+
fut := client.Do(box.NewInfoRequest())
37+
38+
resp := &box.InfoResponse{}
39+
40+
err = fut.GetTyped(resp)
41+
if err != nil {
42+
log.Fatalf("Failed get box info: %s", err)
43+
}
44+
45+
// Or use simple Box implementation.
46+
47+
b := box.New(client)
48+
49+
info, err := b.Info()
50+
if err != nil {
51+
log.Fatalf("Failed get box info: %s", err)
52+
}
53+
54+
if info.UUID != resp.Info.UUID {
55+
log.Fatalf("Box info uuids are not equal")
56+
}
57+
58+
fmt.Printf("Box info uuids are equal")
59+
fmt.Printf("Current box info: %+v\n", resp.Info)
60+
}

box/info.go

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package box
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/tarantool/go-tarantool/v2"
7+
"github.com/vmihailenco/msgpack/v5"
8+
)
9+
10+
var _ tarantool.Request = (*InfoRequest)(nil)
11+
12+
// Info represents detailed information about the Tarantool instance.
13+
// It includes version, node ID, read-only status, process ID, cluster information, and more.
14+
type Info struct {
15+
// The Version of the Tarantool instance.
16+
Version string `msgpack:"version"`
17+
// The node ID (nullable).
18+
ID *int `msgpack:"id"`
19+
// Read-only (RO) status of the instance.
20+
RO bool `msgpack:"ro"`
21+
// UUID - Unique identifier of the instance.
22+
UUID string `msgpack:"uuid"`
23+
// Process ID of the instance.
24+
PID int `msgpack:"pid"`
25+
// Status - Current status of the instance (e.g., running, unconfigured).
26+
Status string `msgpack:"status"`
27+
// LSN - Log sequence number of the instance.
28+
LSN uint64 `msgpack:"lsn"`
29+
}
30+
31+
// InfoResponse represents the response structure
32+
// that holds the information of the Tarantool instance.
33+
// It contains a single field: Info, which holds the instance details (version, UUID, PID, etc.).
34+
type InfoResponse struct {
35+
Info Info
36+
}
37+
38+
func (ir *InfoResponse) DecodeMsgpack(d *msgpack.Decoder) error {
39+
arrayLen, err := d.DecodeArrayLen()
40+
if err != nil {
41+
return err
42+
}
43+
44+
if arrayLen != 1 {
45+
return fmt.Errorf("protocol violation; expected 1 array entry, got %d", arrayLen)
46+
}
47+
48+
i := Info{}
49+
err = d.Decode(&i)
50+
if err != nil {
51+
return err
52+
}
53+
54+
ir.Info = i
55+
56+
return nil
57+
}
58+
59+
// InfoRequest represents a request to retrieve information about the Tarantool instance.
60+
// It implements the tarantool.Request interface.
61+
type InfoRequest struct {
62+
baseRequest
63+
}
64+
65+
// Body method is used to serialize the request's body.
66+
// It is part of the tarantool.Request interface implementation.
67+
func (i InfoRequest) Body(res tarantool.SchemaResolver, enc *msgpack.Encoder) error {
68+
return i.impl.Body(res, enc)
69+
}
70+
71+
// NewInfoRequest returns a new empty info request.
72+
func NewInfoRequest() InfoRequest {
73+
req := InfoRequest{}
74+
req.impl = newCall("box.info")
75+
return req
76+
}

box/request.go

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package box
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
"github.com/tarantool/go-iproto"
8+
"github.com/tarantool/go-tarantool/v2"
9+
)
10+
11+
type baseRequest struct {
12+
impl *tarantool.CallRequest
13+
}
14+
15+
func newCall(method string) *tarantool.CallRequest {
16+
return tarantool.NewCallRequest(method)
17+
}
18+
19+
// Type returns IPROTO type for request.
20+
func (req baseRequest) Type() iproto.Type {
21+
return req.impl.Type()
22+
}
23+
24+
// Ctx returns a context of request.
25+
func (req baseRequest) Ctx() context.Context {
26+
return req.impl.Ctx()
27+
}
28+
29+
// Async returns request expects a response.
30+
func (req baseRequest) Async() bool {
31+
return req.impl.Async()
32+
}
33+
34+
// Response creates a response for the baseRequest.
35+
func (req baseRequest) Response(header tarantool.Header,
36+
body io.Reader) (tarantool.Response, error) {
37+
return req.impl.Response(header, body)
38+
}

box/tarantool_test.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package box_test
2+
3+
import (
4+
"context"
5+
"log"
6+
"os"
7+
"testing"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"github.com/stretchr/testify/require"
12+
"github.com/tarantool/go-tarantool/v2"
13+
"github.com/tarantool/go-tarantool/v2/box"
14+
"github.com/tarantool/go-tarantool/v2/test_helpers"
15+
)
16+
17+
var server = "127.0.0.1:3013"
18+
var dialer = tarantool.NetDialer{
19+
Address: server,
20+
User: "test",
21+
Password: "test",
22+
}
23+
24+
func validateInfo(t testing.TB, info box.Info) {
25+
var err error
26+
27+
// Check all fields run correctly.
28+
_, err = uuid.Parse(info.UUID)
29+
require.NoErrorf(t, err, "validate instance uuid is valid")
30+
31+
require.NotEmpty(t, info.Version)
32+
// Check that pid parsed correctly.
33+
require.NotEqual(t, info.PID, 0)
34+
}
35+
36+
func TestBox_Sugar_Info(t *testing.T) {
37+
ctx := context.TODO()
38+
39+
conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
40+
require.NoError(t, err)
41+
42+
info, err := box.New(conn).Info()
43+
require.NoError(t, err)
44+
45+
validateInfo(t, info)
46+
}
47+
48+
func TestBox_Info(t *testing.T) {
49+
ctx := context.TODO()
50+
51+
conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{})
52+
require.NoError(t, err)
53+
54+
fut := conn.Do(box.NewInfoRequest())
55+
require.NotNil(t, fut)
56+
57+
resp := &box.InfoResponse{}
58+
err = fut.GetTyped(resp)
59+
require.NoError(t, err)
60+
61+
validateInfo(t, resp.Info)
62+
}
63+
64+
func runTestMain(m *testing.M) int {
65+
instance, err := test_helpers.StartTarantool(test_helpers.StartOpts{
66+
Dialer: dialer,
67+
InitScript: "testdata/config.lua",
68+
Listen: server,
69+
WaitStart: 100 * time.Millisecond,
70+
ConnectRetry: 10,
71+
RetryTimeout: 500 * time.Millisecond,
72+
})
73+
defer test_helpers.StopTarantoolWithCleanup(instance)
74+
75+
if err != nil {
76+
log.Printf("Failed to prepare test Tarantool: %s", err)
77+
return 1
78+
}
79+
80+
return m.Run()
81+
}
82+
83+
func TestMain(m *testing.M) {
84+
code := runTestMain(m)
85+
os.Exit(code)
86+
}

box/testdata/config.lua

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Do not set listen for now so connector won't be
2+
-- able to send requests until everything is configured.
3+
box.cfg{
4+
work_dir = os.getenv("TEST_TNT_WORK_DIR"),
5+
}
6+
7+
box.schema.user.create('test', { password = 'test' , if_not_exists = true })
8+
box.schema.user.grant('test', 'execute', 'universe', nil, { if_not_exists = true })
9+
10+
-- Set listen only when every other thing is configured.
11+
box.cfg{
12+
listen = os.getenv("TEST_TNT_LISTEN"),
13+
}

0 commit comments

Comments
 (0)