fix(personal): keep reservation validation side effect free

This commit is contained in:
2026-07-12 02:20:42 +08:00
parent c80c807283
commit cdc47ee97e
2 changed files with 14 additions and 19 deletions
@@ -38,6 +38,14 @@ public class PersonalSpaceService {
}
}
/**
* Ensures and locks the current owner's space, then validates that one future item of the given size can fit.
* This check intentionally does not mutate usage counters. The future ingestion application service must call
* it inside its outer transaction and perform the item insert plus counter mutation in that same transaction so
* the row lock remains effective. This method must not use an independent transaction.
*
* @return the locked personal space id
*/
@Transactional
public long reserve(PersonalOwner owner, long bytes) {
if (bytes < 0 || properties.getMaxItems() <= 0 || properties.getMaxSpaceMb() <= 0) {
@@ -54,14 +62,6 @@ public class PersonalSpaceService {
throw new ServiceException(QUOTA_EXCEEDED);
}
int updated = jdbcTemplate.update("""
update aihr_personal_space
set used_bytes = used_bytes + ?, item_count = item_count + 1
where id = ? and tenant_id = ? and owner_user_id = ?
""", bytes, spaceId, owner.tenantId(), owner.userId());
if (updated != 1) {
throw new ServiceException(QUOTA_EXCEEDED);
}
return spaceId;
}
@@ -76,12 +76,10 @@ class PersonalSpaceServiceTest {
}
@Test
void reserveUpsertsOwnerSpaceBeforeLockAndAtomicallyConsumesCapacity() {
void reserveUpsertsOwnerSpaceBeforeLockAndReturnsWithoutMutatingCounters() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.queryForMap(contains("from aihr_personal_space"), eq("000000"), eq(101L)))
.thenReturn(space(7L, 500L * 1024 * 1024, 0L, 0));
when(jdbc.update(contains("set used_bytes = used_bytes + ?"),
eq(1024L), eq(7L), eq("000000"), eq(101L))).thenReturn(1);
PersonalSpaceService service = new PersonalSpaceService(jdbc, properties());
assertEquals(7L, service.reserve(new PersonalOwner("000000", 101L, "ext-101"), 1024L));
@@ -90,25 +88,22 @@ class PersonalSpaceServiceTest {
order.verify(jdbc).update(contains("on duplicate key update"),
eq("000000"), eq(101L), eq("ext-101"), eq(500L * 1024 * 1024));
order.verify(jdbc).queryForMap(contains("for update"), eq("000000"), eq(101L));
order.verify(jdbc).update(
contains("where id = ? and tenant_id = ? and owner_user_id = ?"),
eq(1024L), eq(7L), eq("000000"), eq(101L));
verifyNoMoreInteractions(jdbc);
}
@Test
void reserveZeroBytesStillConsumesOneItemSlot() {
void reserveZeroBytesIsValidWithoutMutatingCounters() {
JdbcTemplate jdbc = mock(JdbcTemplate.class);
when(jdbc.queryForMap(contains("from aihr_personal_space"), eq("000000"), eq(101L)))
.thenReturn(space(7L, 1024L, 500L, 3));
when(jdbc.update(contains("set used_bytes = used_bytes + ?"),
eq(0L), eq(7L), eq("000000"), eq(101L))).thenReturn(1);
PersonalSpaceService service = new PersonalSpaceService(jdbc, properties());
assertEquals(7L, service.reserve(new PersonalOwner("000000", 101L, null), 0L));
verify(jdbc).update(contains("item_count = item_count + 1"),
eq(0L), eq(7L), eq("000000"), eq(101L));
verify(jdbc).update(contains("on duplicate key update"),
eq("000000"), eq(101L), eq(null), eq(500L * 1024 * 1024));
verify(jdbc).queryForMap(contains("for update"), eq("000000"), eq(101L));
verifyNoMoreInteractions(jdbc);
}
@Test