381 lines
10 KiB
Go
381 lines
10 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jeremytregunna/kevo/pkg/transport"
|
|
)
|
|
|
|
// CompressionType represents a compression algorithm
|
|
type CompressionType = transport.CompressionType
|
|
|
|
// Compression options
|
|
const (
|
|
CompressionNone = transport.CompressionNone
|
|
CompressionGzip = transport.CompressionGzip
|
|
CompressionSnappy = transport.CompressionSnappy
|
|
)
|
|
|
|
// ClientOptions configures a Kevo client
|
|
type ClientOptions struct {
|
|
// Connection options
|
|
Endpoint string // Server address
|
|
ConnectTimeout time.Duration // Timeout for connection attempts
|
|
RequestTimeout time.Duration // Default timeout for requests
|
|
TransportType string // Transport type (e.g. "grpc")
|
|
PoolSize int // Connection pool size
|
|
|
|
// Security options
|
|
TLSEnabled bool // Enable TLS
|
|
CertFile string // Client certificate file
|
|
KeyFile string // Client key file
|
|
CAFile string // CA certificate file
|
|
|
|
// Retry options
|
|
MaxRetries int // Maximum number of retries
|
|
InitialBackoff time.Duration // Initial retry backoff
|
|
MaxBackoff time.Duration // Maximum retry backoff
|
|
BackoffFactor float64 // Backoff multiplier
|
|
RetryJitter float64 // Random jitter factor
|
|
|
|
// Performance options
|
|
Compression CompressionType // Compression algorithm
|
|
MaxMessageSize int // Maximum message size
|
|
}
|
|
|
|
// DefaultClientOptions returns sensible default client options
|
|
func DefaultClientOptions() ClientOptions {
|
|
return ClientOptions{
|
|
Endpoint: "localhost:50051",
|
|
ConnectTimeout: time.Second * 5,
|
|
RequestTimeout: time.Second * 10,
|
|
TransportType: "grpc",
|
|
PoolSize: 5,
|
|
TLSEnabled: false,
|
|
MaxRetries: 3,
|
|
InitialBackoff: time.Millisecond * 100,
|
|
MaxBackoff: time.Second * 2,
|
|
BackoffFactor: 1.5,
|
|
RetryJitter: 0.2,
|
|
Compression: CompressionNone,
|
|
MaxMessageSize: 16 * 1024 * 1024, // 16MB
|
|
}
|
|
}
|
|
|
|
// Client represents a connection to a Kevo database server
|
|
type Client struct {
|
|
options ClientOptions
|
|
client transport.Client
|
|
}
|
|
|
|
// NewClient creates a new Kevo client with the given options
|
|
func NewClient(options ClientOptions) (*Client, error) {
|
|
if options.Endpoint == "" {
|
|
return nil, errors.New("endpoint is required")
|
|
}
|
|
|
|
transportOpts := transport.TransportOptions{
|
|
Timeout: options.ConnectTimeout,
|
|
MaxMessageSize: options.MaxMessageSize,
|
|
Compression: options.Compression,
|
|
TLSEnabled: options.TLSEnabled,
|
|
CertFile: options.CertFile,
|
|
KeyFile: options.KeyFile,
|
|
CAFile: options.CAFile,
|
|
RetryPolicy: transport.RetryPolicy{
|
|
MaxRetries: options.MaxRetries,
|
|
InitialBackoff: options.InitialBackoff,
|
|
MaxBackoff: options.MaxBackoff,
|
|
BackoffFactor: options.BackoffFactor,
|
|
Jitter: options.RetryJitter,
|
|
},
|
|
}
|
|
|
|
transportClient, err := transport.GetClient(options.TransportType, options.Endpoint, transportOpts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create transport client: %w", err)
|
|
}
|
|
|
|
return &Client{
|
|
options: options,
|
|
client: transportClient,
|
|
}, nil
|
|
}
|
|
|
|
// Connect establishes a connection to the server
|
|
func (c *Client) Connect(ctx context.Context) error {
|
|
return c.client.Connect(ctx)
|
|
}
|
|
|
|
// Close closes the connection to the server
|
|
func (c *Client) Close() error {
|
|
return c.client.Close()
|
|
}
|
|
|
|
// IsConnected returns whether the client is connected to the server
|
|
func (c *Client) IsConnected() bool {
|
|
return c.client != nil && c.client.IsConnected()
|
|
}
|
|
|
|
// Get retrieves a value by key
|
|
func (c *Client) Get(ctx context.Context, key []byte) ([]byte, bool, error) {
|
|
if !c.IsConnected() {
|
|
return nil, false, errors.New("not connected to server")
|
|
}
|
|
|
|
req := struct {
|
|
Key []byte `json:"key"`
|
|
}{
|
|
Key: key,
|
|
}
|
|
|
|
reqData, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypeGet, reqData))
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var getResp struct {
|
|
Value []byte `json:"value"`
|
|
Found bool `json:"found"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &getResp); err != nil {
|
|
return nil, false, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return getResp.Value, getResp.Found, nil
|
|
}
|
|
|
|
// Put stores a key-value pair
|
|
func (c *Client) Put(ctx context.Context, key, value []byte, sync bool) (bool, error) {
|
|
if !c.IsConnected() {
|
|
return false, errors.New("not connected to server")
|
|
}
|
|
|
|
req := struct {
|
|
Key []byte `json:"key"`
|
|
Value []byte `json:"value"`
|
|
Sync bool `json:"sync"`
|
|
}{
|
|
Key: key,
|
|
Value: value,
|
|
Sync: sync,
|
|
}
|
|
|
|
reqData, err := json.Marshal(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypePut, reqData))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var putResp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &putResp); err != nil {
|
|
return false, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return putResp.Success, nil
|
|
}
|
|
|
|
// Delete removes a key-value pair
|
|
func (c *Client) Delete(ctx context.Context, key []byte, sync bool) (bool, error) {
|
|
if !c.IsConnected() {
|
|
return false, errors.New("not connected to server")
|
|
}
|
|
|
|
req := struct {
|
|
Key []byte `json:"key"`
|
|
Sync bool `json:"sync"`
|
|
}{
|
|
Key: key,
|
|
Sync: sync,
|
|
}
|
|
|
|
reqData, err := json.Marshal(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypeDelete, reqData))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var deleteResp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &deleteResp); err != nil {
|
|
return false, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return deleteResp.Success, nil
|
|
}
|
|
|
|
// BatchOperation represents a single operation in a batch
|
|
type BatchOperation struct {
|
|
Type string // "put" or "delete"
|
|
Key []byte
|
|
Value []byte // only used for "put" operations
|
|
}
|
|
|
|
// BatchWrite performs multiple operations in a single atomic batch
|
|
func (c *Client) BatchWrite(ctx context.Context, operations []BatchOperation, sync bool) (bool, error) {
|
|
if !c.IsConnected() {
|
|
return false, errors.New("not connected to server")
|
|
}
|
|
|
|
req := struct {
|
|
Operations []struct {
|
|
Type string `json:"type"`
|
|
Key []byte `json:"key"`
|
|
Value []byte `json:"value"`
|
|
} `json:"operations"`
|
|
Sync bool `json:"sync"`
|
|
}{
|
|
Sync: sync,
|
|
}
|
|
|
|
for _, op := range operations {
|
|
req.Operations = append(req.Operations, struct {
|
|
Type string `json:"type"`
|
|
Key []byte `json:"key"`
|
|
Value []byte `json:"value"`
|
|
}{
|
|
Type: op.Type,
|
|
Key: op.Key,
|
|
Value: op.Value,
|
|
})
|
|
}
|
|
|
|
reqData, err := json.Marshal(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypeBatchWrite, reqData))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var batchResp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &batchResp); err != nil {
|
|
return false, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return batchResp.Success, nil
|
|
}
|
|
|
|
// GetStats retrieves database statistics
|
|
func (c *Client) GetStats(ctx context.Context) (*Stats, error) {
|
|
if !c.IsConnected() {
|
|
return nil, errors.New("not connected to server")
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
// GetStats doesn't require a payload
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypeGetStats, nil))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var statsResp struct {
|
|
KeyCount int64 `json:"key_count"`
|
|
StorageSize int64 `json:"storage_size"`
|
|
MemtableCount int32 `json:"memtable_count"`
|
|
SstableCount int32 `json:"sstable_count"`
|
|
WriteAmplification float64 `json:"write_amplification"`
|
|
ReadAmplification float64 `json:"read_amplification"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &statsResp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return &Stats{
|
|
KeyCount: statsResp.KeyCount,
|
|
StorageSize: statsResp.StorageSize,
|
|
MemtableCount: statsResp.MemtableCount,
|
|
SstableCount: statsResp.SstableCount,
|
|
WriteAmplification: statsResp.WriteAmplification,
|
|
ReadAmplification: statsResp.ReadAmplification,
|
|
}, nil
|
|
}
|
|
|
|
// Compact triggers compaction of the database
|
|
func (c *Client) Compact(ctx context.Context, force bool) (bool, error) {
|
|
if !c.IsConnected() {
|
|
return false, errors.New("not connected to server")
|
|
}
|
|
|
|
req := struct {
|
|
Force bool `json:"force"`
|
|
}{
|
|
Force: force,
|
|
}
|
|
|
|
reqData, err := json.Marshal(req)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(ctx, c.options.RequestTimeout)
|
|
defer cancel()
|
|
|
|
resp, err := c.client.Send(timeoutCtx, transport.NewRequest(transport.TypeCompact, reqData))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
var compactResp struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
if err := json.Unmarshal(resp.Payload(), &compactResp); err != nil {
|
|
return false, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
return compactResp.Success, nil
|
|
}
|
|
|
|
// Stats contains database statistics
|
|
type Stats struct {
|
|
KeyCount int64
|
|
StorageSize int64
|
|
MemtableCount int32
|
|
SstableCount int32
|
|
WriteAmplification float64
|
|
ReadAmplification float64
|
|
} |