diff --git a/src/main/java/com/ssu/unithon/call/application/CallService.java b/src/main/java/com/ssu/unithon/call/application/CallService.java index cea293e..b31ef3b 100644 --- a/src/main/java/com/ssu/unithon/call/application/CallService.java +++ b/src/main/java/com/ssu/unithon/call/application/CallService.java @@ -35,6 +35,8 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; @Service public class CallService { @@ -174,7 +176,7 @@ public void terminate(UUID callId, String participantKey) { Call call = lockedCall(callId); if (call.getStatus() == CallStatus.ENDED) return; call.end(clock.instant()); - closeLiveKitRoom(call); + afterCommit(() -> closeLiveKitRoom(call)); } @Transactional(readOnly = true) @@ -320,7 +322,7 @@ public void end(UUID callId, String creatorKey) { } call.end(clock.instant()); - closeLiveKitRoom(call); + afterCommit(() -> closeLiveKitRoom(call)); } @Transactional(readOnly = true) @@ -345,6 +347,27 @@ private void cleanupStalePhoneCalls(String phoneNumber) { } } + /** + * Runs {@code action} only after this transaction commits. closeLiveKitRoom kicks the other + * participant's live LiveKit connection immediately (a real-time push, not a poll), which was + * racing the ENDED/REJECTED status write it's paired with: that write doesn't durably commit + * until the surrounding @Transactional method returns, so the kicked participant's client + * could poll /status right after being disconnected and still read the old ACTIVE status. + * Deferring to afterCommit makes the status change visible before the disconnect fires. + */ + private void afterCommit(Runnable action) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + action.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + action.run(); + } + }); + } + private void closeLiveKitRoom(Call call) { for (ParticipantSlot slot : slots.findByCallId(call.getId())) { gateway.remove(call.getLivekitRoomName(), slot.getIdentity());