• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wz2cool / local-queue / #28

31 Jan 2025 11:37AM UTC coverage: 91.871% (+0.4%) from 91.466%
#28

push

web-flow
Merge pull request #5 from wz2cool/0.1.1

0.1.1

92 of 96 new or added lines in 8 files covered. (95.83%)

486 of 529 relevant lines covered (91.87%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.74
/src/main/java/com/github/wz2cool/localqueue/impl/SimpleConsumer.java
1
package com.github.wz2cool.localqueue.impl;
2

3
import com.github.wz2cool.localqueue.IConsumer;
4
import com.github.wz2cool.localqueue.event.CloseListener;
5
import com.github.wz2cool.localqueue.model.config.SimpleConsumerConfig;
6
import com.github.wz2cool.localqueue.model.enums.ConsumeFromWhere;
7
import com.github.wz2cool.localqueue.model.message.InternalReadMessage;
8
import com.github.wz2cool.localqueue.model.message.QueueMessage;
9
import net.openhft.chronicle.queue.ChronicleQueue;
10
import net.openhft.chronicle.queue.ExcerptTailer;
11
import net.openhft.chronicle.queue.RollCycles;
12
import net.openhft.chronicle.queue.impl.single.SingleChronicleQueue;
13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15

16
import java.util.ArrayList;
17
import java.util.List;
18
import java.util.Objects;
19
import java.util.Optional;
20
import java.util.concurrent.*;
21
import java.util.concurrent.atomic.AtomicInteger;
22
import java.util.concurrent.locks.Lock;
23
import java.util.concurrent.locks.ReentrantLock;
24

25
/**
26
 * simple consumer
27
 *
28
 * @author frank
29
 */
30
public class SimpleConsumer implements IConsumer {
31

32
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
1✔
33
    private final SimpleConsumerConfig config;
34
    private final PositionStore positionStore;
35
    private final SingleChronicleQueue queue;
36
    // should only call by readCacheExecutor
37
    private final ExcerptTailer mainTailer;
38
    private final ExecutorService readCacheExecutor = Executors.newSingleThreadExecutor();
1✔
39
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
1✔
40
    private final LinkedBlockingQueue<QueueMessage> messageCache;
41
    private final ConcurrentLinkedQueue<CloseListener> closeListenerList = new ConcurrentLinkedQueue<>();
1✔
42
    private volatile long ackedReadPosition = -1;
1✔
43
    private volatile boolean isReadToCacheRunning = true;
1✔
44
    private volatile boolean isClosing = false;
1✔
45
    private volatile boolean isClosed = false;
1✔
46
    private final AtomicInteger positionVersion = new AtomicInteger(0);
1✔
47
    private final Lock internalLock = new ReentrantLock();
1✔
48

49
    /**
50
     * constructor
51
     *
52
     * @param config the config of consumer
53
     */
54
    public SimpleConsumer(final SimpleConsumerConfig config) {
1✔
55
        this.config = config;
1✔
56
        this.messageCache = new LinkedBlockingQueue<>(config.getCacheSize());
1✔
57
        this.positionStore = new PositionStore(config.getPositionFile());
1✔
58
        this.queue = ChronicleQueue.singleBuilder(config.getDataDir()).rollCycle(RollCycles.FAST_DAILY).build();
1✔
59
        this.mainTailer = initMainTailer();
1✔
60
        startReadToCache();
1✔
61
        scheduler.scheduleAtFixedRate(this::flushPosition, 0, config.getFlushPositionInterval(), TimeUnit.MILLISECONDS);
1✔
62
    }
1✔
63

64
    @Override
65
    public synchronized QueueMessage take() throws InterruptedException {
66
        return this.messageCache.take();
1✔
67
    }
68

69
    @Override
70
    public synchronized List<QueueMessage> batchTake(int maxBatchSize) throws InterruptedException {
71
        List<QueueMessage> result = new ArrayList<>(maxBatchSize);
1✔
72
        QueueMessage take = this.messageCache.take();
1✔
73
        result.add(take);
1✔
74
        this.messageCache.drainTo(result, maxBatchSize - 1);
1✔
75
        return result;
1✔
76
    }
77

78
    @Override
79
    public synchronized Optional<QueueMessage> take(long timeout, TimeUnit unit) throws InterruptedException {
80
        QueueMessage message = this.messageCache.poll(timeout, unit);
1✔
81
        return Optional.ofNullable(message);
1✔
82
    }
83

84
    @Override
85
    public synchronized List<QueueMessage> batchTake(int maxBatchSize, long timeout, TimeUnit unit) throws InterruptedException {
86
        List<QueueMessage> result = new ArrayList<>(maxBatchSize);
1✔
87
        QueueMessage poll = this.messageCache.poll(timeout, unit);
1✔
88
        if (Objects.nonNull(poll)) {
1✔
89
            result.add(poll);
1✔
90
            this.messageCache.drainTo(result, maxBatchSize - 1);
1✔
91
        }
92
        return result;
1✔
93
    }
94

95
    @Override
96
    public synchronized Optional<QueueMessage> poll() {
97
        QueueMessage message = this.messageCache.poll();
1✔
98
        return Optional.ofNullable(message);
1✔
99
    }
100

101
    @Override
102
    public synchronized List<QueueMessage> batchPoll(int maxBatchSize) {
103
        List<QueueMessage> result = new ArrayList<>(maxBatchSize);
1✔
104
        this.messageCache.drainTo(result, maxBatchSize);
1✔
105
        return result;
1✔
106
    }
107

108
    @Override
109
    public synchronized void ack(final QueueMessage message) {
110
        if (Objects.isNull(message)) {
1✔
111
            return;
1✔
112
        }
113

114
        if (message.getPositionVersion() != positionVersion.get()) {
1✔
115
            return;
×
116
        }
117

118
        this.ackedReadPosition = message.getPosition();
1✔
119
    }
1✔
120

121
    @Override
122
    public synchronized void ack(final List<QueueMessage> messages) {
123
        if (Objects.isNull(messages) || messages.isEmpty()) {
1✔
124
            return;
1✔
125
        }
126
        QueueMessage lastOne = messages.get(messages.size() - 1);
1✔
127
        if (lastOne.getPositionVersion() != positionVersion.get()) {
1✔
128
            return;
×
129
        }
130
        this.ackedReadPosition = lastOne.getPosition();
1✔
131
    }
1✔
132

133
    @Override
134
    public boolean moveToPosition(final long position) {
135
        logDebug("[moveToPosition] start");
1✔
136
        stopReadToCache();
1✔
137
        try {
138
            internalLock.lock();
1✔
139
            return moveToPositionInternal(position);
1✔
140
        } finally {
141
            internalLock.unlock();
1✔
142
            startReadToCache();
1✔
143
            logDebug("[moveToPosition] end");
1✔
144
        }
×
145
    }
146

147
    @Override
148
    public boolean moveToTimestamp(final long timestamp) {
149
        logDebug("[moveToTimestamp] start, timestamp: {}", timestamp);
1✔
150
        stopReadToCache();
1✔
151
        try {
152
            internalLock.lock();
1✔
153
            Optional<Long> positionOptional = findPosition(timestamp);
1✔
154
            if (!positionOptional.isPresent()) {
1✔
155
                return false;
1✔
156
            }
157
            Long position = positionOptional.get();
1✔
158
            return moveToPositionInternal(position);
1✔
159
        } finally {
160
            internalLock.unlock();
1✔
161
            startReadToCache();
1✔
162
            logDebug("[moveToTimestamp] end");
1✔
163
        }
×
164
    }
165

166
    @Override
167
    public Optional<QueueMessage> get(final String messageKey) {
168
        return get(messageKey, 0L, Long.MAX_VALUE);
1✔
169
    }
170

171
    @Override
172
    public Optional<QueueMessage> get(final String messageKey, long searchTimestampStart, long searchTimestampEnd) {
173
        if (messageKey == null || messageKey.isEmpty()) {
1✔
174
            return Optional.empty();
1✔
175
        }
176
        try (ExcerptTailer tailer = queue.createTailer()) {
1✔
177
            while (true) {
178
                // for performance, ignore read content.
179
                InternalReadMessage internalReadMessage = new InternalReadMessage(true);
1✔
180
                boolean readResult = tailer.readBytes(internalReadMessage);
1✔
181
                if (!readResult) {
1✔
182
                    return Optional.empty();
1✔
183
                }
184
                if (internalReadMessage.getWriteTime() < searchTimestampStart) {
1✔
185
                    continue;
1✔
186
                }
187
                if (internalReadMessage.getWriteTime() > searchTimestampEnd) {
1✔
NEW
188
                    return Optional.empty();
×
189
                }
190
                boolean moveToResult = tailer.moveToIndex(tailer.lastReadIndex());
1✔
191
                if (!moveToResult) {
1✔
NEW
192
                    return Optional.empty();
×
193
                }
194
                internalReadMessage = new InternalReadMessage();
1✔
195
                readResult = tailer.readBytes(internalReadMessage);
1✔
196
                if (!readResult) {
1✔
NEW
197
                    return Optional.empty();
×
198
                }
199
                QueueMessage queueMessage = toQueueMessage(internalReadMessage, tailer.lastReadIndex());
1✔
200
                if (Objects.equals(messageKey, queueMessage.getMessageKey())) {
1✔
201
                    return Optional.of(queueMessage);
1✔
202
                }
203
            }
1✔
204
        }
1✔
205
    }
206

207
    private QueueMessage toQueueMessage(final InternalReadMessage internalReadMessage, final long position) {
208
        return new QueueMessage(
1✔
209
                internalReadMessage.getMessageKey(),
1✔
210
                positionVersion.get(),
1✔
211
                position,
212
                internalReadMessage.getContent(),
1✔
213
                internalReadMessage.getWriteTime());
1✔
214
    }
215

216
    private boolean moveToPositionInternal(final long position) {
217
        return CompletableFuture.supplyAsync(() -> {
1✔
218
            try {
219
                logDebug("[moveToPositionInternal] start, position: {}", position);
1✔
220
                boolean moveToResult = mainTailer.moveToIndex(position);
1✔
221
                if (moveToResult) {
1✔
222
                    positionVersion.incrementAndGet();
1✔
223
                    messageCache.clear();
1✔
224
                    this.ackedReadPosition = position;
1✔
225
                }
226
                return moveToResult;
1✔
227
            } finally {
228
                logDebug("[moveToPositionInternal] end");
1✔
229
            }
×
230
        }, this.readCacheExecutor).join();
1✔
231
    }
232

233
    private Optional<Long> findPosition(final long timestamp) {
234
        logDebug("[findPosition] start, timestamp: {}", timestamp);
1✔
235
        try (ExcerptTailer excerptTailer = queue.createTailer()) {
1✔
236
            while (true) {
237
                InternalReadMessage internalReadMessage = new InternalReadMessage(true);
1✔
238
                boolean resultResult = excerptTailer.readBytes(internalReadMessage);
1✔
239
                if (resultResult) {
1✔
240
                    if (internalReadMessage.getWriteTime() >= timestamp) {
1✔
241
                        return Optional.of(excerptTailer.lastReadIndex());
1✔
242
                    }
243
                } else {
244
                    return Optional.empty();
1✔
245
                }
246
            }
1✔
247
        } finally {
1✔
248
            logDebug("[findPosition] end");
1✔
249
        }
×
250
    }
251

252
    public long getAckedReadPosition() {
253
        return ackedReadPosition;
1✔
254
    }
255

256
    public boolean isClosed() {
257
        return isClosed;
1✔
258
    }
259

260
    private void stopReadToCache() {
261
        this.isReadToCacheRunning = false;
1✔
262
    }
1✔
263

264
    private void startReadToCache() {
265
        this.isReadToCacheRunning = true;
1✔
266
        readCacheExecutor.execute(this::readToCache);
1✔
267
    }
1✔
268

269
    private void readToCache() {
270
        try {
271
            logDebug("[readToCache] start");
1✔
272
            internalLock.lock();
1✔
273
            long pullInterval = config.getPullInterval();
1✔
274
            long fillCacheInterval = config.getFillCacheInterval();
1✔
275
            while (this.isReadToCacheRunning && !isClosing) {
1✔
276
                try {
277
                    InternalReadMessage internalReadMessage = new InternalReadMessage();
1✔
278
                    boolean readResult = mainTailer.readBytes(internalReadMessage);
1✔
279
                    if (!readResult) {
1✔
280
                        TimeUnit.MILLISECONDS.sleep(pullInterval);
1✔
281
                        continue;
1✔
282
                    }
283
                    long lastedReadIndex = mainTailer.lastReadIndex();
1✔
284
                    QueueMessage queueMessage = toQueueMessage(internalReadMessage, lastedReadIndex);
1✔
285
                    boolean offerResult = this.messageCache.offer(queueMessage, fillCacheInterval, TimeUnit.MILLISECONDS);
1✔
286
                    if (!offerResult) {
1✔
287
                        // if offer failed, move to last read position
288
                        mainTailer.moveToIndex(lastedReadIndex);
1✔
289
                    }
290
                } catch (InterruptedException e) {
×
291
                    Thread.currentThread().interrupt();
×
292
                }
1✔
293
            }
294
        } finally {
295
            internalLock.unlock();
1✔
296
            logDebug("[readToCache] end");
1✔
297
        }
1✔
298
    }
1✔
299

300
    private ExcerptTailer initMainTailer() {
301
        return CompletableFuture.supplyAsync(this::initMainTailerInternal, this.readCacheExecutor).join();
1✔
302
    }
303

304
    private ExcerptTailer initMainTailerInternal() {
305
        try {
306
            logDebug("[initExcerptTailerInternal] start");
1✔
307
            ExcerptTailer tailer = queue.createTailer();
1✔
308
            Optional<Long> lastPositionOptional = getLastPosition();
1✔
309
            if (lastPositionOptional.isPresent()) {
1✔
310
                Long position = lastPositionOptional.get();
1✔
311
                long beginPosition = position + 1;
1✔
312
                tailer.moveToIndex(beginPosition);
1✔
313
                logDebug("[initExcerptTailerInternal] find last position and move to position: {}", beginPosition);
1✔
314
            } else {
1✔
315
                ConsumeFromWhere consumeFromWhere = this.config.getConsumeFromWhere();
1✔
316
                if (consumeFromWhere == ConsumeFromWhere.LAST) {
1✔
317
                    tailer.toEnd();
1✔
318
                    logDebug("[initExcerptTailerInternal] move to end");
1✔
319
                } else if (consumeFromWhere == ConsumeFromWhere.FIRST) {
1✔
320
                    tailer.toStart();
1✔
321
                    logDebug("[initExcerptTailerInternal] move to start");
1✔
322
                }
323
            }
324
            return tailer;
1✔
325
        } finally {
326
            logDebug("[initExcerptTailer] end");
1✔
327
        }
×
328

329
    }
330

331
    /// region position
332

333
    private void flushPosition() {
334
        if (ackedReadPosition != -1) {
1✔
335
            setLastPosition(this.ackedReadPosition);
1✔
336
        }
337
    }
1✔
338

339
    private Optional<Long> getLastPosition() {
340
        Long position = positionStore.get(config.getConsumerId());
1✔
341
        if (position == null) {
1✔
342
            return Optional.empty();
1✔
343
        }
344
        return Optional.of(position);
1✔
345
    }
346

347
    private void setLastPosition(long position) {
348
        positionStore.put(config.getConsumerId(), position);
1✔
349
    }
1✔
350

351
    /// endregion
352

353
    @Override
354
    public void close() {
355
        logDebug("[close] start");
1✔
356
        try {
357
            isClosing = true;
1✔
358
            this.internalLock.lock();
1✔
359
            stopReadToCache();
1✔
360
            this.internalLock.unlock();
1✔
361
            if (!positionStore.isClosed()) {
1✔
362
                positionStore.close();
1✔
363
            }
364
            scheduler.shutdown();
1✔
365
            readCacheExecutor.shutdown();
1✔
366
            try {
367
                if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
1✔
368
                    scheduler.shutdownNow();
×
369
                }
370
                if (!readCacheExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
1✔
371
                    readCacheExecutor.shutdownNow();
×
372
                }
373
            } catch (InterruptedException e) {
×
374
                scheduler.shutdownNow();
×
375
                readCacheExecutor.shutdownNow();
×
376
                Thread.currentThread().interrupt();
×
377
            }
1✔
378
            if (!queue.isClosed()) {
1✔
379
                queue.close();
1✔
380
            }
381

382
            for (CloseListener closeListener : closeListenerList) {
1✔
383
                closeListener.onClose();
1✔
384
            }
1✔
385
            isClosed = true;
1✔
386
        } finally {
387
            logDebug("[close] end");
1✔
388
        }
1✔
389
    }
1✔
390

391
    @Override
392
    public void addCloseListener(CloseListener listener) {
393
        closeListenerList.add(listener);
1✔
394
    }
1✔
395

396
    private void logDebug(String format) {
397
        if (logger.isDebugEnabled()) {
1✔
398
            logger.debug(format);
×
399
        }
400
    }
1✔
401

402
    private void logDebug(String format, Object arg) {
403
        if (logger.isDebugEnabled()) {
1✔
404
            logger.debug(format, arg);
×
405
        }
406
    }
1✔
407
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc