Skip to content

Commit

Permalink
feature/box: first box and box.info implementation
Browse files Browse the repository at this point in the history
implemented the box interface for tarantool with a small number of fields, which in the future can be supplemented
  • Loading branch information
maksim.konovalov committed Dec 10, 2024
1 parent f9eb951 commit f94f942
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 36 additions & 0 deletions box/box.go
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions box/box_test.go
Original file line number Diff line number Diff line change
@@ -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()
})
}
60 changes: 60 additions & 0 deletions box/example_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 76 additions & 0 deletions box/info.go
Original file line number Diff line number Diff line change
@@ -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
}
38 changes: 38 additions & 0 deletions box/request.go
Original file line number Diff line number Diff line change
@@ -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)
}
86 changes: 86 additions & 0 deletions box/tarantool_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 13 additions & 0 deletions box/testdata/config.lua
Original file line number Diff line number Diff line change
@@ -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"),
}

0 comments on commit f94f942

Please sign in to comment.