diff --git a/src/cache/disk_cache.cpp b/src/cache/disk_cache.cpp index 361f9de..20d2d6f 100644 --- a/src/cache/disk_cache.cpp +++ b/src/cache/disk_cache.cpp @@ -49,14 +49,9 @@ elio::coro::task DiskStore::read_file( uint64_t offset, uint64_t length) { - int flags = O_RDONLY; -#ifdef O_DIRECT - if (config_.use_direct_io && length >= config_.block_size) { - flags |= O_DIRECT; - } -#endif - - int fd = ::open(path.c_str(), flags); + // Note: O_DIRECT requires aligned buffers which we can't guarantee, + // so we use regular buffered I/O. + int fd = ::open(path.c_str(), O_RDONLY); if (fd < 0) { co_return ByteBuffer{}; } @@ -72,22 +67,19 @@ elio::coro::task DiskStore::read_file( ByteBuffer buffer(length); size_t total_read = 0; - // Use Elio async read with offset support + // Use synchronous pread for regular files + // (epoll backend cannot handle regular files - they don't support polling) while (total_read < length) { - auto result = co_await elio::io::async_read( - io_ctx_, fd, - buffer.data() + total_read, - length - total_read, - static_cast(offset + total_read)); - - if (!result.success() || result.result <= 0) { + ssize_t n = ::pread(fd, buffer.data() + total_read, + length - total_read, + static_cast(offset + total_read)); + if (n <= 0) { break; } - total_read += result.result; + total_read += n; } - // Async close - co_await elio::io::async_close(io_ctx_, fd); + ::close(fd); buffer.resize(total_read); co_return buffer; @@ -98,16 +90,15 @@ elio::coro::task DiskStore::write_file( ByteView data, bool sync) { - int flags = O_WRONLY | O_CREAT | O_TRUNC; -#ifdef O_DIRECT - if (config_.use_direct_io && data.size() >= config_.block_size) { - flags |= O_DIRECT; - } -#endif + // Note: O_DIRECT requires aligned buffers which we can't guarantee, + // so we don't use it here. Regular buffered I/O is sufficient for + // most use cases and the kernel's page cache helps performance. + int flags = O_RDWR | O_CREAT | O_TRUNC; int fd = ::open(path.c_str(), flags, 0644); if (fd < 0) { - co_return Status::error(ErrorCode::DiskError, "Failed to open file for writing"); + co_return Status::error(ErrorCode::DiskError, + std::string("Failed to open file for writing: ") + strerror(errno)); } // Ensure file is readable regardless of umask @@ -115,33 +106,37 @@ elio::coro::task DiskStore::write_file( // Pre-allocate if supported #ifdef __linux__ - if (config_.use_fallocate) { + if (config_.use_fallocate && data.size() > 0) { fallocate(fd, 0, 0, data.size()); } #endif size_t total_written = 0; - // Use Elio async write + // Use synchronous pwrite for regular files + // (epoll backend cannot handle regular files - they don't support polling) while (total_written < data.size()) { - auto result = co_await elio::io::async_write( - io_ctx_, fd, - data.data() + total_written, - data.size() - total_written, - static_cast(total_written)); - - if (!result.success() || result.result <= 0) { - co_await elio::io::async_close(io_ctx_, fd); - co_return Status::error(ErrorCode::DiskError, "Write failed"); + ssize_t n = ::pwrite(fd, data.data() + total_written, + data.size() - total_written, + static_cast(total_written)); + if (n < 0) { + int err = errno; + ::close(fd); + co_return Status::error(ErrorCode::DiskError, + std::string("Write failed: ") + strerror(err)); + } + if (n == 0) { + // Unexpected - pwrite should not return 0 unless count is 0 + break; } - total_written += result.result; + total_written += n; } if (sync) { - fsync(fd); // TODO: Use async fsync when available in Elio + fsync(fd); } - co_await elio::io::async_close(io_ctx_, fd); + ::close(fd); co_return Status::make_ok(); } diff --git a/tests/test_http_api.sh b/tests/test_http_api.sh new file mode 100755 index 0000000..fe4acff --- /dev/null +++ b/tests/test_http_api.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# Integration tests for ElCache HTTP API +# Requires running ElCache server +# +# Usage: ./test_http_api.sh [host:port] +# Default: localhost:8080 + +HOST="${1:-localhost:8080}" +BASE_URL="http://${HOST}" +PASS=0 +FAIL=0 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +pass() { + echo -e "${GREEN}PASS${NC}: $1" + PASS=$((PASS + 1)) +} + +fail() { + echo -e "${RED}FAIL${NC}: $1 - $2" + FAIL=$((FAIL + 1)) +} + +echo "ElCache HTTP API Tests" +echo "======================" +echo "Target: ${BASE_URL}" +echo "" + +# Test 1: Health check +echo -n "Test 1: Health check... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/health") +if [ "$STATUS" = "200" ]; then + pass "Health check" +else + fail "Health check" "Expected 200, got $STATUS" +fi + +# Test 2: PUT small value +echo -n "Test 2: PUT small value... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "${BASE_URL}/cache/test_small" -d "Hello World") +if [ "$STATUS" = "201" ]; then + pass "PUT small value" +else + fail "PUT small value" "Expected 201, got $STATUS" +fi + +# Test 3: GET small value +echo -n "Test 3: GET small value... " +BODY=$(curl -s "${BASE_URL}/cache/test_small") +if [ "$BODY" = "Hello World" ]; then + pass "GET small value" +else + fail "GET small value" "Expected 'Hello World', got '$BODY'" +fi + +# Test 4: PUT larger value (10KB) +echo -n "Test 4: PUT 10KB value... " +dd if=/dev/urandom bs=10240 count=1 2>/dev/null | base64 > /tmp/test_10k.dat +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "${BASE_URL}/cache/test_10k" --data-binary @/tmp/test_10k.dat) +if [ "$STATUS" = "201" ]; then + pass "PUT 10KB value" +else + fail "PUT 10KB value" "Expected 201, got $STATUS" +fi + +# Test 5: GET 10KB value and verify size +echo -n "Test 5: GET 10KB value... " +curl -s "${BASE_URL}/cache/test_10k" > /tmp/test_10k_get.dat +ORIG_SIZE=$(wc -c < /tmp/test_10k.dat) +GET_SIZE=$(wc -c < /tmp/test_10k_get.dat) +if [ "$ORIG_SIZE" = "$GET_SIZE" ]; then + pass "GET 10KB value (size: $GET_SIZE bytes)" +else + fail "GET 10KB value" "Size mismatch: original=$ORIG_SIZE, got=$GET_SIZE" +fi + +# Test 6: PUT 100KB value +echo -n "Test 6: PUT 100KB value... " +dd if=/dev/urandom bs=102400 count=1 2>/dev/null | base64 > /tmp/test_100k.dat +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "${BASE_URL}/cache/test_100k" --data-binary @/tmp/test_100k.dat) +if [ "$STATUS" = "201" ]; then + pass "PUT 100KB value" +else + fail "PUT 100KB value" "Expected 201, got $STATUS" +fi + +# Test 7: HEAD request for metadata +echo -n "Test 7: HEAD request... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -I "${BASE_URL}/cache/test_100k") +if [ "$STATUS" = "200" ]; then + pass "HEAD request" +else + fail "HEAD request" "Expected 200, got $STATUS" +fi + +# Test 8: Range request +echo -n "Test 8: Range request... " +RANGE_DATA=$(curl -s -H "Range: bytes=0-9" "${BASE_URL}/cache/test_small") +if [ "$RANGE_DATA" = "Hello Worl" ]; then + pass "Range request" +else + fail "Range request" "Expected 'Hello Worl', got '$RANGE_DATA'" +fi + +# Test 9: DELETE +echo -n "Test 9: DELETE... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${BASE_URL}/cache/test_small") +if [ "$STATUS" = "204" ]; then + pass "DELETE" +else + fail "DELETE" "Expected 204, got $STATUS" +fi + +# Test 10: GET after DELETE should 404 +echo -n "Test 10: GET after DELETE... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/cache/test_small") +if [ "$STATUS" = "404" ]; then + pass "GET after DELETE returns 404" +else + fail "GET after DELETE" "Expected 404, got $STATUS" +fi + +# Test 11: Stats endpoint +echo -n "Test 11: Stats endpoint... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/stats") +if [ "$STATUS" = "200" ]; then + pass "Stats endpoint" +else + fail "Stats endpoint" "Expected 200, got $STATUS" +fi + +# Test 12: PUT with TTL header +echo -n "Test 12: PUT with TTL... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "${BASE_URL}/cache/test_ttl" -H "X-ElCache-TTL: 3600" -d "TTL Test") +if [ "$STATUS" = "201" ]; then + pass "PUT with TTL" +else + fail "PUT with TTL" "Expected 201, got $STATUS" +fi + +# Test 13: Sparse write - Create +echo -n "Test 13: Sparse write - Create... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${BASE_URL}/sparse/test_sparse" -H "X-ElCache-Size: 1024") +if [ "$STATUS" = "201" ]; then + pass "Sparse create" +else + fail "Sparse create" "Expected 201, got $STATUS" +fi + +# Test 14: Sparse write - Write range +echo -n "Test 14: Sparse write - Write range... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "${BASE_URL}/sparse/test_sparse?offset=0" -d "$(head -c 512 /dev/zero | tr '\0' 'A')") +if [ "$STATUS" = "202" ]; then + pass "Sparse write range 1" +else + fail "Sparse write range 1" "Expected 202, got $STATUS" +fi + +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH "${BASE_URL}/sparse/test_sparse?offset=512" -d "$(head -c 512 /dev/zero | tr '\0' 'B')") +if [ "$STATUS" = "202" ]; then + pass "Sparse write range 2" +else + fail "Sparse write range 2" "Expected 202, got $STATUS" +fi + +# Test 15: Sparse write - Status check +echo -n "Test 15: Sparse write - Status... " +COMPLETION=$(curl -s "${BASE_URL}/sparse/test_sparse" | grep -o '"completion_percent": [0-9]*' | grep -o '[0-9]*') +if [ "$COMPLETION" = "100" ]; then + pass "Sparse status shows 100%" +else + fail "Sparse status" "Expected 100% completion, got $COMPLETION%" +fi + +# Test 16: Sparse write - Finalize +echo -n "Test 16: Sparse write - Finalize... " +STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${BASE_URL}/sparse/test_sparse/finalize") +if [ "$STATUS" = "201" ]; then + pass "Sparse finalize" +else + fail "Sparse finalize" "Expected 201, got $STATUS" +fi + +# Test 17: Read finalized sparse data +echo -n "Test 17: Read finalized sparse... " +DATA=$(curl -s -H "Range: bytes=0-0" "${BASE_URL}/cache/test_sparse") +if [ "$DATA" = "A" ]; then + pass "Read finalized sparse (first byte)" +else + fail "Read finalized sparse" "Expected 'A', got '$DATA'" +fi + +DATA=$(curl -s -H "Range: bytes=512-512" "${BASE_URL}/cache/test_sparse") +if [ "$DATA" = "B" ]; then + pass "Read finalized sparse (byte 512)" +else + fail "Read finalized sparse" "Expected 'B', got '$DATA'" +fi + +# Cleanup +rm -f /tmp/test_10k.dat /tmp/test_10k_get.dat /tmp/test_100k.dat + +echo "" +echo "======================" +echo "Results: ${PASS} passed, ${FAIL} failed" + +if [ $FAIL -gt 0 ]; then + exit 1 +fi +exit 0