-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathResponseHandler.java
More file actions
208 lines (171 loc) · 7.31 KB
/
Copy pathResponseHandler.java
File metadata and controls
208 lines (171 loc) · 7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
package community.solace.spring.cloud.requestreply.service;
import community.solace.spring.cloud.requestreply.service.header.parser.errormessage.RemoteErrorException;
import community.solace.spring.cloud.requestreply.service.logging.RequestReplyLogger;
import io.micrometer.core.instrument.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import org.springframework.messaging.Message;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.time.Instant;
import java.util.BitSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class ResponseHandler {
private static final Logger LOG = LoggerFactory.getLogger(ResponseHandler.class);
private final CountDownLatch countDownLatch;
private final AtomicLong expectedReplies = new AtomicLong(1);
private final AtomicLong receivedReplies = new AtomicLong(0);
private final boolean supportMultipleResponses;
private final Instant requestTime;
private final Timer timer;
private final Consumer<Message<?>> responseMessageConsumer;
private boolean isFirstMessage = true;
private String errorMessage;
private final RequestReplyLogger requestReplyLogger;
/**
* Fast-path dedup store for replyIndex values when totalReplies is known.
*
* <p>We store numeric indices (e.g. "12") and numeric ranges (e.g. "0-15") inside a BitSet.
* This is designed for very large reply counts without excessive allocation pressure.</p>
*
* <p>Protected by synchronizing on {@code numericReplyIndexBitSetLock} because BitSet is not thread-safe.</p>
*/
private volatile BitSet numericReplyIndexBitSet;
private volatile int numericReplyIndexBitSetSize = -1;
private final Object numericReplyIndexBitSetLock = new Object();
// Grow limit for unknown-size / streaming cases to avoid unbounded memory use on malformed indices.
// For known totalReplies, numericReplyIndexBitSetSize will cap growth.
private static final int MAX_DEDUP_BITS_WHEN_UNKNOWN = Integer.getInteger(
"spring.cloud.stream.requestreply.dedup.maxBitsWhenUnknown",
100_000
);
public ResponseHandler(Consumer<Message<?>> responseMessageConsumer, boolean supportMultipleResponses, Timer timer, RequestReplyLogger requestReplyLogger) {
this.countDownLatch = new CountDownLatch(1);
this.responseMessageConsumer = responseMessageConsumer;
this.supportMultipleResponses = supportMultipleResponses;
this.requestTime = Instant.now();
this.timer = timer;
this.requestReplyLogger = requestReplyLogger;
}
public void receive(Message<?> message) {
long remainingReplies = expectedReplies.get() - receivedReplies.incrementAndGet();
if (remainingReplies >= 0) { // In case of unknown replies, the last message has no valid content.
responseMessageConsumer.accept(message);
}
if (remainingReplies <= 0) { // Normally -1
finished();
}
requestReplyLogger.logReply(LOG, Level.DEBUG, "received response(remaining={}) {}", remainingReplies, message);
}
public boolean checkDuplicate(String replyIndex) {
if (replyIndex == null) {
return false;
}
// Support both "n" and "start-end" (range dedup is by start only).
Integer start = tryParseNonNegativeReplyIndexStart(replyIndex);
if (start == null) {
// Keep runtime fast: ignore non-numeric indices rather than allocating fallback structures.
requestReplyLogger.log(LOG, Level.DEBUG, "replyIndex '{}' is not numeric; skipping dedup", replyIndex);
return false;
}
synchronized (numericReplyIndexBitSetLock) {
// Lazily allocate & grow BitSet.
if (numericReplyIndexBitSet == null) {
numericReplyIndexBitSet = new BitSet(Math.min(1024, Math.max(start + 1, 0)));
}
// If totalReplies is known, clamp to [0, total-1].
if (numericReplyIndexBitSetSize > 0) {
if (start >= numericReplyIndexBitSetSize) {
return false;
}
} else {
// Unknown totalReplies (streaming): cap growth to avoid unbounded memory.
if (start >= MAX_DEDUP_BITS_WHEN_UNKNOWN) {
return false;
}
}
// Dedup by start index only.
if (numericReplyIndexBitSet.get(start)) {
requestReplyLogger.log(LOG, Level.WARN, "received duplicate response(index={})", replyIndex);
return true;
}
numericReplyIndexBitSet.set(start);
return false;
}
}
public void await() throws RemoteErrorException, InterruptedException {
countDownLatch.await();
if (StringUtils.hasText(errorMessage)) {
throw new RemoteErrorException(errorMessage);
}
}
public void setTotalReplies(Long totalReplies) {
if (supportMultipleResponses && isFirstMessage && totalReplies >= 1) {
// Set total messages to expect when a multi message on a first message.
expectedReplies.set(totalReplies);
isFirstMessage = false;
// If totalReplies is known and within Integer range, enable bounded numeric dedup.
if (totalReplies <= Integer.MAX_VALUE) {
numericReplyIndexBitSetSize = totalReplies.intValue();
// Don't eagerly allocate; it might never be needed if replyIndex isn't present.
}
}
}
public void setUnknownReplies() {
if (supportMultipleResponses) {
expectedReplies.set(Long.MAX_VALUE);
}
}
public void emptyResponse() {
isFirstMessage = false;
finished();
}
public void errorResponse(String errorMessage) {
isFirstMessage = false;
this.errorMessage = errorMessage;
finished();
}
private static Integer tryParseNonNegativeReplyIndexStart(String text) {
// Accept:
// - "123" -> 123
// - "12-34" -> 12 (start)
// Reject:
// - "-1", "12-", "-12", "12-34-56", "", non-digits
int len = text.length();
if (len == 0) {
return null;
}
int value = 0;
for (int i = 0; i < len; i++) {
char c = text.charAt(i);
if (c == '-') {
// must not be leading or trailing
return (i == 0 || i == len - 1) ? null : value;
}
if (c < '0' || c > '9') {
return null;
}
value = value * 10 + (c - '0');
if (value < 0) {
return null;
}
}
return value;
}
private void finished() {
// Clear per-request dedup bookkeeping to avoid retaining replyIndex values
// longer than necessary (success, error, or timeout/abort paths all call finished()).
numericReplyIndexBitSet = null;
numericReplyIndexBitSetSize = -1;
if (timer != null) {
timer.record(Duration.between(requestTime, Instant.now()));
}
countDownLatch.countDown();
}
public void abort() {
finished();
}
}