/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.distributedlog; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.twitter.distributedlog.callback.ReadAheadCallback; import com.twitter.distributedlog.exceptions.DLInterruptedException; import com.twitter.distributedlog.exceptions.EndOfStreamException; import com.twitter.distributedlog.util.FutureUtils; import com.twitter.util.Future; import com.twitter.util.FutureEventListener; import com.twitter.util.Promise; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * Synchronous Log Reader based on {@link AsyncLogReader} */ class BKSyncLogReaderDLSN implements LogReader, Runnable, FutureEventListener<LogRecordWithDLSN>, ReadAheadCallback { private final BKAsyncLogReaderDLSN reader; private final ScheduledExecutorService executorService; private final LinkedBlockingQueue<LogRecordWithDLSN> readAheadRecords; private final AtomicReference<IOException> readerException = new AtomicReference<IOException>(null); private final int maxNumCachedRecords; private final int maxReadAheadWaitTime; private ReadAheadCallback readAheadCallback = null; private Promise<Void> closeFuture; private final Optional<Long> startTransactionId; private final DLSN startDLSN; private volatile DLSN lastSeenDLSN = DLSN.InvalidDLSN; // lock on variables that would be accessed by both background threads and foreground threads private final Object sharedLock = new Object(); BKSyncLogReaderDLSN(DistributedLogConfiguration conf, BKAsyncLogReaderDLSN reader, ScheduledExecutorService executorService, Optional<Long> startTransactionId) { this.maxNumCachedRecords = conf.getReadAheadMaxRecords(); this.maxReadAheadWaitTime = conf.getReadAheadWaitTime(); this.reader = reader; this.executorService = executorService; this.readAheadRecords = new LinkedBlockingQueue<LogRecordWithDLSN>(); this.startTransactionId = startTransactionId; this.startDLSN = reader.getStartDLSN(); scheduleReadNext(); } @VisibleForTesting BKAsyncLogReaderDLSN getAsyncReader() { return reader; } private void scheduleReadNext() { synchronized (sharedLock) { if (null != closeFuture) { return; } } this.executorService.submit(this); } private void invokeReadAheadCallback() { synchronized (sharedLock) { if (null != readAheadCallback) { readAheadCallback.resumeReadAhead(); readAheadCallback = null; } } } private void setReadAheadCallback(ReadAheadCallback callback) { synchronized (sharedLock) { this.readAheadCallback = callback; if (readAheadRecords.size() < maxNumCachedRecords) { invokeReadAheadCallback(); } } } // Background Read Future Listener @Override public void resumeReadAhead() { scheduleReadNext(); } @Override public void onSuccess(LogRecordWithDLSN record) { this.lastSeenDLSN = record.getDlsn(); if (!startTransactionId.isPresent() || record.getTransactionId() >= startTransactionId.get()) { readAheadRecords.add(record); } if (readAheadRecords.size() >= maxNumCachedRecords) { setReadAheadCallback(this); } else { scheduleReadNext(); } } @Override public void onFailure(Throwable cause) { if (cause instanceof IOException) { readerException.compareAndSet(null, (IOException) cause); } else { readerException.compareAndSet(null, new IOException("Encountered exception on reading " + reader.getStreamName() + " : ", cause)); } } // Background Read @Override public void run() { this.reader.readNext().addEventListener(this); } @Override public synchronized LogRecordWithDLSN readNext(boolean nonBlocking) throws IOException { if (null != readerException.get()) { throw readerException.get(); } LogRecordWithDLSN record = null; if (nonBlocking) { record = readAheadRecords.poll(); } else { try { // reader is still catching up, waiting for next record while (!reader.bkLedgerManager.isReadAheadCaughtUp() && null == readerException.get() && null == record) { record = readAheadRecords.poll(maxReadAheadWaitTime, TimeUnit.MILLISECONDS); } // reader caught up boolean shallWait = true; while (shallWait && reader.bkLedgerManager.isReadAheadCaughtUp() && null == record && null == readerException.get()) { record = readAheadRecords.poll(maxReadAheadWaitTime, TimeUnit.MILLISECONDS); if (null != record) { break; } DLSN lastDLSNSeenByReadAhead = reader.bkLedgerManager.readAheadCache.getLastReadAheadUserDLSN(); // if last seen DLSN by reader is same as the one seen by ReadAhead // that means that reader is caught up with ReadAhead and ReadAhead // is caught up with stream shallWait = DLSN.InitialDLSN != lastDLSNSeenByReadAhead && lastSeenDLSN.compareTo(lastDLSNSeenByReadAhead) < 0 && startDLSN.compareTo(lastDLSNSeenByReadAhead) <= 0; } } catch (InterruptedException e) { throw new DLInterruptedException("Interrupted on waiting next available log record for stream " + reader.getStreamName(), e); } } if (null != readerException.get()) { throw readerException.get(); } if (null != record) { if (record.isEndOfStream()) { EndOfStreamException eos = new EndOfStreamException("End of Stream Reached for " + reader.bkLedgerManager.getFullyQualifiedName()); readerException.compareAndSet(null, eos); throw eos; } invokeReadAheadCallback(); } return record; } @Override public synchronized List<LogRecordWithDLSN> readBulk(boolean nonBlocking, int numLogRecords) throws IOException { LinkedList<LogRecordWithDLSN> retList = new LinkedList<LogRecordWithDLSN>(); int numRead = 0; LogRecordWithDLSN record = readNext(nonBlocking); while ((null != record)) { retList.add(record); numRead++; if (numRead >= numLogRecords) { break; } record = readNext(nonBlocking); } return retList; } @Override public Future<Void> asyncClose() { Promise<Void> closePromise; synchronized (sharedLock) { if (null != closeFuture) { return closeFuture; } closeFuture = closePromise = new Promise<Void>(); } reader.asyncClose().proxyTo(closePromise); return closePromise; } @Override public void close() throws IOException { FutureUtils.result(asyncClose()); } // // Test Methods // @VisibleForTesting void disableReadAheadZKNotification() { reader.bkLedgerManager.disableReadAheadZKNotification(); } @VisibleForTesting LedgerReadPosition getReadAheadPosition() { if (null != reader.bkLedgerManager.readAheadWorker) { return reader.bkLedgerManager.readAheadWorker.getNextReadAheadPosition(); } return null; } }