-
Notifications
You must be signed in to change notification settings - Fork 177
Client side consumer recovery #1043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
3ca2b8e
77c69f0
b477e14
94d2ab7
2275447
d3acee4
3b168d4
8f2d46f
8c8c0b6
0493d06
8c7c8ee
26d3aaa
22ed0ee
ee0fb9d
2c297b3
fa30d51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,13 @@ public CachedStreamInfo(StreamInfo si) { | |
multipleSubjectFilter210Available = conn.getInfo().isNewerVersionThan("2.9.99"); | ||
} | ||
|
||
NatsJetStreamImpl(NatsJetStreamImpl impl) throws IOException { | ||
conn = impl.conn; | ||
jso = impl.jso; | ||
consumerCreate290Available = impl.consumerCreate290Available; | ||
multipleSubjectFilter210Available = impl.multipleSubjectFilter210Available; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. implementation to make a JetStream context from a JetStreamManagement context |
||
// ---------------------------------------------------------------------------------------------------- | ||
// Management that is also needed by regular context | ||
// ---------------------------------------------------------------------------------------------------- | ||
|
@@ -173,7 +180,7 @@ String generateConsumerName() { | |
return NUID.nextGlobalSequence(); | ||
} | ||
|
||
ConsumerConfiguration nextOrderedConsumerConfiguration( | ||
ConsumerConfiguration consumerConfigurationStartAfterLast( | ||
ConsumerConfiguration originalCc, | ||
long lastStreamSeq, | ||
String newDeliverSubject) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,9 +19,12 @@ | |
import java.io.IOException; | ||
|
||
class NatsMessageConsumer extends NatsMessageConsumerBase implements PullManagerObserver { | ||
protected final PullRequestOptions rePullPro; | ||
protected final ConsumeOptions opts; | ||
protected final int thresholdMessages; | ||
protected final long thresholdBytes; | ||
protected final SimplifiedSubscriptionMaker subscriptionMaker; | ||
protected final Dispatcher userDispatcher; | ||
protected final MessageHandler userMessageHandler; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. making the PullRequestOptions on the fly, so need to keep the original user options, etc. around. |
||
|
||
NatsMessageConsumer(SimplifiedSubscriptionMaker subscriptionMaker, | ||
ConsumerInfo cachedConsumerInfo, | ||
|
@@ -31,40 +34,67 @@ class NatsMessageConsumer extends NatsMessageConsumerBase implements PullManager | |
{ | ||
super(cachedConsumerInfo); | ||
|
||
this.subscriptionMaker = subscriptionMaker; | ||
this.opts = opts; | ||
this.userDispatcher = userDispatcher; | ||
this.userMessageHandler = userMessageHandler; | ||
|
||
int bm = opts.getBatchSize(); | ||
long bb = opts.getBatchBytes(); | ||
|
||
int rePullMessages = Math.max(1, bm * opts.getThresholdPercent() / 100); | ||
long rePullBytes = bb == 0 ? 0 : Math.max(1, bb * opts.getThresholdPercent() / 100); | ||
rePullPro = PullRequestOptions.builder(rePullMessages) | ||
.maxBytes(rePullBytes) | ||
.expiresIn(opts.getExpiresInMillis()) | ||
.idleHeartbeat(opts.getIdleHeartbeat()) | ||
.build(); | ||
|
||
thresholdMessages = bm - rePullMessages; | ||
thresholdBytes = bb == 0 ? Integer.MIN_VALUE : bb - rePullBytes; | ||
|
||
doSub(); | ||
} | ||
|
||
void doSub() throws JetStreamApiException, IOException { | ||
MessageHandler mh = userMessageHandler == null ? null : msg -> { | ||
userMessageHandler.onMessage(msg); | ||
if (stopped.get() && pmm.noMorePending()) { | ||
finished.set(true); | ||
} | ||
}; | ||
initSub(subscriptionMaker.subscribe(mh, userDispatcher)); | ||
sub._pull(PullRequestOptions.builder(bm) | ||
.maxBytes(bb) | ||
.expiresIn(opts.getExpiresInMillis()) | ||
.idleHeartbeat(opts.getIdleHeartbeat()) | ||
.build(), | ||
false, this); | ||
super.initSub(subscriptionMaker.subscribe(mh, userDispatcher)); | ||
repull(); | ||
} | ||
|
||
@Override | ||
public void pendingUpdated() { | ||
if (!stopped.get() && (pmm.pendingMessages <= thresholdMessages || (pmm.trackingBytes && pmm.pendingBytes <= thresholdBytes))) | ||
{ | ||
sub._pull(rePullPro, false, this); | ||
repull(); | ||
} | ||
} | ||
|
||
boolean subMadeAfterHeartbeatError = false; | ||
|
||
@Override | ||
public void heartbeatError() { | ||
try { | ||
if (pmm.hasUnansweredPulls() && subMadeAfterHeartbeatError) { | ||
// we went an entire heartbeat cycle without so much as | ||
// this consumer is dead | ||
lenientClose(); | ||
return; | ||
} | ||
subMadeAfterHeartbeatError = true; | ||
doSub(); | ||
} | ||
catch (JetStreamApiException | IOException e) { | ||
// TODO FIGURE OUT WHAT TO DO HERE IF ANYTHING | ||
} | ||
} | ||
|
||
private void repull() { | ||
int rePullMessages = Math.max(1, opts.getBatchSize() - pmm.pendingMessages); | ||
long rePullBytes = opts.getBatchBytes() == 0 ? 0 : opts.getBatchBytes() - pmm.pendingBytes; | ||
PullRequestOptions pro = PullRequestOptions.builder(rePullMessages) | ||
.maxBytes(rePullBytes) | ||
.expiresIn(opts.getExpiresInMillis()) | ||
.idleHeartbeat(opts.getIdleHeartbeat()) | ||
.build(); | ||
sub._pull(pro, false, this); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,4 +15,5 @@ | |
|
||
interface PullManagerObserver { | ||
void pendingUpdated(); | ||
void heartbeatError(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs this because the manager can't be responsible for restarts because the PMM is only managing messages, not the subscriptions / state getting messages. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,9 @@ | |
import io.nats.client.SubscribeOptions; | ||
import io.nats.client.support.Status; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
import static io.nats.client.impl.MessageManager.ManageResult.*; | ||
import static io.nats.client.support.NatsJetStreamConstants.NATS_PENDING_BYTES; | ||
import static io.nats.client.support.NatsJetStreamConstants.NATS_PENDING_MESSAGES; | ||
|
@@ -30,12 +33,14 @@ class PullMessageManager extends MessageManager { | |
protected boolean trackingBytes; | ||
protected boolean raiseStatusWarnings; | ||
protected PullManagerObserver pullManagerObserver; | ||
protected boolean initialized; | ||
protected List<String> unansweredPulls; | ||
|
||
protected PullMessageManager(NatsConnection conn, SubscribeOptions so, boolean syncMode) { | ||
super(conn, so, syncMode); | ||
trackingBytes = false; | ||
pendingMessages = 0; | ||
pendingBytes = 0; | ||
initialized = false; | ||
unansweredPulls = new ArrayList<>(); | ||
reset(); | ||
} | ||
|
||
@Override | ||
|
@@ -59,61 +64,98 @@ protected void startPullRequest(String pullSubject, PullRequestOptions pro, bool | |
else { | ||
shutdownHeartbeatTimer(); | ||
} | ||
unansweredPulls.add(pullSubject); | ||
} | ||
} | ||
|
||
@Override | ||
protected void handleHeartbeatError() { | ||
super.handleHeartbeatError(); | ||
reset(); | ||
if (pullManagerObserver != null) { | ||
pullManagerObserver.heartbeatError(); | ||
} | ||
} | ||
|
||
private void trackPending(int m, long b) { | ||
private void trackIncoming(int m, long b, String pullsubject) { | ||
synchronized (stateChangeLock) { | ||
pendingMessages -= m; | ||
boolean zero = pendingMessages < 1; | ||
if (trackingBytes) { | ||
pendingBytes -= b; | ||
zero |= pendingBytes < 1; | ||
// message time used for heartbeat tracking | ||
// subjects used to detect multiple failed heartbeats | ||
lastMsgReceived = System.currentTimeMillis(); | ||
|
||
if (pullsubject == null) { | ||
unansweredPulls.clear(); | ||
} | ||
if (zero) { | ||
pendingMessages = 0; | ||
pendingBytes = 0; | ||
trackingBytes = false; | ||
if (hb) { | ||
shutdownHeartbeatTimer(); | ||
} | ||
else { | ||
unansweredPulls.remove(pullsubject); | ||
} | ||
if (pullManagerObserver != null) { | ||
pullManagerObserver.pendingUpdated(); | ||
|
||
if (m != Integer.MIN_VALUE) { | ||
pendingMessages -= m; | ||
boolean zero = pendingMessages < 1; | ||
if (trackingBytes) { | ||
pendingBytes -= b; | ||
zero |= pendingBytes < 1; | ||
} | ||
if (zero) { | ||
reset(); | ||
} | ||
|
||
if (pullManagerObserver != null) { | ||
pullManagerObserver.pendingUpdated(); | ||
} | ||
} | ||
} | ||
} | ||
|
||
protected void reset() { | ||
pendingMessages = 0; | ||
pendingBytes = 0; | ||
trackingBytes = false; | ||
if (initialized && hb) { | ||
shutdownHeartbeatTimer(); | ||
} | ||
initialized = true; | ||
} | ||
|
||
protected boolean hasUnansweredPulls() { | ||
synchronized (stateChangeLock) { | ||
return !unansweredPulls.isEmpty(); | ||
} | ||
} | ||
|
||
@Override | ||
protected Boolean beforeQueueProcessorImpl(NatsMessage msg) { | ||
messageReceived(); // record message time. Used for heartbeat tracking | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now handled in |
||
|
||
Status status = msg.getStatus(); | ||
|
||
// normal js message | ||
if (status == null) { | ||
trackPending(1, msg.consumeByteCount()); | ||
trackIncoming(1, msg.consumeByteCount(), null); | ||
return true; | ||
} | ||
|
||
// heartbeat just needed to be recorded | ||
if (status.isHeartbeat()) { | ||
trackIncoming(Integer.MIN_VALUE, -1, msg.subject); | ||
return false; | ||
} | ||
|
||
Headers h = msg.getHeaders(); | ||
int m = Integer.MIN_VALUE; | ||
long b = -1; | ||
if (h != null) { | ||
String s = h.getFirst(NATS_PENDING_MESSAGES); | ||
if (s != null) { | ||
try { | ||
int m = Integer.parseInt(s); | ||
long b = Long.parseLong(h.getFirst(NATS_PENDING_BYTES)); | ||
trackPending(m, b); | ||
m = Integer.parseInt(s); | ||
b = Long.parseLong(h.getFirst(NATS_PENDING_BYTES)); | ||
} | ||
catch (NumberFormatException ignore) { | ||
m = Integer.MIN_VALUE; // shouldn't happen but don't fail; make sure don't track m/b | ||
} | ||
catch (NumberFormatException ignore) {} // shouldn't happen but don't fail | ||
} | ||
} | ||
|
||
trackIncoming(m, b, msg.subject); | ||
return true; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was tired of not being able to get a JetStream context from a JetStreamManagement context.