Skip to content

Commit 294e7fb

Browse files
committed
fix: resolve connection pool error
1 parent 3c664aa commit 294e7fb

3 files changed

Lines changed: 43 additions & 18 deletions

File tree

โ€Žsrc/main/java/com/example/echoshotx/notification/application/service/SseConnectionManager.javaโ€Ž

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import java.util.List;
99
import java.util.Map;
1010
import java.util.concurrent.ConcurrentHashMap;
11-
import java.util.concurrent.CopyOnWriteArrayList;
1211

1312
import lombok.extern.slf4j.Slf4j;
1413
import org.springframework.stereotype.Component;
@@ -27,40 +26,39 @@ public class SseConnectionManager {
2726
private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; // 60๋ถ„
2827
private static final String SSE_EVENT_NAME = "notification";
2928

30-
// Key: memberId, Value: List of SseEmitters (๋‹ค์ค‘ ๋””๋ฐ”์ด์Šค ์ง€์›)
29+
// Key: memberId, Value: SseEmitter (๋‹จ์ผ ๋””๋ฐ”์ด์Šค)
3130
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
3231

3332
public SseEmitter createConnection(Long memberId) {
34-
// ๊ธฐ์กด ์—ฐ๊ฒฐ์ด ์žˆ์œผ๋ฉด ์ข…๋ฃŒ (์ƒˆ ์—ฐ๊ฒฐ ์šฐ์„ )
35-
SseEmitter existingEmitter = emitters.get(memberId);
36-
if (existingEmitter != null) {
37-
existingEmitter.complete();
38-
log.info("Existing SSE connection closed for member: {}", memberId);
39-
}
40-
4133
SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT);
42-
emitters.put(memberId, emitter);
34+
35+
// ๊ธฐ์กด ์—ฐ๊ฒฐ์ด ์žˆ์–ด๋„ ๊ฐ•์ œ ์ข…๋ฃŒํ•˜์ง€ ์•Š์Œ - ๊ทธ๋ƒฅ ์ƒˆ ์—ฐ๊ฒฐ๋กœ ๊ต์ฒด
36+
// ๊ธฐ์กด emitter๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ timeout/completion ์ฒ˜๋ฆฌ๋จ
37+
SseEmitter oldEmitter = emitters.put(memberId, emitter);
38+
if (oldEmitter != null) {
39+
log.info("Replacing existing SSE connection for member: {} (old connection will timeout naturally)", memberId);
40+
}
4341

4442
log.info("SSE connection created for member: {}", memberId);
4543

46-
// ์—ฐ๊ฒฐ ์™„๋ฃŒ ์‹œ ์ œ๊ฑฐ
44+
// ์—ฐ๊ฒฐ ์™„๋ฃŒ ์‹œ ์ œ๊ฑฐ (ํ˜„์žฌ ํ™œ์„ฑ emitter์ธ ๊ฒฝ์šฐ๋งŒ)
4745
emitter.onCompletion(
4846
() -> {
49-
removeEmitter(memberId);
47+
removeEmitterIfMatch(memberId, emitter);
5048
log.info("SSE connection completed for member: {}", memberId);
5149
});
5250

53-
// ํƒ€์ž„์•„์›ƒ ์‹œ ์ œ๊ฑฐ
51+
// ํƒ€์ž„์•„์›ƒ ์‹œ ์ œ๊ฑฐ (ํ˜„์žฌ ํ™œ์„ฑ emitter์ธ ๊ฒฝ์šฐ๋งŒ)
5452
emitter.onTimeout(
5553
() -> {
56-
removeEmitter(memberId);
54+
removeEmitterIfMatch(memberId, emitter);
5755
log.warn("SSE connection timeout for member: {}", memberId);
5856
});
5957

60-
// ์—๋Ÿฌ ์‹œ ์ œ๊ฑฐ
58+
// ์—๋Ÿฌ ์‹œ ์ œ๊ฑฐ (ํ˜„์žฌ ํ™œ์„ฑ emitter์ธ ๊ฒฝ์šฐ๋งŒ)
6159
emitter.onError(
6260
e -> {
63-
removeEmitter(memberId);
61+
removeEmitterIfMatch(memberId, emitter);
6462
log.error("SSE connection error for member: {}, error: {}", memberId, e.getMessage());
6563
});
6664

@@ -69,7 +67,7 @@ public SseEmitter createConnection(Long memberId) {
6967
emitter.send(SseEmitter.event().name("connected").data("SSE connection established"));
7068
} catch (IOException e) {
7169
log.error("Failed to send initial connection event to member: {}", memberId, e);
72-
removeEmitter(memberId);
70+
removeEmitterIfMatch(memberId, emitter);
7371
throw new NotificationHandler(NotificationErrorStatus.SSE_CONNECTION_FAILED);
7472
}
7573

@@ -95,7 +93,7 @@ public boolean sendToMember(Long memberId, Object data) {
9593
emitter.send(SseEmitter.event().name(SSE_EVENT_NAME).data(data));
9694
log.info("Notification sent to member: {}", memberId);
9795
return true;
98-
} catch (IOException e) {
96+
} catch (IOException | IllegalStateException e) {
9997
log.error("Failed to send notification to member: {}, removing dead emitter", memberId, e);
10098
removeEmitter(memberId);
10199
return false;
@@ -202,6 +200,14 @@ private void removeEmitter(Long memberId) {
202200
emitters.remove(memberId);
203201
}
204202

203+
/**
204+
* ํŠน์ • Emitter๊ฐ€ ํ˜„์žฌ ํ™œ์„ฑ emitter์ธ ๊ฒฝ์šฐ์—๋งŒ ์ œ๊ฑฐ.
205+
* ์ด๋ฏธ ์ƒˆ ์—ฐ๊ฒฐ๋กœ ๊ต์ฒด๋œ ๊ฒฝ์šฐ ์ด์ „ emitter์˜ ์ฝœ๋ฐฑ์ด ์ƒˆ emitter๋ฅผ ์ œ๊ฑฐํ•˜์ง€ ์•Š๋„๋ก ํ•จ.
206+
*/
207+
private void removeEmitterIfMatch(Long memberId, SseEmitter emitter) {
208+
emitters.remove(memberId, emitter);
209+
}
210+
205211
/**
206212
* ๋ชจ๋“  ์—ฐ๊ฒฐ ํ•ด์ œ (์„œ๋ฒ„ ์ข…๋ฃŒ ์‹œ ์‚ฌ์šฉ).
207213
*/

โ€Žsrc/main/java/com/example/echoshotx/notification/presentation/controller/TestNotificationController.javaโ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import jakarta.validation.Valid;
1616
import lombok.RequiredArgsConstructor;
1717
import lombok.extern.slf4j.Slf4j;
18+
import org.springframework.http.MediaType;
1819
import org.springframework.web.bind.annotation.GetMapping;
1920
import org.springframework.web.bind.annotation.PathVariable;
2021
import org.springframework.web.bind.annotation.PostMapping;
2122
import org.springframework.web.bind.annotation.RequestBody;
2223
import org.springframework.web.bind.annotation.RequestMapping;
2324
import org.springframework.web.bind.annotation.RestController;
25+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
2426

2527
/**
2628
* ํ…Œ์ŠคํŠธ์šฉ ์•Œ๋ฆผ API ์ปจํŠธ๋กค๋Ÿฌ.
@@ -114,4 +116,13 @@ public ApiResponseDto<String> broadcastNotification(
114116
return ApiResponseDto.onSuccess(
115117
String.format("%d๊ฐœ์˜ ์—ฐ๊ฒฐ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ๋ฅผ ์ „์†กํ–ˆ์Šต๋‹ˆ๋‹ค.", totalConnections));
116118
}
119+
120+
@Operation(summary = "๐Ÿ”ง ํ…Œ์ŠคํŠธ์šฉ SSE ๊ตฌ๋… (ํ† ํฐ ๋ถˆํ•„์š”)",
121+
description = "ํ† ํฐ ์—†์ด memberId๋ฅผ ์ง์ ‘ ์ง€์ •ํ•˜์—ฌ SSE ์—ฐ๊ฒฐ์„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค. " +
122+
"curl -N 'http://์„œ๋ฒ„์ฃผ์†Œ/test/notifications/subscribe/1' ๋กœ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
123+
@GetMapping(value = "/subscribe/{memberId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
124+
public SseEmitter testSubscribe(@PathVariable Long memberId) {
125+
log.info("๐Ÿ”ง [TEST] SSE connection request for memberId: {}", memberId);
126+
return sseConnectionManager.createConnection(memberId);
127+
}
117128
}

โ€Žsrc/main/resources/application.ymlโ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ spring:
33
active:
44
- local
55
datasource:
6+
7+
hikari:
8+
maximum-pool-size: 30
9+
minimum-idle: 10
10+
connection-timeout: 30000
11+
idle-timeout: 600000
12+
max-lifetime: 1800000
613
url: ${SPRING_DATASOURCE_URL}
714
username: ${SPRING_DATASOURCE_USERNAME}
815
password: ${SPRING_DATASOURCE_PASSWORD}
@@ -15,6 +22,7 @@ spring:
1522

1623

1724
jpa:
25+
open-in-view: false
1826
hibernate:
1927
ddl-auto: update
2028
defer-datasource-initialization: true

0 commit comments

Comments
ย (0)