kevo/pkg/client/client.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
}