Add library build, local cluster script, and example client
Introduce a Makefile that builds libtoastyfs.a and libtoastyfs.so from the client-side sources (client, tcp, byte_queue, message, basic) so external programs can link against the ToastyFS client API in include/toastyfs.h. Add cluster.sh, a convenience script that manages a local 3-node cluster by spawning toastyfs server processes on ports 8081-8083. Sub-commands: up, down, status, logs, and "run <source.c>" which builds the library, compiles the source against it, starts the cluster, runs the binary, and tears everything down. Add examples/example.c demonstrating synchronous PUT, GET, DELETE, and a NOT_FOUND verification. Fix a stack overflow in the MAIN_SERVER entry point: ServerState is ~40 MB (MetaStore holds 4096 ObjectMeta entries) and was declared on the stack. Heap-allocate it instead. https://claude.ai/code/session_019DVhmc25jfzcHnmdNxzib2
This commit is contained in:
@@ -7,3 +7,8 @@ toastyfs_simulation
|
|||||||
*.info
|
*.info
|
||||||
coverage_html/
|
coverage_html/
|
||||||
*.log
|
*.log
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
examples/example
|
||||||
|
.cluster/
|
||||||
@@ -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
|
||||||
Executable
+176
@@ -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 <source.c> Build the library, compile <source.c>
|
||||||
|
# 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
|
||||||
@@ -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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <toastyfs.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+12
-4
@@ -255,7 +255,14 @@ int main(int argc, char **argv)
|
|||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
int ret;
|
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];
|
void* poll_ctxs[POLL_CAPACITY];
|
||||||
struct pollfd poll_array[POLL_CAPACITY];
|
struct pollfd poll_array[POLL_CAPACITY];
|
||||||
@@ -263,7 +270,7 @@ int main(int argc, char **argv)
|
|||||||
int poll_timeout;
|
int poll_timeout;
|
||||||
|
|
||||||
ret = server_init(
|
ret = server_init(
|
||||||
&state,
|
state,
|
||||||
argc,
|
argc,
|
||||||
argv,
|
argv,
|
||||||
poll_ctxs,
|
poll_ctxs,
|
||||||
@@ -284,7 +291,7 @@ int main(int argc, char **argv)
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
ret = server_tick(
|
ret = server_tick(
|
||||||
&state,
|
state,
|
||||||
poll_ctxs,
|
poll_ctxs,
|
||||||
poll_array,
|
poll_array,
|
||||||
POLL_CAPACITY,
|
POLL_CAPACITY,
|
||||||
@@ -295,7 +302,8 @@ int main(int argc, char **argv)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
server_free(&state);
|
server_free(state);
|
||||||
|
free(state);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user