diff --git a/.gitignore b/.gitignore index 436edca..3deefac 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,9 @@ toastyfs_simulation *.gcov *.info coverage_html/ -*.log \ No newline at end of file +*.log +*.o +*.a +*.so +examples/example +.cluster/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3ed195 --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +CC ?= gcc +CFLAGS ?= -Wall -Wextra -ggdb -O0 +AR ?= ar + +INCLUDES = -Iquakey/include -Iinclude -I. + +# ---- Library (client-side only) ---- + +LIB_SRCS = src/client.c src/tcp.c src/byte_queue.c src/message.c src/basic.c +LIB_OBJS = $(LIB_SRCS:.c=.o) + +STATIC_LIB = libtoastyfs.a +SHARED_LIB = libtoastyfs.so + +# ---- Server binary ---- + +SERVER_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ + src/tcp.c src/server.c src/main.c src/log.c src/client_table.c \ + src/chunk_store.c src/metadata.c + +# ---- Client binary (random test client) ---- + +CLIENT_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ + src/tcp.c src/server.c src/client.c src/random_client.c src/main.c \ + src/log.c src/client_table.c src/chunk_store.c src/metadata.c + +# ---- Simulation binary ---- + +SIM_SRCS = src/basic.c src/file_system.c src/byte_queue.c src/message.c \ + src/tcp.c src/server.c src/client.c src/random_client.c src/main.c \ + src/log.c src/client_table.c src/invariant_checker.c src/chunk_store.c \ + src/metadata.c quakey/src/mockfs.c quakey/src/quakey.c + +# ---- Default target ---- + +all: $(STATIC_LIB) $(SHARED_LIB) toastyfs toastyfs_client toastyfs_simulation + +# ---- Library targets ---- + +lib: $(STATIC_LIB) $(SHARED_LIB) + +$(STATIC_LIB): $(LIB_OBJS) + $(AR) rcs $@ $^ + +$(SHARED_LIB): $(LIB_SRCS) + $(CC) $(CFLAGS) -shared -fPIC $(INCLUDES) $^ -o $@ + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(INCLUDES) -fPIC -c $< -o $@ + +# ---- Binary targets ---- + +toastyfs: $(SERVER_SRCS) + $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_SERVER $^ -o $@ + +toastyfs_client: $(CLIENT_SRCS) + $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_CLIENT $^ -o $@ + +toastyfs_simulation: $(SIM_SRCS) + $(CC) $(CFLAGS) $(INCLUDES) -DMAIN_SIMULATION -DFAULT_INJECTION $^ -o $@ + +# ---- Install ---- + +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR?= $(PREFIX)/include + +install: $(STATIC_LIB) $(SHARED_LIB) + install -d $(DESTDIR)$(LIBDIR) + install -d $(DESTDIR)$(INCLUDEDIR) + install -m 644 $(STATIC_LIB) $(DESTDIR)$(LIBDIR)/ + install -m 755 $(SHARED_LIB) $(DESTDIR)$(LIBDIR)/ + install -m 644 include/toastyfs.h $(DESTDIR)$(INCLUDEDIR)/ + +# ---- Clean ---- + +clean: + rm -f $(LIB_OBJS) $(STATIC_LIB) $(SHARED_LIB) + rm -f toastyfs toastyfs_client toastyfs_simulation + +.PHONY: all lib clean install diff --git a/cluster.sh b/cluster.sh new file mode 100755 index 0000000..0d8800e --- /dev/null +++ b/cluster.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# +# cluster.sh - Manage a local 3-node ToastyFS cluster. +# +# Usage: +# ./cluster.sh up Build the server and start 3 nodes +# ./cluster.sh down Stop all nodes +# ./cluster.sh status Show which nodes are running +# ./cluster.sh logs [1|2|3] Tail logs (all nodes, or one) +# ./cluster.sh run Build the library, compile +# against it, start the cluster, run the +# binary, then tear down the cluster. +# +# The three server nodes listen on: +# 127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8083 +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CLUSTER_DIR="$SCRIPT_DIR/.cluster" +ADDRS=("127.0.0.1:8081" "127.0.0.1:8082" "127.0.0.1:8083") + +usage() { + sed -n '3,14s/^# \?//p' "$0" + exit 1 +} + +cluster_up() { + echo "==> Building server..." + make -C "$SCRIPT_DIR" toastyfs + + mkdir -p "$CLUSTER_DIR" + + for i in 1 2 3; do + pidfile="$CLUSTER_DIR/node${i}.pid" + if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then + echo " Node $i already running (pid $(cat "$pidfile"))" + continue + fi + + node_dir="$CLUSTER_DIR/node${i}" + mkdir -p "$node_dir" + + # Build argument list: --addr for self, --peer for the others. + args=() + for j in 1 2 3; do + idx=$((j - 1)) + if [ "$j" -eq "$i" ]; then + args+=(--addr "${ADDRS[$idx]}") + else + args+=(--peer "${ADDRS[$idx]}") + fi + done + + ( + cd "$node_dir" + # ServerState is ~40 MB (MetaStore), which exceeds the + # default 8 MB stack. Raise the limit for the server. + ulimit -s unlimited + "$SCRIPT_DIR/toastyfs" "${args[@]}" \ + >> "$CLUSTER_DIR/node${i}.log" 2>&1 & + echo $! > "$pidfile" + ) + echo " Node $i started (pid $(cat "$pidfile")) on ${ADDRS[$((i-1))]}" + done + echo "==> Cluster is running." +} + +cluster_down() { + if [ ! -d "$CLUSTER_DIR" ]; then + echo "No cluster state found." + return + fi + + for i in 1 2 3; do + pidfile="$CLUSTER_DIR/node${i}.pid" + if [ -f "$pidfile" ]; then + pid="$(cat "$pidfile")" + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + echo " Node $i stopped (pid $pid)" + else + echo " Node $i was not running" + fi + rm -f "$pidfile" + fi + done + + # Clean up node data directories so fresh starts don't trigger + # the crash-recovery path (vsr_boot_marker). + rm -rf "$CLUSTER_DIR" + echo "==> Cluster stopped." +} + +cluster_status() { + if [ ! -d "$CLUSTER_DIR" ]; then + echo "No cluster state found. Run './cluster.sh up' first." + return + fi + + printf "%-8s %-8s %-20s %s\n" "NODE" "PID" "ADDRESS" "STATUS" + for i in 1 2 3; do + pidfile="$CLUSTER_DIR/node${i}.pid" + addr="${ADDRS[$((i-1))]}" + if [ -f "$pidfile" ]; then + pid="$(cat "$pidfile")" + if kill -0 "$pid" 2>/dev/null; then + printf "%-8s %-8s %-20s %s\n" "node$i" "$pid" "$addr" "running" + else + printf "%-8s %-8s %-20s %s\n" "node$i" "$pid" "$addr" "dead" + fi + else + printf "%-8s %-8s %-20s %s\n" "node$i" "-" "$addr" "not started" + fi + done +} + +cluster_logs() { + if [ $# -gt 0 ]; then + logfile="$CLUSTER_DIR/node${1}.log" + if [ ! -f "$logfile" ]; then + echo "No log file for node $1." + return 1 + fi + tail -f "$logfile" + else + tail -f "$CLUSTER_DIR"/node*.log + fi +} + +cluster_run() { + local src="$1" + local bin + bin="$(basename "${src%.c}")" + + echo "==> Building libtoastyfs..." + make -C "$SCRIPT_DIR" lib + + echo "==> Compiling $src..." + ${CC:-gcc} -Wall -Wextra -o "$bin" "$src" \ + -I"$SCRIPT_DIR/include" \ + -L"$SCRIPT_DIR" \ + -ltoastyfs + + echo "==> Starting cluster..." + cluster_up + # Give the servers a moment to elect a leader. + sleep 2 + + echo "==> Running ./$bin" + echo "---" + LD_LIBRARY_PATH="$SCRIPT_DIR:${LD_LIBRARY_PATH:-}" "./$bin" || true + echo "---" + + echo "==> Stopping cluster..." + cluster_down +} + +if [ $# -lt 1 ]; then + usage +fi + +case "$1" in + up) cluster_up ;; + down) cluster_down ;; + status) cluster_status ;; + logs) shift; cluster_logs "$@" ;; + run) + if [ $# -lt 2 ]; then + echo "Error: 'run' requires a source file argument." >&2 + usage + fi + cluster_run "$2" + ;; + *) usage ;; +esac diff --git a/examples/example.c b/examples/example.c new file mode 100644 index 0000000..7f4cc21 --- /dev/null +++ b/examples/example.c @@ -0,0 +1,120 @@ +/* + * example.c - Minimal ToastyFS client example. + * + * Connects to a running 3-node cluster (ports 8081-8083 on localhost), + * performs a PUT, a GET, and a DELETE using the synchronous API, then + * verifies the results. + * + * Build & run against a live cluster: + * ./cluster.sh run examples/example.c + * + * Or manually: + * make lib + * gcc -Wall -Wextra -o example examples/example.c \ + * -Iinclude -L. -ltoastyfs + * LD_LIBRARY_PATH=. ./example + */ + +#include +#include +#include +#include + +static const char *error_name(ToastyFS_Error e) +{ + switch (e) { + case TOASTYFS_ERROR_VOID: return "OK"; + case TOASTYFS_ERROR_OUT_OF_MEMORY: return "OUT_OF_MEMORY"; + case TOASTYFS_ERROR_UNEXPECTED_MESSAGE: return "UNEXPECTED_MESSAGE"; + case TOASTYFS_ERROR_REJECTED: return "REJECTED"; + case TOASTYFS_ERROR_FULL: return "FULL"; + case TOASTYFS_ERROR_NOT_FOUND: return "NOT_FOUND"; + case TOASTYFS_ERROR_TRANSFER_FAILED: return "TRANSFER_FAILED"; + } + return "UNKNOWN"; +} + +int main(void) +{ + /* Cluster addresses (mapped via docker-compose ports). + * These must be writable char arrays because parse_addr_arg + * temporarily modifies the string in-place. */ + char addr1[] = "127.0.0.1:8081"; + char addr2[] = "127.0.0.1:8082"; + char addr3[] = "127.0.0.1:8083"; + char *addrs[] = { addr1, addr2, addr3 }; + + printf("Connecting to cluster...\n"); + ToastyFS *tfs = toastyfs_init(1, addrs, 3); + if (tfs == NULL) { + fprintf(stderr, "toastyfs_init failed\n"); + return 1; + } + + ToastyFS_Result res; + int ret; + + /* ---- PUT ---- */ + char key[] = "hello"; + char data[] = "world"; + printf("PUT key=\"%s\" data=\"%s\" (%d bytes)\n", + key, data, (int)strlen(data)); + + ret = toastyfs_put(tfs, key, strlen(key), data, strlen(data), &res); + if (ret < 0) { + fprintf(stderr, "toastyfs_put returned %d\n", ret); + toastyfs_free(tfs); + return 1; + } + printf(" result: %s\n", error_name(res.error)); + + /* ---- GET ---- */ + printf("GET key=\"%s\"\n", key); + ret = toastyfs_get(tfs, key, strlen(key), &res); + if (ret < 0) { + fprintf(stderr, "toastyfs_get returned %d\n", ret); + toastyfs_free(tfs); + return 1; + } + printf(" result: %s\n", error_name(res.error)); + if (res.error == TOASTYFS_ERROR_VOID && res.data) { + printf(" data: \"%.*s\" (%d bytes)\n", res.size, res.data, res.size); + if (res.size == (int)strlen(data) && + memcmp(res.data, data, res.size) == 0) { + printf(" PASS - data matches\n"); + } else { + printf(" FAIL - data mismatch\n"); + } + free(res.data); + } + + /* ---- DELETE ---- */ + printf("DELETE key=\"%s\"\n", key); + ret = toastyfs_delete(tfs, key, strlen(key), &res); + if (ret < 0) { + fprintf(stderr, "toastyfs_delete returned %d\n", ret); + toastyfs_free(tfs); + return 1; + } + printf(" result: %s\n", error_name(res.error)); + + /* ---- GET after DELETE (expect NOT_FOUND) ---- */ + printf("GET key=\"%s\" (after delete)\n", key); + ret = toastyfs_get(tfs, key, strlen(key), &res); + if (ret < 0) { + fprintf(stderr, "toastyfs_get returned %d\n", ret); + toastyfs_free(tfs); + return 1; + } + printf(" result: %s\n", error_name(res.error)); + if (res.error == TOASTYFS_ERROR_NOT_FOUND) { + printf(" PASS - key not found as expected\n"); + } else { + printf(" FAIL - expected NOT_FOUND\n"); + free(res.data); + } + + toastyfs_free(tfs); + printf("Done.\n"); + return 0; +} diff --git a/src/main.c b/src/main.c index 2721efb..b80d5cc 100644 --- a/src/main.c +++ b/src/main.c @@ -255,7 +255,14 @@ int main(int argc, char **argv) int main(int argc, char **argv) { int ret; - ServerState state; + + // ServerState is ~40 MB (MetaStore holds 4096 ObjectMeta entries), + // which exceeds the default stack size. Heap-allocate it. + ServerState *state = malloc(sizeof(ServerState)); + if (state == NULL) { + fprintf(stderr, "Failed to allocate ServerState\n"); + return -1; + } void* poll_ctxs[POLL_CAPACITY]; struct pollfd poll_array[POLL_CAPACITY]; @@ -263,7 +270,7 @@ int main(int argc, char **argv) int poll_timeout; ret = server_init( - &state, + state, argc, argv, poll_ctxs, @@ -284,7 +291,7 @@ int main(int argc, char **argv) #endif ret = server_tick( - &state, + state, poll_ctxs, poll_array, POLL_CAPACITY, @@ -295,7 +302,8 @@ int main(int argc, char **argv) return -1; } - server_free(&state); + server_free(state); + free(state); return 0; }