diff --git a/src/lotman.cpp b/src/lotman.cpp
index 4d5e358..9a09d25 100644
--- a/src/lotman.cpp
+++ b/src/lotman.cpp
@@ -1718,3 +1718,40 @@ int lotman_get_context_int(const char *key, int *output, char **err_msg) {
return -1;
}
}
+
+int lotman_get_max_mpas_for_period(int64_t start_ms, int64_t end_ms, bool include_deletion, char **output,
+ char **err_msg) {
+ try {
+ // Call internal function
+ auto [result, error] = lotman::get_max_mpas_for_period_internal(start_ms, end_ms, include_deletion);
+
+ // Check for errors from internal function
+ if (!error.empty()) {
+ if (err_msg) {
+ *err_msg = strdup(error.c_str());
+ }
+ return -1;
+ }
+
+ // Build output JSON
+ json output_obj;
+ output_obj["start_ms"] = start_ms;
+ output_obj["end_ms"] = end_ms;
+ output_obj["include_deletion"] = include_deletion;
+ output_obj["max_dedicated_GB"] = result.max_dedicated_GB;
+ output_obj["max_opportunistic_GB"] = result.max_opportunistic_GB;
+ output_obj["max_combined_GB"] = result.max_combined_GB;
+ output_obj["max_num_objects"] = result.max_num_objects;
+
+ // Convert to string and allocate output
+ std::string output_str = output_obj.dump();
+ *output = strdup(output_str.c_str());
+
+ return 0;
+ } catch (std::exception &exc) {
+ if (err_msg) {
+ *err_msg = strdup(exc.what());
+ }
+ return -1;
+ }
+}
diff --git a/src/lotman.h b/src/lotman.h
index dac2023..c92e2af 100644
--- a/src/lotman.h
+++ b/src/lotman.h
@@ -936,6 +936,72 @@ int lotman_get_context_int(const char *key, int *output, char **err_msg);
A reference to a char array that can store any error messages.
*/
+int lotman_get_max_mpas_for_period(int64_t start_ms, int64_t end_ms, bool include_deletion, char **output,
+ char **err_msg);
+/**
+ DESCRIPTION: A function for determining the maximum summed Management Policy Attributes (MPAs)
+ across all overlapping lots during a specified time period. This function uses a sweep line
+ algorithm to efficiently calculate the peak resource allocation at any point during the period.
+ This is useful for capacity planning and scheduling systems that need to determine available
+ space for new lot allocations, e.g. "Can I create a lot with 50GB dedicated storage from time
+ A to time B without overcommitting resources?"
+
+ RETURNS: Returns 0 on success. Any other values indicate an error.
+
+ INPUTS:
+ start_ms:
+ A Unix timestamp in milliseconds indicating the start of the query period (inclusive).
+
+ end_ms:
+ A Unix timestamp in milliseconds indicating the end of the query period (inclusive).
+ Must be greater than start_ms or the function will return an error.
+
+ include_deletion:
+ A boolean indicating which lot endpoint to consider:
+ - When false: lots are considered active until their expiration_time
+ - When true: lots are considered active until their deletion_time
+ For most capacity planning scenarios, false is recommended since expired lots may still
+ consume resources even if they become opportunistic.
+
+ output:
+ A reference to a char * that will be allocated and populated with a JSON string containing
+ the results. The caller is responsible for freeing this memory.
+
+ err_msg:
+ A reference to a char array that can store any error messages.
+
+ Output JSON Specification:
+ The output JSON contains both the query parameters (for logging/debugging) and the results:
+{
+ "start_ms": ,
+ "end_ms": ,
+ "include_deletion": ,
+ "max_dedicated_GB": ,
+ "max_opportunistic_GB": ,
+ "max_combined_GB": ,
+ "max_num_objects":
+}
+
+ Notes:
+ - max_dedicated_GB represents the maximum cumulative storage Lotman has dedicated to lots during the
+ specified period
+ - max_combined_GB sums over both opportunistic and dedicated storage, representing the total maximum storage
+ Lotman has allocated to lots during the specified period
+ - max_opportunistic_GB and max_combined_GB may be produced by different sets of overlapping lots
+ - If no lots overlap the specified period, all maximum values will be 0.0
+
+ Example:
+ If the query period contains three overlapping lots:
+ - Lot A: 10GB dedicated, 5GB opportunistic
+ - Lot B: 8GB dedicated, 0GB opportunistic
+ - Lot C: 7GB dedicated, 3GB opportunistic
+
+ When all three overlap in time with the overall query interval:
+ - max_dedicated_GB = 25.0 (10 + 8 + 7)
+ - max_opportunistic_GB = 8.0 (5 + 0 + 3)
+ - max_combined_GB = 33.0 (25 + 8, the sum of total dedicated plus total opportunistic)
+*/
+
// int lotman_get_matching_lots(const char *criteria_JSON, char ***output, char **err_msg);
// int lotman_check_db_health(char **err_msg); // Should eventually check that data structure conforms to expectations.
// If there's a cycle or a non-self-parent root, something is wrong
diff --git a/src/lotman_internal.cpp b/src/lotman_internal.cpp
index 6b3a7a1..90ea0db 100644
--- a/src/lotman_internal.cpp
+++ b/src/lotman_internal.cpp
@@ -2182,6 +2182,153 @@ bool lotman::Checks::will_be_orphaned(const std::string <BR, const std::string
return false;
}
+/**
+ * Implementation of sweep line algorithm for finding maximum MPAs during a time period.
+ *
+ * This implements the classic sweep line algorithm for interval scheduling problems.
+ * See: https://www.geeksforgeeks.org/maximum-number-of-overlapping-intervals/
+ *
+ * The algorithm works by:
+ * 1. Creating "events" for each lot's start (creation) and end (expiration/deletion)
+ * 2. Sorting all events by time
+ * 3. Sweeping through events chronologically, tracking current resource usage with deltas
+ * that correspond to each event's attributes
+ * 4. Recording the maximum usage observed at any point
+ *
+ * Key semantic: Lot lifetimes are INCLUSIVE intervals [creation_time, end_time].
+ * A lot is active at both its start and end timestamps. Therefore, we schedule
+ * removal events at end_time + 1 (the first moment the lot is no longer active).
+ */
+
+std::pair lotman::get_max_mpas_for_period_internal(int64_t start_ms, int64_t end_ms,
+ bool include_deletion) {
+ // Validate input
+ if (start_ms >= end_ms) {
+ return {{0.0, 0.0, 0.0, 0}, "Error: start_ms must be less than end_ms"};
+ }
+
+ auto &storage = lotman::db::StorageManager::get_storage();
+
+ // Determine which time field to use for lot end time
+ using MPA = lotman::db::ManagementPolicyAttributes;
+ using Parent = lotman::db::Parent;
+ using namespace sqlite_orm;
+
+ // Query lots that overlap with the period, filtering to only ROOT lots.
+ //
+ // IMPORTANT: We only count root lots (self-parent lots) to avoid double-counting in hierarchies.
+ // A root lot is one where the lot has only itself as a parent in the parents table.
+ // Child lots consume quota from their parents, so counting both would be incorrect.
+ //
+ // For example, if parent_lot has 5GB and child_lot (child of parent_lot) has 3GB,
+ // the maximum capacity usage should be 5GB (from the parent), not 8GB (parent + child).
+ //
+ // Overlap condition for inclusive intervals: creation_time <= end_ms AND end_time >= start_ms
+ // This correctly handles all overlap cases including point-in-time overlaps at boundaries.
+ //
+ // Root lot condition: EXISTS exactly one parent record WHERE parent = lot_name
+ // We use a SQL subquery to identify root lots directly in the database for optimal performance.
+ std::string time_field = include_deletion ? "deletion_time" : "expiration_time";
+ std::string query = "SELECT mpa.lot_name, mpa.dedicated_GB, mpa.opportunistic_GB, mpa.max_num_objects, "
+ " mpa.creation_time, mpa." +
+ time_field +
+ " "
+ "FROM management_policy_attributes mpa "
+ "WHERE mpa.creation_time <= ? AND mpa." +
+ time_field +
+ " >= ? "
+ " AND mpa.lot_name IN ( "
+ " SELECT p.lot_name "
+ " FROM parents p "
+ " WHERE p.lot_name = p.parent "
+ " GROUP BY p.lot_name "
+ " HAVING COUNT(*) = 1 "
+ " )";
+
+ std::map> query_int_map{{end_ms, {1}}, {start_ms, {2}}};
+ auto rp = lotman::db::SQL_get_matches_multi_col(query, 6, std::map>(), query_int_map);
+
+ if (!rp.second.empty()) {
+ return {{0.0, 0.0, 0.0, 0}, "Database query failed: " + rp.second};
+ }
+
+ auto &lots = rp.first;
+
+ // If no root lots overlap, return zeros with no error
+ if (lots.empty()) {
+ return {{0.0, 0.0, 0.0, 0}, ""};
+ }
+
+ // Event structure for sweep line algorithm
+ struct Event {
+ int64_t time;
+ double ded_delta; // Change in dedicated storage
+ double opp_delta; // Change in opportunistic storage
+ int64_t obj_delta; // Change in object count
+ bool is_start; // true for creation event, false for expiration/deletion event
+ };
+
+ std::vector events;
+ events.reserve(lots.size() * 2); // Each lot creates at most 2 events
+
+ // Build event list from query results
+ // Each row contains: [lot_name, dedicated_GB, opportunistic_GB, max_num_objects, creation_time, end_time]
+ for (const auto &lot_row : lots) {
+ // Parse query results from string vector (columns 0-5)
+ // lot_row[0] = lot_name (string, not used in sweep line)
+ double dedicated = std::stod(lot_row[1]); // dedicated_GB
+ double opportunistic = std::stod(lot_row[2]); // opportunistic_GB
+ int64_t objects = std::stoll(lot_row[3]); // max_num_objects
+ int64_t creation = std::stoll(lot_row[4]); // creation_time
+ int64_t end_time = std::stoll(lot_row[5]); // expiration_time or deletion_time
+
+ // Clamp lot start to query range (if lot starts before start_ms, treat as starting at start_ms)
+ int64_t effective_start = std::max(start_ms, creation);
+
+ // Add creation/start event at the lot's effective start time
+ events.push_back({effective_start, dedicated, opportunistic, objects, true});
+
+ // Add expiration/deletion event AFTER the lot ends (since lot is active through end_time inclusive)
+ // Only add if the lot ends before the query period ends
+ if (end_time < end_ms) {
+ // Schedule removal at end_time + 1 (first moment lot is no longer active)
+ events.push_back({end_time + 1, -dedicated, -opportunistic, -objects, false});
+ }
+ // If end_time >= end_ms, the lot extends beyond our query range, so no removal event needed
+ }
+
+ // Sort events chronologically, with start events before end events at the same timestamp
+ std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
+ if (a.time != b.time) {
+ return a.time < b.time;
+ }
+ // At same time, process starts before ends (true sorts before false)
+ // This ensures we correctly handle simultaneous creation/expiration events
+ return a.is_start > b.is_start;
+ });
+
+ // Sweep through events chronologically, tracking current and maximum resource usage
+ double current_ded = 0.0, current_opp = 0.0, current_combined = 0.0;
+ double max_ded = 0.0, max_opp = 0.0, max_combined = 0.0;
+ int64_t current_obj = 0, max_obj = 0;
+
+ for (const auto &event : events) {
+ // Update current resource usage based on event deltas
+ current_ded += event.ded_delta;
+ current_opp += event.opp_delta;
+ current_obj += event.obj_delta;
+ current_combined = current_ded + current_opp;
+
+ // Track the maximum values observed at any point
+ max_ded = std::max(max_ded, current_ded);
+ max_opp = std::max(max_opp, current_opp);
+ max_combined = std::max(max_combined, current_combined);
+ max_obj = std::max(max_obj, current_obj);
+ }
+
+ return {{max_ded, max_opp, max_combined, max_obj}, ""};
+}
+
void lotman::Context::set_caller(const std::string caller) {
m_caller = std::make_shared(caller);
}
diff --git a/src/lotman_internal.h b/src/lotman_internal.h
index 88287e8..ca8e4c3 100644
--- a/src/lotman_internal.h
+++ b/src/lotman_internal.h
@@ -424,4 +424,42 @@ class Checks {
// a parent/child, which should update data for the child
static bool will_be_orphaned(const std::string <BR, const std::string &child);
};
+
+/**
+ * Result structure for maximum MPA queries.
+ */
+struct MaxMPAResult {
+ double max_dedicated_GB;
+ double max_opportunistic_GB;
+ double max_combined_GB;
+ int64_t max_num_objects;
+};
+
+/**
+ * Calculate maximum Management Policy Attributes (MPAs) during a time period using sweep line algorithm.
+ *
+ * This function implements a sweep line algorithm to efficiently find the maximum resource usage
+ * across all overlapping lots during a specified time period. The algorithm is based on the
+ * classic interval scheduling problem solution described at:
+ * https://www.geeksforgeeks.org/maximum-number-of-overlapping-intervals/
+ *
+ * Time Complexity: O(n log n) where n is the number of lots overlapping the query period
+ * Space Complexity: O(n) for the event list
+ *
+ * IMPORTANT: Lot lifetimes are treated as INCLUSIVE intervals [creation_time, end_time].
+ * A lot is considered active at both its creation_time and its end_time (expiration or deletion).
+ * This means a lot with creation_time=100 and expiration_time=200 is active during the entire
+ * range [100, 200], including both endpoints.
+ *
+ * @param start_ms Start of the query period in milliseconds since Unix epoch (inclusive)
+ * @param end_ms End of the query period in milliseconds since Unix epoch (inclusive)
+ * @param include_deletion If true, use deletion_time as lot end; if false, use expiration_time
+ * @return A pair containing:
+ * - first: MaxMPAResult struct with all maximum values
+ * - second: Error message string (empty on success, descriptive message on error)
+ * On error, all numeric values in the result struct are set to 0.
+ */
+std::pair get_max_mpas_for_period_internal(int64_t start_ms, int64_t end_ms,
+ bool include_deletion);
+
} // namespace lotman
diff --git a/test/main.cpp b/test/main.cpp
index 8cb7211..9ef279a 100644
--- a/test/main.cpp
+++ b/test/main.cpp
@@ -82,19 +82,20 @@ class LotManTest : public ::testing::Test {
}
// Helper to add default lot - MUST be created before any other lots
+ // Times are chosen to not overlap with test cases (which use times 50-600)
void addDefaultLot() {
const char *default_lot = R"({
"lot_name": "default",
- "owner": "owner2",
+ "owner": "owner1",
"parents": ["default"],
"paths": [{"path": "/default/paths", "recursive": true}],
"management_policy_attrs": {
"dedicated_GB": 5,
"opportunistic_GB": 2.5,
"max_num_objects": 100,
- "creation_time": 123,
- "expiration_time": 234,
- "deletion_time": 345
+ "creation_time": 1000,
+ "expiration_time": 2000,
+ "deletion_time": 3000
}
})";
addLot(default_lot);
@@ -1965,6 +1966,510 @@ TEST_F(LotManTest, PathTrailingSlashNormalizationTest) {
ASSERT_EQ(paths_json.size(), 1);
EXPECT_EQ(paths_json[0]["path"], "/no/slash/here/");
}
+
+// Test: get_max_mpas_for_period - Single lot within period
+TEST_F(LotManTest, MaxMPAsForPeriod_SingleLot) {
+ addDefaultLot();
+
+ const char *lot = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+ addLot(lot);
+
+ // Query period that contains the lot
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(50, 250, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ EXPECT_EQ(result["start_ms"], 50);
+ EXPECT_EQ(result["end_ms"], 250);
+ EXPECT_EQ(result["include_deletion"], false);
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 10.0);
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 5.0);
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 15.0);
+ EXPECT_EQ(result["max_num_objects"], 1000);
+}
+
+// Test: Multiple overlapping lots
+TEST_F(LotManTest, MaxMPAsForPeriod_MultipleOverlapping) {
+ addDefaultLot();
+
+ // Three lots that overlap during [150, 220]
+ // Note that default lot does NOT overlap in time with this period.
+ const char *testlot1 = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 250,
+ "deletion_time": 300
+ }
+ })";
+
+ const char *testlot2 = R"({
+ "lot_name": "testlot2",
+ "owner": "owner1",
+ "parents": ["testlot2"],
+ "management_policy_attrs": {
+ "dedicated_GB": 8.0,
+ "opportunistic_GB": 3.0,
+ "max_num_objects": 500,
+ "creation_time": 120,
+ "expiration_time": 230,
+ "deletion_time": 280
+ }
+ })";
+
+ const char *testlot3 = R"({
+ "lot_name": "testlot3",
+ "owner": "owner1",
+ "parents": ["testlot3"],
+ "management_policy_attrs": {
+ "dedicated_GB": 7.0,
+ "opportunistic_GB": 2.0,
+ "max_num_objects": 300,
+ "creation_time": 150,
+ "expiration_time": 220,
+ "deletion_time": 270
+ }
+ })";
+
+ addLot(testlot1);
+ addLot(testlot2);
+ addLot(testlot3);
+
+ // Query period where all three overlap
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 300, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 25.0); // 10 + 8 + 7
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 10.0); // 5 + 3 + 2
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 35.0); // 25 + 10
+ EXPECT_EQ(result["max_num_objects"], 1800); // 1000 + 500 + 300
+}
+
+// Test: Multiple lots where max cumulative values and dedicated values occur at
+// different times from different lots
+TEST_F(LotManTest, MaxMPAsForPeriod_DifferentMaximums) {
+ addDefaultLot();
+
+ // Three lots with high dedicated, low opportunistic, overlapping early
+ const char *testlot1 = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 0.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 250
+ }
+ })";
+
+ const char *testlot2 = R"({
+ "lot_name": "testlot2",
+ "owner": "owner1",
+ "parents": ["testlot2"],
+ "management_policy_attrs": {
+ "dedicated_GB": 8.0,
+ "opportunistic_GB": 0.0,
+ "max_num_objects": 500,
+ "creation_time": 120,
+ "expiration_time": 220,
+ "deletion_time": 270
+ }
+ })";
+
+ const char *testlot3 = R"({
+ "lot_name": "testlot3",
+ "owner": "owner1",
+ "parents": ["testlot3"],
+ "management_policy_attrs": {
+ "dedicated_GB": 7.0,
+ "opportunistic_GB": 0.0,
+ "max_num_objects": 300,
+ "creation_time": 130,
+ "expiration_time": 210,
+ "deletion_time": 260
+ }
+ })";
+
+ // One lot with low dedicated, high opportunistic, later in timeline
+ const char *testlot4 = R"({
+ "lot_name": "testlot4",
+ "owner": "owner1",
+ "parents": ["testlot4"],
+ "management_policy_attrs": {
+ "dedicated_GB": 2.0,
+ "opportunistic_GB": 30.0,
+ "max_num_objects": 100,
+ "creation_time": 300,
+ "expiration_time": 400,
+ "deletion_time": 450
+ }
+ })";
+
+ addLot(testlot1);
+ addLot(testlot2);
+ addLot(testlot3);
+ addLot(testlot4);
+
+ // Query entire period
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 500, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ // max_dedicated occurs when testlot1+testlot2+testlot3 overlap (130-200)
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 25.0); // 10 + 8 + 7
+ // max_opportunistic occurs when only testlot4 is active (300-400)
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 30.0); // just testlot4
+ // max_combined is testlot4's total since 32 > 25
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 32.0); // 2 + 30 from testlot4
+ // max_objects occurs when testlot1+testlot2+testlot3 overlap
+ EXPECT_EQ(result["max_num_objects"], 1800); // 1000 + 500 + 300
+}
+
+// Test: Lots spanning beyond query range
+TEST_F(LotManTest, MaxMPAsForPeriod_LotsSpanBeyondRange) {
+ addDefaultLot();
+
+ const char *lot = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 50,
+ "expiration_time": 500,
+ "deletion_time": 600
+ }
+ })";
+ addLot(lot);
+
+ // Query a window within the lot's lifetime
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 200, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ // Lot extends throughout the entire query period
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 10.0);
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 5.0);
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 15.0);
+ EXPECT_EQ(result["max_num_objects"], 1000);
+}
+
+// Test: include_deletion parameter (expiration vs deletion)
+TEST_F(LotManTest, MaxMPAsForPeriod_IncludeDeletion) {
+ addDefaultLot();
+
+ const char *lot = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+ addLot(lot);
+
+ // Query with include_deletion=false (uses expiration_time=200)
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 250, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 10.0);
+
+ // Query with include_deletion=true (uses deletion_time=300)
+ raw_output = nullptr;
+ raw_err = nullptr;
+ rv = lotman_get_max_mpas_for_period(200, 250, true, &raw_output, &raw_err);
+ output.reset(raw_output);
+ err_msg.reset(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ result = json::parse(output.get());
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 10.0); // Still active with deletion endpoint
+ EXPECT_EQ(result["include_deletion"], true);
+}
+
+// Test: Error case - start_ms >= end_ms
+TEST_F(LotManTest, MaxMPAsForPeriod_InvalidTimeRange) {
+ addDefaultLot();
+
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+
+ // start_ms == end_ms should error
+ int rv = lotman_get_max_mpas_for_period(100, 100, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ EXPECT_NE(rv, 0);
+ EXPECT_NE(err_msg.get(), nullptr);
+
+ // start_ms > end_ms should also error
+ raw_output = nullptr;
+ raw_err = nullptr;
+ rv = lotman_get_max_mpas_for_period(200, 100, false, &raw_output, &raw_err);
+ output.reset(raw_output);
+ err_msg.reset(raw_err);
+ EXPECT_NE(rv, 0);
+ EXPECT_NE(err_msg.get(), nullptr);
+}
+
+// Test: No lots overlap the period (should return zeros)
+TEST_F(LotManTest, MaxMPAsForPeriod_NoOverlappingLots) {
+ addDefaultLot();
+
+ const char *lot = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+ addLot(lot);
+
+ // Query period that doesn't overlap with any lot
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(400, 500, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 0.0);
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 0.0);
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 0.0);
+ EXPECT_EQ(result["max_num_objects"], 0);
+}
+
+// Test: Simultaneous events (lots created/expired at same time)
+TEST_F(LotManTest, MaxMPAsForPeriod_SimultaneousEvents) {
+ addDefaultLot();
+
+ // Two lots with identical creation times
+ const char *testlot1 = R"({
+ "lot_name": "testlot1",
+ "owner": "owner1",
+ "parents": ["testlot1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 10.0,
+ "opportunistic_GB": 5.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ const char *testlot2 = R"({
+ "lot_name": "testlot2",
+ "owner": "owner1",
+ "parents": ["testlot2"],
+ "management_policy_attrs": {
+ "dedicated_GB": 8.0,
+ "opportunistic_GB": 3.0,
+ "max_num_objects": 500,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ addLot(testlot1);
+ addLot(testlot2);
+
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 200, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+ // Both lots should be counted together
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 18.0); // 10 + 8
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 8.0); // 5 + 3
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 26.0); // 18 + 8
+ EXPECT_EQ(result["max_num_objects"], 1500); // 1000 + 500
+}
+
+// Test: Lot hierarchies should only count root lots, not children
+TEST_F(LotManTest, MaxMPAsForPeriod_LotHierarchy) {
+ addDefaultLot();
+
+ // Create parent lot (root) with 5GB dedicated
+ const char *parent_lot = R"({
+ "lot_name": "parent_lot",
+ "owner": "owner1",
+ "parents": ["parent_lot"],
+ "management_policy_attrs": {
+ "dedicated_GB": 5.0,
+ "opportunistic_GB": 2.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ // Create child lot with 3GB dedicated (child of parent_lot)
+ const char *child_lot = R"({
+ "lot_name": "child_lot",
+ "owner": "owner1",
+ "parents": ["parent_lot"],
+ "management_policy_attrs": {
+ "dedicated_GB": 3.0,
+ "opportunistic_GB": 1.0,
+ "max_num_objects": 500,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ addLot(parent_lot);
+ addLot(child_lot);
+
+ // Query during overlap period
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 200, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+
+ // Should only count the parent lot (5GB), not parent + child (8GB)
+ // Child's allocation counts toward parent's quota
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 5.0) << "Should only count root lot, not child";
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 2.0);
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 7.0); // 5 + 2
+ EXPECT_EQ(result["max_num_objects"], 1000);
+}
+
+// Test: Multiple independent root lots should be summed
+TEST_F(LotManTest, MaxMPAsForPeriod_MultipleRoots) {
+ addDefaultLot();
+
+ // Create first root lot with 5GB
+ const char *root1 = R"({
+ "lot_name": "root1",
+ "owner": "owner1",
+ "parents": ["root1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 5.0,
+ "opportunistic_GB": 2.0,
+ "max_num_objects": 1000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ // Create second root lot with 8GB
+ const char *root2 = R"({
+ "lot_name": "root2",
+ "owner": "owner1",
+ "parents": ["root2"],
+ "management_policy_attrs": {
+ "dedicated_GB": 8.0,
+ "opportunistic_GB": 3.0,
+ "max_num_objects": 2000,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ // Create child of root1 with 3GB
+ const char *child1 = R"({
+ "lot_name": "child1",
+ "owner": "owner1",
+ "parents": ["root1"],
+ "management_policy_attrs": {
+ "dedicated_GB": 3.0,
+ "opportunistic_GB": 1.0,
+ "max_num_objects": 500,
+ "creation_time": 100,
+ "expiration_time": 200,
+ "deletion_time": 300
+ }
+ })";
+
+ addLot(root1);
+ addLot(root2);
+ addLot(child1);
+
+ // Query during overlap period
+ char *raw_output = nullptr;
+ char *raw_err = nullptr;
+ int rv = lotman_get_max_mpas_for_period(100, 200, false, &raw_output, &raw_err);
+ UniqueCString output(raw_output);
+ UniqueCString err_msg(raw_err);
+ ASSERT_EQ(rv, 0) << err_msg.get();
+
+ json result = json::parse(output.get());
+
+ // Should count both root lots (5 + 8 = 13GB), but not the child
+ EXPECT_DOUBLE_EQ(result["max_dedicated_GB"], 13.0) << "Should count both roots but not child";
+ EXPECT_DOUBLE_EQ(result["max_opportunistic_GB"], 5.0); // 2 + 3
+ EXPECT_DOUBLE_EQ(result["max_combined_GB"], 18.0); // 13 + 5
+ EXPECT_EQ(result["max_num_objects"], 3000); // 1000 + 2000
+}
+
} // namespace
int main(int argc, char **argv) {