Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ inputs:
description: "Enable audit in ReductStore during tests"
required: false
default: "true"
system-events-enabled:
description: "Enable $system event storage in ReductStore during tests"
required: false
default: "false"
runs:
using: "composite"
steps:
Expand All @@ -25,6 +29,7 @@ runs:
run: docker run -p 8383:8383 -v ${PWD}:/workdir
--env RS_API_TOKEN=${{inputs.api-token}}
--env RS_AUDIT_ENABLED=${{inputs.audit-enabled}}
--env RS_SYSTEM_EVENTS_ENABLED=${{inputs.system-events-enabled}}
--env RS_EXT_PATH=/tmp
--name reduct-store
-d reduct/store:${{inputs.reductstore-version}}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add lifecycle `compress` action support and rename lifecycle age field from `max_age` to `older_than` to match the ReductStore API, [PR-130](https://github.com/reductstore/reduct-cpp/pull/130)
- Add lifecycle policy API support, [PR-128](https://github.com/reductstore/reduct-cpp/pull/128)

## 1.19.1 - 2026-04-21
Expand Down
4 changes: 2 additions & 2 deletions src/reduct/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ class IClient {
/**
* Lifecycle information
*/
enum class LifecycleType { kDelete };
enum class LifecycleType { kDelete, kCompress };

enum class LifecycleMode { kEnabled, kDisabled, kDryRun };

Expand All @@ -308,7 +308,7 @@ class IClient {
LifecycleType type = LifecycleType::kDelete; // Lifecycle action type
std::string bucket; // Bucket to apply lifecycle policy
std::vector<std::string> entries; // Entries to process. If empty, all matching entries are used.
std::string max_age; // Maximum record age
std::string older_than; // Process records older than this duration
std::optional<std::string> interval; // Interval between lifecycle runs
std::optional<std::string> when; // Lifecycle condition
LifecycleMode mode = LifecycleMode::kEnabled; // Lifecycle mode
Expand Down
9 changes: 7 additions & 2 deletions src/reduct/internal/serialisation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ IClient::LifecycleType ParseLifecycleType(const nlohmann::json& type_json) {
if (type == "delete") {
return IClient::LifecycleType::kDelete;
}
if (type == "compress") {
return IClient::LifecycleType::kCompress;
}

throw std::invalid_argument("Invalid lifecycle type: " + type);
}
Expand Down Expand Up @@ -97,6 +100,8 @@ std::string LifecycleTypeToString(IClient::LifecycleType type) {
switch (type) {
case IClient::LifecycleType::kDelete:
return "delete";
case IClient::LifecycleType::kCompress:
return "compress";
}

throw std::invalid_argument("Invalid lifecycle type");
Expand Down Expand Up @@ -327,7 +332,7 @@ Result<nlohmann::json> LifecycleSettingsToJsonString(IClient::LifecycleSettings
json_data["type"] = LifecycleTypeToString(settings.type);
json_data["bucket"] = settings.bucket;
json_data["entries"] = settings.entries;
json_data["max_age"] = settings.max_age;
json_data["older_than"] = settings.older_than;
if (settings.interval) {
json_data["interval"] = *settings.interval;
}
Expand Down Expand Up @@ -363,7 +368,7 @@ Result<IClient::FullLifecycleInfo> ParseFullLifecycleInfo(const nlohmann::json&
.type = settings.contains("type") ? ParseLifecycleType(settings.at("type")) : IClient::LifecycleType::kDelete,
.bucket = settings.at("bucket"),
.entries = settings.at("entries"),
.max_age = settings.at("max_age"),
.older_than = settings.at("older_than"),
.mode = settings.contains("mode") ? ParseLifecycleMode(settings.at("mode"))
: IClient::LifecycleMode::kEnabled,
};
Expand Down
31 changes: 20 additions & 11 deletions tests/fixture.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,28 @@ struct Fixture {
}

client = IClient::Build(url, opts);
auto bucket_list = client->GetBucketList();
if (bucket_list.error) {
throw std::runtime_error(fmt::format("Failed to get bucket list: {}", bucket_list.error.ToString()));
}
for (auto&& info : client->GetBucketList().result) {
if (!info.name.starts_with("test_bucket")) {
continue;
auto cleanup_test_buckets = [&]() {
auto bucket_list = client->GetBucketList();
if (bucket_list.error) {
throw std::runtime_error(fmt::format("Failed to get bucket list: {}", bucket_list.error.ToString()));
}
std::unique_ptr<IBucket> bucket = client->GetBucket(info.name);
[[maybe_unused]] auto ret = bucket->Remove();
}

std::this_thread::sleep_for(std::chrono::milliseconds(100)); // wait for bucket removal
bool removed = false;
for (auto&& info : bucket_list.result) {
if (!info.name.starts_with("test_bucket")) {
continue;
}
std::unique_ptr<IBucket> bucket = client->GetBucket(info.name);
[[maybe_unused]] auto ret = bucket->Remove();
removed = true;
}

return removed;
};

while (cleanup_test_buckets()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

for (auto&& t : client->GetTokenList().result) {
if (!t.name.starts_with("test_token")) {
Expand Down
5 changes: 3 additions & 2 deletions tests/reduct/lifecycle_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ IClient::LifecycleSettings DefaultSettings() {
.type = IClient::LifecycleType::kDelete,
.bucket = "test_bucket_1",
.entries = {"entry-1"},
.max_age = "1h",
.older_than = "1h",
.interval = "10m",
.mode = IClient::LifecycleMode::kEnabled,
};
Expand All @@ -35,6 +35,7 @@ TEST_CASE("reduct::Client should get list of lifecycles", "[lifecycle_api][1_20]
TEST_CASE("reduct::Client should create a lifecycle", "[lifecycle_api][1_20]") {
Fixture ctx;
auto settings = DefaultSettings();
settings.type = IClient::LifecycleType::kCompress;

auto err = ctx.client->CreateLifecycle("test_lifecycle", settings);
REQUIRE(err == Error::kOk);
Expand Down Expand Up @@ -63,7 +64,7 @@ TEST_CASE("reduct::Client should update a lifecycle", "[lifecycle_api][1_20]") {
REQUIRE(err == Error::kOk);

settings.entries = {"entry-2"};
settings.max_age = "2h";
settings.older_than = "2h";
settings.interval = "20m";
err = ctx.client->UpdateLifecycle("test_lifecycle", settings);
REQUIRE(err == Error::kOk);
Expand Down
39 changes: 24 additions & 15 deletions tests/reduct/server_api_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ TEST_CASE("reduct::Client should get info", "[server_api]") {
REQUIRE(err == Error::kOk);
REQUIRE(info.version >= "1.10.0");

REQUIRE(info.bucket_count == 2);
REQUIRE(info.usage == 234);
REQUIRE(info.bucket_count >= 2);
REQUIRE(info.usage >= 234);
REQUIRE(info.uptime.count() >= 1);
REQUIRE(info.oldest_record.time_since_epoch() == s(1));
REQUIRE(info.latest_record.time_since_epoch() == s(6));
Expand All @@ -38,19 +38,28 @@ TEST_CASE("reduct::Client should list buckets", "[server_api]") {
auto [list, err] = ctx.client->GetBucketList();

REQUIRE_FALSE(list.empty());
REQUIRE(list[0].name == "test_bucket_1");
REQUIRE(list[0].size == 156);
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);

std::vector<IBucket::BucketInfo> test_buckets;
for (const auto& bucket : list) {
if (bucket.name.starts_with("test_bucket_")) {
test_buckets.push_back(bucket);
}
}

REQUIRE(test_buckets.size() == 2);
REQUIRE(test_buckets[0].name == "test_bucket_1");
REQUIRE(test_buckets[0].size == 156);
REQUIRE(test_buckets[0].entry_count == 2);
REQUIRE(test_buckets[0].oldest_record.time_since_epoch() == s(1));
REQUIRE(test_buckets[0].latest_record.time_since_epoch() == s(4));
REQUIRE(test_buckets[0].status == IBucket::Status::kReady);

REQUIRE(test_buckets[1].name == "test_bucket_2");
REQUIRE(test_buckets[1].size == 78);
REQUIRE(test_buckets[1].entry_count == 1);
REQUIRE(test_buckets[1].oldest_record.time_since_epoch() == s(5));
REQUIRE(test_buckets[1].latest_record.time_since_epoch() == s(6));
REQUIRE(test_buckets[1].status == IBucket::Status::kReady);
}

TEST_CASE("reduct::Client should return error", "[server_api]") {
Expand Down
Loading