diff --git a/.gitignore b/.gitignore index 77f4c36..3b986fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .cmake +build/ diff --git a/AGENTS.md b/AGENTS.md index abc3683..9679c52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,3 +29,39 @@ - Commits: concise, imperative summaries (`Fix parsing server url`), optionally append issue refs like `(#102)`. - PRs: describe intent and behavior changes, link issues, call out API impacts, include test commands/output, and note any server or env requirements. - Ensure builds/tests pass before review; include screenshots only when modifying docs or examples. + +## Development Workflow +Before pushing changes, always complete these steps in order: + +1. **Run cpplint** - Ensure code follows style guidelines: + ```bash + pip install "cpplint<2" + find src/ \( -name "*.cc" -o -name "*.h" \) -print0 | xargs -0 cpplint + find tests/ \( -name "*.cc" -o -name "*.h" \) -print0 | xargs cpplint + ``` + +2. **Build the code** - Verify compilation succeeds: + ```bash + cmake -S . -B build -DREDUCT_CPP_USE_FETCHCONTENT=ON -DREDUCT_CPP_ENABLE_TESTS=ON + cmake --build build + ``` + +3. **Test against both server versions** - Ensure backward compatibility: + ```bash + # Test with development version (main - new features) + docker run -d -p 8383:8383 --name reductstore-test reduct/store:main + ./build/bin/reduct-tests + docker stop reductstore-test && docker rm reductstore-test + + # Test with stable version (latest) + docker run -d -p 8383:8383 --name reductstore-test reduct/store:latest + ./build/bin/reduct-tests "~[1_18]" # Exclude v1.18+ specific tests + docker stop reductstore-test && docker rm reductstore-test + ``` + +4. **Reference GitHub Actions** - See `.github/workflows/ci.yml` for the complete CI pipeline that runs cpplint, builds on multiple platforms, and tests against both `reduct/store:main` and `reduct/store:latest`. + +## Documentation Guidelines +- **Do not update README.md or create examples for new features unless explicitly requested in the issue description.** +- Keep documentation changes minimal and focused on the specific requirements outlined in the issue. +- AGENTS.md is the appropriate place for development guidelines and internal documentation, not feature documentation. diff --git a/src/reduct/bucket.cc b/src/reduct/bucket.cc index d9348a3..18fa507 100644 --- a/src/reduct/bucket.cc +++ b/src/reduct/bucket.cc @@ -23,6 +23,7 @@ namespace reduct { using internal::IHttpClient; +using internal::ParseStatus; using internal::QueryOptionsToJsonString; class Bucket : public IBucket { @@ -85,6 +86,7 @@ class Bucket : public IBucket { .oldest_record = Time() + std::chrono::microseconds(info.at("oldest_record")), .latest_record = Time() + std::chrono::microseconds(info.at("latest_record")), .is_provisioned = info.value("is_provisioned", false), + .status = ParseStatus(info), }, Error::kOk, }; @@ -111,6 +113,7 @@ class Bucket : public IBucket { .size = entry.at("size"), .oldest_record = Time() + std::chrono::microseconds(entry.at("oldest_record")), .latest_record = Time() + std::chrono::microseconds(entry.at("latest_record")), + .status = ParseStatus(entry), }; } @@ -649,17 +652,20 @@ std::ostream& operator<<(std::ostream& os, const reduct::IBucket::Settings& sett std::ostream& operator<<(std::ostream& os, const IBucket::BucketInfo& info) { os << fmt::format( "", + "oldest_record={}, latest_record={}, is_provisioned={}, status={}>", info.name, info.entry_count, info.size, info.oldest_record.time_since_epoch().count() / 1000, - info.latest_record.time_since_epoch().count() / 1000, info.is_provisioned ? "true" : "false"); + info.latest_record.time_since_epoch().count() / 1000, info.is_provisioned ? "true" : "false", + info.status == IBucket::Status::kReady ? "READY" : "DELETING"); return os; } std::ostream& operator<<(std::ostream& os, const IBucket::EntryInfo& info) { - os << fmt::format("", - info.name, info.record_count, info.block_count, info.size, - info.oldest_record.time_since_epoch().count() / 1000, - info.latest_record.time_since_epoch().count() / 1000); + os << fmt::format( + "", + info.name, info.record_count, info.block_count, info.size, + info.oldest_record.time_since_epoch().count() / 1000, + info.latest_record.time_since_epoch().count() / 1000, + info.status == IBucket::Status::kReady ? "READY" : "DELETING"); return os; } } // namespace reduct diff --git a/src/reduct/bucket.h b/src/reduct/bucket.h index 71b2ca1..489f7e4 100644 --- a/src/reduct/bucket.h +++ b/src/reduct/bucket.h @@ -31,6 +31,11 @@ class IBucket { enum class QuotaType { kNone, kFifo, kHard }; + /** + * Status of bucket or entry + */ + enum class Status { kReady, kDeleting }; + /** * Bucket Settings */ @@ -56,6 +61,7 @@ class IBucket { Time oldest_record; // timestamp of the oldest record in the bucket Time latest_record; // timestamp of the latest record in the bucket bool is_provisioned; // is bucket provisioned, you can't remove it or change settings + Status status; // status of bucket (READY or DELETING) auto operator<=>(const BucketInfo&) const noexcept = default; friend std::ostream& operator<<(std::ostream& os, const BucketInfo& info); @@ -71,6 +77,7 @@ class IBucket { size_t size; // size of stored data in the bucket in bytes Time oldest_record; // timestamp of the oldest record in the entry Time latest_record; // timestamp of the latest record in the entry + Status status; // status of entry (READY or DELETING) auto operator<=>(const EntryInfo&) const noexcept = default; friend std::ostream& operator<<(std::ostream& os, const EntryInfo& info); diff --git a/src/reduct/client.cc b/src/reduct/client.cc index 82411a1..1f7be85 100644 --- a/src/reduct/client.cc +++ b/src/reduct/client.cc @@ -14,6 +14,8 @@ namespace reduct { +using internal::ParseStatus; + /** * Hidden implement of IClient. */ @@ -89,6 +91,8 @@ class Client : public IClient { .size = bucket.at("size"), .oldest_record = Time() + std::chrono::microseconds(bucket.at("oldest_record")), .latest_record = Time() + std::chrono::microseconds(bucket.at("latest_record")), + .is_provisioned = bucket.value("is_provisioned", false), + .status = ParseStatus(bucket), }); } } catch (const std::exception& e) { diff --git a/src/reduct/internal/serialisation.cc b/src/reduct/internal/serialisation.cc index 87de738..355995b 100644 --- a/src/reduct/internal/serialisation.cc +++ b/src/reduct/internal/serialisation.cc @@ -329,4 +329,14 @@ Result QueryLinkOptionsToJsonString(std::string_view bucket, std return {json_data, Error::kOk}; } +IBucket::Status ParseStatus(const nlohmann::json& json) { + if (json.contains("status")) { + const auto status = json.at("status").get(); + if (status == "DELETING") { + return IBucket::Status::kDeleting; + } + } + return IBucket::Status::kReady; +} + } // namespace reduct::internal diff --git a/src/reduct/internal/serialisation.h b/src/reduct/internal/serialisation.h index e94e0a5..2affa3e 100644 --- a/src/reduct/internal/serialisation.h +++ b/src/reduct/internal/serialisation.h @@ -67,6 +67,13 @@ Result QueryOptionsToJsonString(std::string_view type, s Result QueryLinkOptionsToJsonString(std::string_view bucket, std::string_view entry_name, const IBucket::QueryLinkOptions& options); +/** + * @brief Parse status from JSON + * @param json JSON object to parse status from + * @return Status enum value (defaults to kReady if not present) + */ +IBucket::Status ParseStatus(const nlohmann::json& json); + }; // namespace reduct::internal #endif // REDUCTCPP_SERIALISATION_H diff --git a/tests/reduct/bucket_api_test.cc b/tests/reduct/bucket_api_test.cc index bc93145..c439662 100644 --- a/tests/reduct/bucket_api_test.cc +++ b/tests/reduct/bucket_api_test.cc @@ -143,6 +143,7 @@ TEST_CASE("reduct::IBucket should get bucket stats", "[bucket_api]") { .oldest_record = t, .latest_record = t + std::chrono::seconds(1), .is_provisioned = false, + .status = IBucket::Status::kReady, }); } @@ -161,6 +162,7 @@ TEST_CASE("reduct::IBucket should get list of entries", "[bucket_api]") { .size = 78, .oldest_record = t + s(1), .latest_record = t + s(2), + .status = IBucket::Status::kReady, }); REQUIRE(entries[1] == IBucket::EntryInfo{ @@ -170,6 +172,7 @@ TEST_CASE("reduct::IBucket should get list of entries", "[bucket_api]") { .size = 78, .oldest_record = t + s(3), .latest_record = t + s(4), + .status = IBucket::Status::kReady, }); } @@ -191,8 +194,9 @@ TEST_CASE("reduct::IBucket should remove entry", "[bucket_api][1_6]") { Error::kOk); REQUIRE(bucket->RemoveEntry("entry-1") == Error::kOk); - REQUIRE(bucket->RemoveEntry("entry-1") == - Error{.code = 404, .message = fmt::format("Entry 'entry-1' not found in bucket '{}'", kBucketName)}); + // After removal, the entry may be in DELETING state (409) or not found (404) + auto err = bucket->RemoveEntry("entry-1"); + REQUIRE((err.code == 404 || err.code == 409)); } TEST_CASE("reduct::IBucket should rename bucket", "[bucket_api][1_12]") { diff --git a/tests/reduct/server_api_test.cc b/tests/reduct/server_api_test.cc index 94694e8..f3bfedf 100644 --- a/tests/reduct/server_api_test.cc +++ b/tests/reduct/server_api_test.cc @@ -58,12 +58,14 @@ TEST_CASE("reduct::Client should list buckets", "[server_api]") { REQUIRE(list[0].entry_count == 2); REQUIRE(list[0].oldest_record.time_since_epoch() == s(1)); REQUIRE(list[0].latest_record.time_since_epoch() == s(4)); + REQUIRE(list[0].status == IBucket::Status::kReady); REQUIRE(list[1].name == "test_bucket_2"); REQUIRE(list[1].size == 78); REQUIRE(list[1].entry_count == 1); REQUIRE(list[1].oldest_record.time_since_epoch() == s(5)); REQUIRE(list[1].latest_record.time_since_epoch() == s(6)); + REQUIRE(list[1].status == IBucket::Status::kReady); } TEST_CASE("reduct::Client should return error", "[server_api]") {