kevo/pkg/client/transaction.go
Jeremy Tregunna a0a1c0512f
All checks were successful
Go Tests / Run Tests (1.24.2) (push) Successful in 9m50s
chore: formatting
2025-04-22 14:09:54 -06:00

289 lines
6.6 KiB
Go

package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"github.com/KevoDB/kevo/pkg/transport"
)
// Transaction represents a database transaction
type Transaction struct {
client *Client
id string
readOnly bool
closed bool
mu sync.RWMutex
}
// ErrTransactionClosed is returned when attempting to use a closed transaction
var ErrTransactionClosed = errors.New("transaction is closed")
// BeginTransaction starts a new transaction
func (c *Client) BeginTransaction(ctx context.Context, readOnly bool) (*Transaction, error) {
if !c.IsConnected() {
return nil, errors.New("not connected to server")
}
req := struct {
ReadOnly bool `json:"read_only"`
}{
ReadOnly: readOnly,
}
reqData, err := json.Marshal(req)
if err != nil {
return nil, 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.TypeBeginTx, reqData))
if err != nil {
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
var txResp struct {
TransactionID string `json:"transaction_id"`
}
if err := json.Unmarshal(resp.Payload(), &txResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &Transaction{
client: c,
id: txResp.TransactionID,
readOnly: readOnly,
closed: false,
}, nil
}
// Commit commits the transaction
func (tx *Transaction) Commit(ctx context.Context) error {
tx.mu.Lock()
defer tx.mu.Unlock()
if tx.closed {
return ErrTransactionClosed
}
req := struct {
TransactionID string `json:"transaction_id"`
}{
TransactionID: tx.id,
}
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
timeoutCtx, cancel := context.WithTimeout(ctx, tx.client.options.RequestTimeout)
defer cancel()
resp, err := tx.client.client.Send(timeoutCtx, transport.NewRequest(transport.TypeCommitTx, reqData))
if err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
var commitResp struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(resp.Payload(), &commitResp); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
tx.closed = true
if !commitResp.Success {
return errors.New("transaction commit failed")
}
return nil
}
// Rollback aborts the transaction
func (tx *Transaction) Rollback(ctx context.Context) error {
tx.mu.Lock()
defer tx.mu.Unlock()
if tx.closed {
return ErrTransactionClosed
}
req := struct {
TransactionID string `json:"transaction_id"`
}{
TransactionID: tx.id,
}
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal request: %w", err)
}
timeoutCtx, cancel := context.WithTimeout(ctx, tx.client.options.RequestTimeout)
defer cancel()
resp, err := tx.client.client.Send(timeoutCtx, transport.NewRequest(transport.TypeRollbackTx, reqData))
if err != nil {
return fmt.Errorf("failed to rollback transaction: %w", err)
}
var rollbackResp struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(resp.Payload(), &rollbackResp); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
tx.closed = true
if !rollbackResp.Success {
return errors.New("transaction rollback failed")
}
return nil
}
// Get retrieves a value by key within the transaction
func (tx *Transaction) Get(ctx context.Context, key []byte) ([]byte, bool, error) {
tx.mu.RLock()
defer tx.mu.RUnlock()
if tx.closed {
return nil, false, ErrTransactionClosed
}
req := struct {
TransactionID string `json:"transaction_id"`
Key []byte `json:"key"`
}{
TransactionID: tx.id,
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, tx.client.options.RequestTimeout)
defer cancel()
resp, err := tx.client.client.Send(timeoutCtx, transport.NewRequest(transport.TypeTxGet, 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 within the transaction
func (tx *Transaction) Put(ctx context.Context, key, value []byte) (bool, error) {
tx.mu.RLock()
defer tx.mu.RUnlock()
if tx.closed {
return false, ErrTransactionClosed
}
if tx.readOnly {
return false, errors.New("cannot write to a read-only transaction")
}
req := struct {
TransactionID string `json:"transaction_id"`
Key []byte `json:"key"`
Value []byte `json:"value"`
}{
TransactionID: tx.id,
Key: key,
Value: value,
}
reqData, err := json.Marshal(req)
if err != nil {
return false, fmt.Errorf("failed to marshal request: %w", err)
}
timeoutCtx, cancel := context.WithTimeout(ctx, tx.client.options.RequestTimeout)
defer cancel()
resp, err := tx.client.client.Send(timeoutCtx, transport.NewRequest(transport.TypeTxPut, 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 within the transaction
func (tx *Transaction) Delete(ctx context.Context, key []byte) (bool, error) {
tx.mu.RLock()
defer tx.mu.RUnlock()
if tx.closed {
return false, ErrTransactionClosed
}
if tx.readOnly {
return false, errors.New("cannot delete in a read-only transaction")
}
req := struct {
TransactionID string `json:"transaction_id"`
Key []byte `json:"key"`
}{
TransactionID: tx.id,
Key: key,
}
reqData, err := json.Marshal(req)
if err != nil {
return false, fmt.Errorf("failed to marshal request: %w", err)
}
timeoutCtx, cancel := context.WithTimeout(ctx, tx.client.options.RequestTimeout)
defer cancel()
resp, err := tx.client.client.Send(timeoutCtx, transport.NewRequest(transport.TypeTxDelete, 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
}