Skip to content

Latest commit

 

History

History
856 lines (640 loc) · 19.3 KB

File metadata and controls

856 lines (640 loc) · 19.3 KB

ElCache C SDK Guide

The ElCache C SDK provides high-performance cache access with zero external dependencies.

Features

  • Zero dependencies: Only requires POSIX APIs
  • Two transport modes: Unix sockets (reliable) and shared memory (fastest)
  • Partial read support: Efficiently read byte ranges of large values
  • Position-based writes: Write large values in chunks with parallel support
  • Streaming API: Handle values larger than memory
  • Thread safety: Each client instance is single-threaded; create one per thread

Installation

Building

cd build
cmake ..
make elcache_sdk

# Install
sudo make install

Linking

# Dynamic linking
gcc myapp.c -lelcache_sdk -o myapp

# Static linking
gcc myapp.c -l:libelcache_sdk.a -lpthread -lrt -o myapp

Quick Start

#include <elcache/elcache_sdk.h>
#include <stdio.h>
#include <string.h>

int main() {
    // Create client
    elcache_client_t* client = elcache_client_create();
    if (!client) {
        fprintf(stderr, "Failed to create client\n");
        return 1;
    }
    
    // Connect
    elcache_error_t err = elcache_client_connect_unix(client, 
        "/var/run/elcache/elcache.sock");
    if (err != ELCACHE_OK) {
        fprintf(stderr, "Connect failed: %s\n", elcache_error_string(err));
        elcache_client_destroy(client);
        return 1;
    }
    
    // Store a value
    const char* key = "greeting";
    const char* value = "Hello, World!";
    err = elcache_client_put(client, key, strlen(key), 
                             value, strlen(value), NULL);
    if (err != ELCACHE_OK) {
        fprintf(stderr, "Put failed: %s\n", elcache_error_string(err));
    }
    
    // Retrieve the value
    char buffer[1024];
    size_t len;
    err = elcache_client_get(client, key, strlen(key),
                             buffer, sizeof(buffer), &len, NULL);
    if (err == ELCACHE_OK) {
        printf("Got: %.*s\n", (int)len, buffer);
    }
    
    // Cleanup
    elcache_client_destroy(client);
    return 0;
}

Quick Reference: Large File Upload

For values larger than 256KB (the default buffer size), use position-based writes:

#include <elcache/elcache_sdk.h>

int upload_large_file(elcache_client_t* client,
                      const char* key,
                      const void* data,
                      size_t total_size) {
    // 1. Create sparse entry
    elcache_client_create_sparse(client, key, strlen(key), total_size, NULL);
    
    // 2. Write in chunks (can be parallel from multiple threads)
    size_t chunk_size = 200 * 1024;  // 200KB chunks
    for (size_t offset = 0; offset < total_size; offset += chunk_size) {
        size_t len = (offset + chunk_size > total_size)
                     ? (total_size - offset) : chunk_size;
        elcache_client_write_range(client, key, strlen(key),
                                   offset, (char*)data + offset, len);
    }
    
    // 3. Finalize
    return elcache_client_finalize(client, key, strlen(key));
}

API Reference

Client Lifecycle

elcache_client_create

elcache_client_t* elcache_client_create(void);

Create a new client instance.

Returns: Client pointer, or NULL on failure.


elcache_client_destroy

void elcache_client_destroy(elcache_client_t* client);

Destroy client and free all resources.


elcache_client_connect_unix

elcache_error_t elcache_client_connect_unix(
    elcache_client_t* client,
    const char* socket_path
);

Connect via Unix domain socket.

Parameters:

  • client - Client instance
  • socket_path - Path to Unix socket (e.g., /var/run/elcache/elcache.sock)

Returns: ELCACHE_OK on success, error code otherwise.


elcache_client_connect_shm

elcache_error_t elcache_client_connect_shm(
    elcache_client_t* client,
    const char* shm_path
);

Connect via shared memory (fastest, requires same host).

Parameters:

  • client - Client instance
  • shm_path - Shared memory name (e.g., /elcache_shm)

Returns: ELCACHE_OK on success, error code otherwise.


elcache_client_disconnect

void elcache_client_disconnect(elcache_client_t* client);

Disconnect from server.


elcache_client_is_connected

int elcache_client_is_connected(const elcache_client_t* client);

Check connection status.

Returns: Non-zero if connected, 0 otherwise.


Basic Operations

elcache_client_put

elcache_error_t elcache_client_put(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    const void* value,
    size_t value_len,
    const elcache_put_options_t* options  // NULL for defaults
);

Store a value.

Options:

typedef struct {
    int64_t ttl_seconds;     // 0 = no expiry
    uint32_t flags;          // User-defined flags
    int no_memory;           // Skip memory cache
    int no_disk;             // Skip disk cache
    int no_cluster;          // Don't replicate
} elcache_put_options_t;

// Initialize with defaults
elcache_put_options_t opts = ELCACHE_PUT_OPTIONS_INIT;

elcache_client_get

elcache_error_t elcache_client_get(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    void* buffer,
    size_t buffer_size,
    size_t* value_len,        // Output: actual value length
    const elcache_get_options_t* options  // NULL for defaults
);

Retrieve a value.

Options:

typedef struct {
    uint64_t offset;         // Start offset (for partial read)
    uint64_t length;         // Length to read (0 = entire value)
    int allow_partial;       // Return partial data if available
    int timeout_ms;          // Timeout (0 = default)
} elcache_get_options_t;

// Initialize with defaults
elcache_get_options_t opts = ELCACHE_GET_OPTIONS_INIT;

Returns:

  • ELCACHE_OK - Full value returned
  • ELCACHE_ERR_NOT_FOUND - Key not found
  • ELCACHE_ERR_BUFFER_TOO_SMALL - Buffer too small (value_len contains required size)
  • ELCACHE_ERR_PARTIAL - Partial data returned (when allow_partial is set)

elcache_client_get_alloc

elcache_error_t elcache_client_get_alloc(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    void** value,             // Output: allocated buffer
    size_t* value_len,        // Output: value length
    const elcache_get_options_t* options
);

Retrieve a value with automatic allocation.

Important: Caller must free the buffer with elcache_free().


elcache_client_delete

elcache_error_t elcache_client_delete(
    elcache_client_t* client,
    const void* key,
    size_t key_len
);

Delete a value.


elcache_client_exists

elcache_error_t elcache_client_exists(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    int* exists               // Output: 1 if exists, 0 otherwise
);

Check if a key exists.


Partial Reads

elcache_client_read_range

elcache_error_t elcache_client_read_range(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    uint64_t offset,
    void* buffer,
    size_t buffer_size,
    size_t* bytes_read
);

Read a specific byte range.

Example:

// Read bytes 1000-1999 of a large file
char buffer[1000];
size_t bytes_read;
err = elcache_client_read_range(client, "bigfile", 7, 
                                 1000,  // offset
                                 buffer, sizeof(buffer),
                                 &bytes_read);

Position-Based Writes (Sparse/Parallel)

The position-based write API allows you to write large values in parts, potentially from multiple threads in parallel. This is useful for:

  • Writing files larger than the SDK buffer size (default 256KB)
  • Parallel uploads from multiple sources
  • Resumable uploads
  • Assembling data from multiple producers

Workflow

  1. Create a sparse entry with total size
  2. Write ranges at specific offsets (can be parallel)
  3. Finalize to commit to cache (validates all bytes written)

elcache_client_create_sparse

elcache_error_t elcache_client_create_sparse(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    uint64_t total_size,
    const elcache_put_options_t* options  // NULL for defaults
);

Create a sparse entry that will be filled with position-based writes.

Parameters:

  • client - Client instance
  • key - Cache key
  • key_len - Key length in bytes
  • total_size - Total size of the final value in bytes
  • options - Optional put options (TTL, flags, etc.)

Returns:

  • ELCACHE_OK - Entry created
  • ELCACHE_ERR_INVALID_ARG - Invalid parameters
  • ELCACHE_ERR_INTERNAL - Entry already exists

elcache_client_write_range

elcache_error_t elcache_client_write_range(
    elcache_client_t* client,
    const void* key,
    size_t key_len,
    uint64_t offset,
    const void* data,
    size_t data_len
);

Write data at a specific offset within a sparse entry.

Parameters:

  • client - Client instance
  • key - Cache key
  • key_len - Key length in bytes
  • offset - Byte offset where data should be written
  • data - Data buffer
  • data_len - Length of data to write

Returns:

  • ELCACHE_OK - Data written successfully
  • ELCACHE_ERR_NOT_FOUND - Sparse entry doesn't exist
  • ELCACHE_ERR_INVALID_ARG - Range exceeds total size
  • ELCACHE_ERR_VALUE_TOO_LARGE - Data too large for buffer

Note: Multiple write_range calls can overlap or be called in parallel from different threads/connections. Each byte position only needs to be written once.


elcache_client_finalize

elcache_error_t elcache_client_finalize(
    elcache_client_t* client,
    const void* key,
    size_t key_len
);

Finalize a sparse entry, validating all bytes have been written and moving it to the main cache.

Parameters:

  • client - Client instance
  • key - Cache key
  • key_len - Key length in bytes

Returns:

  • ELCACHE_OK - Entry finalized and available in cache
  • ELCACHE_ERR_NOT_FOUND - Sparse entry doesn't exist
  • ELCACHE_ERR_PARTIAL - Not all byte ranges have been written

Example: Writing a 1MB file in chunks

#include <elcache/elcache_sdk.h>
#include <string.h>

int upload_large_file(elcache_client_t* client, 
                      const char* key,
                      const uint8_t* data, 
                      size_t total_size) {
    // Step 1: Create sparse entry
    elcache_error_t err = elcache_client_create_sparse(
        client, key, strlen(key), total_size, NULL);
    if (err != ELCACHE_OK) {
        return -1;
    }
    
    // Step 2: Write in 200KB chunks
    const size_t chunk_size = 200 * 1024;
    for (size_t offset = 0; offset < total_size; offset += chunk_size) {
        size_t len = (offset + chunk_size > total_size) 
                     ? (total_size - offset) 
                     : chunk_size;
        
        err = elcache_client_write_range(
            client, key, strlen(key), offset, data + offset, len);
        if (err != ELCACHE_OK) {
            return -1;
        }
    }
    
    // Step 3: Finalize
    err = elcache_client_finalize(client, key, strlen(key));
    if (err != ELCACHE_OK) {
        return -1;
    }
    
    return 0;
}

Example: Parallel writes from multiple threads

#include <elcache/elcache_sdk.h>
#include <pthread.h>

typedef struct {
    const char* socket_path;
    const char* key;
    const uint8_t* data;
    size_t offset;
    size_t length;
    int success;
} write_task_t;

void* write_worker(void* arg) {
    write_task_t* task = (write_task_t*)arg;
    
    // Each thread creates its own client connection
    elcache_client_t* client = elcache_client_create();
    elcache_client_connect_unix(client, task->socket_path);
    
    // Write this thread's chunk
    elcache_error_t err = elcache_client_write_range(
        client, task->key, strlen(task->key),
        task->offset, task->data + task->offset, task->length);
    
    task->success = (err == ELCACHE_OK);
    
    elcache_client_destroy(client);
    return NULL;
}

int parallel_upload(const char* socket_path,
                    const char* key,
                    const uint8_t* data,
                    size_t total_size,
                    int num_threads) {
    // Create sparse entry first
    elcache_client_t* client = elcache_client_create();
    elcache_client_connect_unix(client, socket_path);
    
    elcache_error_t err = elcache_client_create_sparse(
        client, key, strlen(key), total_size, NULL);
    if (err != ELCACHE_OK) {
        elcache_client_destroy(client);
        return -1;
    }
    
    // Spawn threads for parallel writes
    pthread_t* threads = malloc(num_threads * sizeof(pthread_t));
    write_task_t* tasks = malloc(num_threads * sizeof(write_task_t));
    
    size_t chunk_size = total_size / num_threads;
    for (int i = 0; i < num_threads; i++) {
        tasks[i].socket_path = socket_path;
        tasks[i].key = key;
        tasks[i].data = data;
        tasks[i].offset = i * chunk_size;
        tasks[i].length = (i == num_threads - 1) 
                          ? (total_size - tasks[i].offset) 
                          : chunk_size;
        tasks[i].success = 0;
        
        pthread_create(&threads[i], NULL, write_worker, &tasks[i]);
    }
    
    // Wait for all threads
    int all_success = 1;
    for (int i = 0; i < num_threads; i++) {
        pthread_join(threads[i], NULL);
        if (!tasks[i].success) all_success = 0;
    }
    
    free(threads);
    free(tasks);
    
    if (!all_success) {
        elcache_client_destroy(client);
        return -1;
    }
    
    // Finalize
    err = elcache_client_finalize(client, key, strlen(key));
    elcache_client_destroy(client);
    
    return (err == ELCACHE_OK) ? 0 : -1;
}

Streaming API

For values too large to fit in memory:

Write Stream

elcache_write_stream_t* stream;

// Start stream
err = elcache_client_write_stream_start(client,
    key, key_len,
    total_size,
    NULL,  // options
    &stream);

// Write chunks
while (has_more_data) {
    err = elcache_write_stream_write(stream, chunk, chunk_len);
}

// Finish
err = elcache_write_stream_finish(stream);

// Or abort
elcache_write_stream_abort(stream);

Read Stream

elcache_read_stream_t* stream;

// Start stream
err = elcache_client_read_stream_start(client,
    key, key_len,
    NULL,  // options
    &stream);

// Get total size
uint64_t total = elcache_read_stream_total_size(stream);

// Read chunks
char buffer[65536];
size_t bytes_read;
while (!elcache_read_stream_eof(stream)) {
    err = elcache_read_stream_read(stream, buffer, sizeof(buffer), &bytes_read);
    // process buffer...
}

// Close
elcache_read_stream_close(stream);

Configuration

elcache_client_set_timeout

void elcache_client_set_timeout(elcache_client_t* client, int timeout_ms);

Set operation timeout in milliseconds.


elcache_client_set_recv_buffer

void elcache_client_set_recv_buffer(elcache_client_t* client, size_t size);

Set receive buffer size.


elcache_client_set_send_buffer

void elcache_client_set_send_buffer(elcache_client_t* client, size_t size);

Set send buffer size.


Statistics

elcache_client_stats

typedef struct {
    uint64_t hits;
    uint64_t misses;
    uint64_t partial_hits;
    uint64_t bytes_read;
    uint64_t bytes_written;
    uint64_t latency_us_avg;
    uint64_t latency_us_p99;
} elcache_stats_t;

elcache_error_t elcache_client_stats(
    elcache_client_t* client,
    elcache_stats_t* stats
);

Get client-side statistics.


Error Handling

Error Codes

typedef enum {
    ELCACHE_OK = 0,
    ELCACHE_ERR_INVALID_ARG = -1,
    ELCACHE_ERR_NOT_FOUND = -2,
    ELCACHE_ERR_PARTIAL = -3,
    ELCACHE_ERR_TIMEOUT = -4,
    ELCACHE_ERR_CONNECTION = -5,
    ELCACHE_ERR_PROTOCOL = -6,
    ELCACHE_ERR_BUFFER_TOO_SMALL = -7,
    ELCACHE_ERR_KEY_TOO_LARGE = -8,
    ELCACHE_ERR_VALUE_TOO_LARGE = -9,
    ELCACHE_ERR_OUT_OF_MEMORY = -10,
    ELCACHE_ERR_INTERNAL = -11,
} elcache_error_t;

elcache_error_string

const char* elcache_error_string(elcache_error_t error);

Get human-readable error message.

elcache_client_last_error

const char* elcache_client_last_error(const elcache_client_t* client);

Get detailed error message from last operation.


Memory Management

elcache_free

void elcache_free(void* ptr);

Free memory allocated by elcache_client_get_alloc().


Examples

Caching HTTP responses

#include <elcache/elcache_sdk.h>

// Cache an HTTP response
void cache_response(elcache_client_t* client, 
                    const char* url,
                    const char* body, size_t body_len,
                    int ttl_seconds) {
    elcache_put_options_t opts = ELCACHE_PUT_OPTIONS_INIT;
    opts.ttl_seconds = ttl_seconds;
    
    elcache_client_put(client, url, strlen(url), body, body_len, &opts);
}

// Get cached response
char* get_cached_response(elcache_client_t* client, const char* url) {
    void* value;
    size_t len;
    
    elcache_error_t err = elcache_client_get_alloc(client, 
        url, strlen(url), &value, &len, NULL);
    
    if (err == ELCACHE_OK) {
        return (char*)value;  // Caller must elcache_free()
    }
    return NULL;
}

Reading a video file in chunks

#include <elcache/elcache_sdk.h>

void stream_video(elcache_client_t* client, const char* video_id) {
    elcache_read_stream_t* stream;
    
    elcache_error_t err = elcache_client_read_stream_start(
        client, video_id, strlen(video_id), NULL, &stream);
    
    if (err != ELCACHE_OK) {
        return;
    }
    
    uint64_t total = elcache_read_stream_total_size(stream);
    printf("Streaming %lu bytes\n", total);
    
    char buffer[1024 * 1024];  // 1MB chunks
    size_t bytes_read;
    
    while (!elcache_read_stream_eof(stream)) {
        err = elcache_read_stream_read(stream, buffer, sizeof(buffer), &bytes_read);
        if (err == ELCACHE_OK) {
            // Send to video player...
            send_to_player(buffer, bytes_read);
        }
    }
    
    elcache_read_stream_close(stream);
}

Thread-safe usage

#include <elcache/elcache_sdk.h>
#include <pthread.h>

// Thread-local client
__thread elcache_client_t* tls_client = NULL;

elcache_client_t* get_client() {
    if (!tls_client) {
        tls_client = elcache_client_create();
        elcache_client_connect_unix(tls_client, "/var/run/elcache/elcache.sock");
    }
    return tls_client;
}

void* worker_thread(void* arg) {
    elcache_client_t* client = get_client();
    
    // Use client safely in this thread...
    
    return NULL;
}

Performance Tips

  1. Use shared memory transport when client and server are on the same host
  2. Reuse client instances - connection setup has overhead
  3. Use partial reads for large values when you only need a portion
  4. Batch operations by keeping the connection open
  5. Tune buffer sizes based on your typical value sizes

Limits

Limit Value
Maximum key size 8 KB
Maximum value size 20 TB
Default timeout 30 seconds
Default buffer size 256 KB
Maximum single PUT size Buffer size (default 256 KB)
Sparse write chunk size Buffer size (default 256 KB)

Note: For values larger than the buffer size, use the position-based write API (create_sparse, write_range, finalize) which allows uploading in chunks.