Conversation
Implement the subnet_pool_utilization_view endpoint, which was previously a stub returning 500. The implementation follows the IP pool utilization pattern: fetch subnet CIDRs from the database, compute sizes in Rust using u128 arithmetic, and return remaining/capacity as f64. Changes: - Add a new API version (SUBNET_POOL_UTILIZATION_REMAINING) that changes SubnetPoolUtilization from allocated/capacity to remaining/capacity, matching IpPoolUtilization. - Add datastore methods: subnet_pool_utilization, helpers for fetching member and external subnet CIDRs, and accumulate_subnet_sizes. - Wire up the app layer to call the datastore and compute remaining. - Add datastore unit tests for IPv4 and IPv6 pools. - Add integration tests via assert_subnet_pool_utilization helper. - Update verify_endpoints from GetUnimplemented to Get.
b699fb4 to
4e58295
Compare
| } | ||
| Ok(count) | ||
| } | ||
|
|
There was a problem hiding this comment.
Compare to IP pool utilization:
omicron/nexus/db-queries/src/db/datastore/ip_pool.rs
Lines 864 to 986 in b2d7cde
| fn from(new: SubnetPoolUtilization) -> Self { | ||
| Self { allocated: new.capacity - new.remaining, capacity: new.capacity } | ||
| } | ||
| } |
There was a problem hiding this comment.
This is silly and I think loses precision, but the endpoint was unimplemented, so there can't be anyone who is relying on the current version.
| let capacity = (1u128 << 80) as f64; | ||
| assert_subnet_pool_utilization(client, SUBNET_POOL_NAME, 0.0, capacity) | ||
| .await; | ||
| } |
There was a problem hiding this comment.
compare to
omicron/nexus/tests/integration_tests/ip_pools.rs
Lines 939 to 1026 in b2d7cde
|
|
||
| db.terminate().await; | ||
| logctx.cleanup_successful(); | ||
| } |
There was a problem hiding this comment.
compare to
omicron/nexus/db-queries/src/db/datastore/ip_pool.rs
Lines 2784 to 3039 in b2d7cde
| }; | ||
| let remaining = remaining as f64; | ||
| let capacity = capacity as f64; | ||
| Ok(subnet_pool_types::SubnetPoolUtilization { remaining, capacity }) |
There was a problem hiding this comment.
Compare to the below. I noticed this is a utilization file instead of in the IP pools file. Not sure that really makes sense but I'll probably try to make it consistent.
omicron/nexus/src/app/utilization.rs
Lines 35 to 65 in db6f6b0
mergeconflict
left a comment
There was a problem hiding this comment.
Sorry for the very late review!
| .filter(subnet_pool_member::subnet_pool_id.eq(pool_id)) | ||
| .filter(subnet_pool_member::time_deleted.is_null()) | ||
| .select(subnet_pool_member::subnet) | ||
| .limit(10000) |
There was a problem hiding this comment.
I have no idea how likely we are to ever hit the limit(10000) here (and below in subnet_pool_list_allocated_subnets_on_connection), but if we do, we'll start missing stuff.
I wonder if it's possible to do this DB work in a more pipelined fashion so that we don't have to return a giant Vec<IpNet>. Like, would it be possible to join subnet_pool_member with external_subnet, paginate that query, and keep a running tally of the allocations? Or maybe better, is it possible to express the whole thing as SQL so you can just get a pair of scalar values back from the query?
There was a problem hiding this comment.
There's a reason it's not in SQL, but I don't remember what it is. Let me find the PR where I added the IP pool utilization logic.
There was a problem hiding this comment.
Found it: #5258 (comment)
neither CRDB nor Postgres support subtraction of IPv6 addresses when the result is greater than a 64 bit integer, and we couldn't figure out how to convert the addresses to 128 bit integers first and then subtract
I'll let the robots have a shot at it but mostly likely you can't do it.
There was a problem hiding this comment.
We needed subtraction in the IP Range case because they are generic (start, end) tuples. We don't need that here. We do need addition, but since we're working on subnets, which are explicitly powers-of-two, that gives us some options. We can either:
- Convert the sizes to floats in the DB and sum that way. We lose precision, but we already lose precision in the API response anyway.
- Keep track of everything as powers of two, and then do the final exponentiation in Rust where we have a native 128-bit type. There are probably a few ways we could do that, such as keeping track of the count of all subnet sizes / mask lengths grouped by the subnet size. There are at most 128 values to keep track of here.
There was a problem hiding this comment.
This also raises a question IMO: do we want to keep the subnet sizes in the response? For IP Pool utilization, the unit is always a single address, so counting 1s is correct. Here, someone might care about how the members in a pool are broken down as a function of the sizes of those subnets. It would tell you something about fragmentation, which is part of why we added the min / max prefix lengths in the first place.
There was a problem hiding this comment.
Do you have an idea of how to represent fragmentation in a useful way without listing everything there is? it also matters how it's packed, right? Not just the sizes. Like maybe what you really want is the size of the largest gap or something.
There was a problem hiding this comment.
Claude likes option 1 a lot (it ran a bunch of SQL to test it), will try it.
Details
OK, here's the full picture. Three viable approaches for eliminating the limit(10000):
Approach 1: Pure float in SQL — return two scalar f64 values directly:
SELECT
(SELECT SUM(pow(2::FLOAT8, (CASE WHEN family(subnet) = 4 THEN 32 ELSE 128 END
- masklen(subnet))::FLOAT8))
FROM subnet_pool_member WHERE ...) AS capacity,
(SELECT SUM(pow(2::FLOAT8, ...))
FROM external_subnet WHERE ...) AS allocated;This works and returns exactly the (f64, f64) that the API needs (SubnetPoolUtilization is { allocated: f64, capacity: f64 }). The precision loss is real — a /16 (65536 addresses) vanishes when summed with two /48s — but since the API is already f64, this doesn't make things any worse than what the client sees. The imprecision exists today too, just introduced at a different point (Rust u128 → f64 in the API response). This is the simplest option and eliminates both the limit and the round-trip of row data entirely.
Approach 2: GROUP BY masklen, compute in Rust — your colleague's second suggestion:
SELECT 'member' AS source, family(subnet) AS ip_ver, masklen(subnet) AS prefix_len, count(*) AS cnt
FROM subnet_pool_member WHERE ...
GROUP BY family(subnet), masklen(subnet)
UNION ALL
SELECT 'allocated', family(subnet), masklen(subnet), count(*)
FROM external_subnet WHERE ...
GROUP BY family(subnet), masklen(subnet);Returns at most ~258 rows (129 possible prefix lengths × 2 sources). Rust then does sum(cnt * 2^(bits - prefix_len)) with u128 arithmetic. This is exact and eliminates the limit, but requires a custom Diesel query and more Rust-side processing.
Approach 3: Paginate the existing queries. Replace limit(10000) with proper pagination. Most complex, least benefit — you'd still be fetching all the rows and accumulating in Rust.
Given that the API type is already f64, approach 1 seems like the obvious winner: one SQL query, two scalar values, no limit, no row transfer, and no precision regression relative to what the API already exposes. The only argument for approach 2 is if you wanted to keep exact u128 internally for some reason (e.g., logging, assertions, or a future API change away from f64).
There was a problem hiding this comment.
Do you have an idea of how to represent fragmentation in a useful way without listing everything there is? it also matters how it's packed, right? Not just the sizes. Like maybe what you really want is the size of the largest gap or something.
How it's packed does matter, yeah. Ideally you'd want to know how many "slots" there are of each legal size, but that might not be worth it. The largest gap could be enough. We're getting solidly into the "fancy" and "subjective" realm, but you could imagine a "fragmentation index". Something like the ratio of the number of subnets there actually are to the smallest number that could "cover" the same portion of the address space. For example, if you had a bunch of small subnets spaced throughout, those could be compacted into one larger one. 1 - (minimum subnets to cover / actual subnets that cover) would give you an index in [0, 1] with higher numbers being more fragmented.
I admit this is pretty esoteric, and I'm not sure what you do with it. The next question is going to just be, "Ok, how can I rejigger this super-fragmented pool?", which requires looking at the actual allocated external subnets.
All that's to say, converting to floats in the DB seems fine to me too at this point.
There was a problem hiding this comment.
Got it in c620b02. All existing tests pass. Huge victory for the robot — I don't think I could have done this. First it wrote straight SQL, and that was fine. Then I had it convert it to proper Diesel and it was even better.
Add assert_subnet_pool_utilization calls to verify that utilization tracks correctly as subnets are allocated/deleted and pool members are added/removed. external_subnets.rs (allocated utilization): - external_subnet_basic_crud: 0/256 before, 16/256 after /28, 0/256 after delete - external_subnet_pagination: 160/256 after 10 /28s - attach_test_impl (running + stopped): 16/256 after /28 - cannot_attach_too_many_subnets: MAX*4/256, then (MAX+1)*4/256 - external_subnet_create_name_conflict: 16/256 after first, still 16/256 after failed dup - test_limited_collaborator_external_subnet_lifecycle: 16/256 after create, 0/256 after delete subnet_pools.rs (capacity utilization): - basic_subnet_pool_member_crd: 0 initially, grows with 100 /48 members, shrinks after removing one Add comment to dedicated utilization tests pointing to the external subnet tests for allocated coverage.
708d46f to
45d1151
Compare
oxidecomputer/omicron#10157 implements this on the API side. Because subnet pools don't have a type field like IP pools, I had to add last modified time to the properties table on the details page to get an even number of cells. <img width="1122" height="401" alt="image" src="https://github.com/user-attachments/assets/4d7b0f48-67d5-4b54-945f-a2343be471f5" /> <img width="1116" height="245" alt="image" src="https://github.com/user-attachments/assets/825e445d-46e7-4a7c-bccf-22ea2e24f0e1" />
Closes #10109
Follows the same pattern as IP pool utilization. Counting addresses feels a little weird, since subnets are blocks of addresses, but there isn't really another unit we can use.
accumulate_subnet_sizessums subnet sizes using checked 128-bit arithmetic, analogous toaccumulate_ip_range_sizes.remaining = capacity - allocatedin u128 before converting to f64, matchingip_pool_utilization_view.{ remaining, capacity }(both f64), matchingIpPoolUtilization.