Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.cmake
build/
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 12 additions & 6 deletions src/reduct/bucket.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
namespace reduct {

using internal::IHttpClient;
using internal::ParseStatus;
using internal::QueryOptionsToJsonString;

class Bucket : public IBucket {
Expand Down Expand Up @@ -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,
};
Expand All @@ -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),
};
}

Expand Down Expand Up @@ -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(
"<BucketInfo name={}, entry_count={}, size={}, "
"oldest_record={}, latest_record={}, is_provisioned={}>",
"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("<EntryInfo name={}, record_count={}, block_count={}, size={}, oldest_record={}, latest_record={}>",
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(
"<EntryInfo name={}, record_count={}, block_count={}, size={}, oldest_record={}, latest_record={}, status={}>",
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
7 changes: 7 additions & 0 deletions src/reduct/bucket.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class IBucket {

enum class QuotaType { kNone, kFifo, kHard };

/**
* Status of bucket or entry
*/
enum class Status { kReady, kDeleting };

/**
* Bucket Settings
*/
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/reduct/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

namespace reduct {

using internal::ParseStatus;

/**
* Hidden implement of IClient.
*/
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions src/reduct/internal/serialisation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,14 @@ Result<nlohmann::json> 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<std::string>();
if (status == "DELETING") {
return IBucket::Status::kDeleting;
}
}
return IBucket::Status::kReady;
}

} // namespace reduct::internal
7 changes: 7 additions & 0 deletions src/reduct/internal/serialisation.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ Result<nlohmann::ordered_json> QueryOptionsToJsonString(std::string_view type, s
Result<nlohmann::json> 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
8 changes: 6 additions & 2 deletions tests/reduct/bucket_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand All @@ -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{
Expand All @@ -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,
});
}

Expand All @@ -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]") {
Expand Down
2 changes: 2 additions & 0 deletions tests/reduct/server_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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]") {
Expand Down
Loading