Code review comment: There is no connection between this thread and the task thread. Interrupts on the task thread by Flink won't interrupt the fetcher thread. This change is sufficient to stop it.   Generally, it would be smart if Flink itself interrupted not the thread but the thread group of the Flink task. Since all spawned threads are by default in the parent thread group this would automatically interrupt all of them.

Code before review:
protected void emitRecords() {
AsyncRecordQueue<T> min = heads.poll();
runLoop:
while (running) {
while (min == null) {
if (!emptyQueues.isEmpty()) {
for (AsyncRecordQueue<T> queueHead : emptyQueues.keySet()) {
if (!queueHead.queue.isEmpty()) {
emptyQueues.remove(queueHead);
queueHead.headTimestamp = queueHead.queue.peek().getTimestamp();
heads.offer(queueHead);
}
}
}
min = heads.poll();
if (min == null) {
synchronized (condition) {
try {
condition.wait(idleSleepMillis);
} catch (InterruptedException e) {
continue runLoop;
}
}
}
if (!running) {
break runLoop;
}
}
while (min.headTimestamp > maxEmitTimestamp) {
synchronized (condition) {
try {
condition.wait(idleSleepMillis);
} catch (InterruptedException e) {
continue runLoop;
}
if (min.headTimestamp > maxEmitTimestamp && !emptyQueues.isEmpty()) {
heads.offer(min);
min = null;
continue runLoop;
}
}
if (!running) {
break runLoop;
}
}
AsyncRecordQueue<T> nextQueue = heads.poll();
T record;
int emitCount = 0;
while ((record = min.queue.poll()) != null) {
emit(record, min);
min.headTimestamp = record.getTimestamp();
if ((nextQueue != null && min.headTimestamp > nextQueue.headTimestamp)
|| (min.headTimestamp > maxEmitTimestamp)
|| (emitCount++ > queueCapacity && !emptyQueues.isEmpty())) {
break;
}
}
if (record == null) {
this.emptyQueues.put(min, true);
} else if (nextQueue != null && nextQueue.headTimestamp > min.headTimestamp) {
heads.offer(nextQueue);
nextQueue = min;
} else {
heads.offer(min);
}
min = nextQueue;
}
}

Line under review: if (!running) {

Code after addressing the review:
protected void emitRecords() {
AsyncRecordQueue<T> min = heads.poll();
runLoop:
while (running) {
while (min == null) {
if (!emptyQueues.isEmpty()) {
for (AsyncRecordQueue<T> queueHead : emptyQueues.keySet()) {
if (!queueHead.queue.isEmpty()) {
emptyQueues.remove(queueHead);
queueHead.headTimestamp = queueHead.queue.peek().getTimestamp();
heads.offer(queueHead);
}
}
}
min = heads.poll();
if (min == null) {
synchronized (condition) {
try {
condition.wait(idleSleepMillis);
} catch (InterruptedException e) {
continue runLoop;
}
}
}
if (!running) {
break runLoop;
}
}
while (min.headTimestamp > maxEmitTimestamp) {
synchronized (condition) {
try {
condition.wait(idleSleepMillis);
} catch (InterruptedException e) {
continue runLoop;
}
if (min.headTimestamp > maxEmitTimestamp && !emptyQueues.isEmpty()) {
heads.offer(min);
min = null;
continue runLoop;
}
}
if (!running) {
break runLoop;
}
}
AsyncRecordQueue<T> nextQueue = heads.poll();
T record;
int emitCount = 0;
while ((record = min.queue.poll()) != null) {
emit(record, min);
min.headTimestamp = record.getTimestamp();
if ((nextQueue != null && min.headTimestamp > nextQueue.headTimestamp)
|| (min.headTimestamp > maxEmitTimestamp)
|| (emitCount++ > queueCapacity && !emptyQueues.isEmpty())) {
break;
}
}
if (record == null) {
this.emptyQueues.put(min, true);
} else if (nextQueue != null && nextQueue.headTimestamp > min.headTimestamp) {
heads.offer(nextQueue);
nextQueue = min;
} else {
heads.offer(min);
}
min = nextQueue;
}
}