/** * 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.base.Preconditions; import com.google.common.base.Stopwatch; import com.twitter.distributedlog.util.FutureUtils; import com.twitter.util.Future; import com.twitter.util.FutureEventListener; import com.twitter.util.Promise; import org.apache.bookkeeper.client.AsyncCallback; import org.apache.bookkeeper.client.BKException; import org.apache.bookkeeper.client.BookKeeper; import org.apache.bookkeeper.client.LedgerEntry; import org.apache.bookkeeper.client.LedgerHandle; import org.apache.bookkeeper.stats.NullStatsLogger; import org.apache.bookkeeper.stats.OpStatsLogger; import org.apache.bookkeeper.stats.StatsLogger; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import static com.google.common.base.Charsets.UTF_8; /** * A central place on managing open ledgers. */ public class LedgerHandleCache { static final Logger LOG = LoggerFactory.getLogger(LedgerHandleCache.class); public static Builder newBuilder() { return new Builder(); } public static class Builder { private BookKeeperClient bkc; private String digestpw; private StatsLogger statsLogger = NullStatsLogger.INSTANCE; private Builder() {} public Builder bkc(BookKeeperClient bkc) { this.bkc = bkc; return this; } public Builder conf(DistributedLogConfiguration conf) { this.digestpw = conf.getBKDigestPW(); return this; } public Builder statsLogger(StatsLogger statsLogger) { this.statsLogger = statsLogger; return this; } public LedgerHandleCache build() { Preconditions.checkNotNull(bkc, "No bookkeeper client is provided"); Preconditions.checkNotNull(digestpw, "No bookkeeper digest password is provided"); Preconditions.checkNotNull(statsLogger, "No stats logger is provided"); return new LedgerHandleCache(bkc, digestpw, statsLogger); } } final ConcurrentHashMap<LedgerDescriptor, RefCountedLedgerHandle> handlesMap = new ConcurrentHashMap<LedgerDescriptor, RefCountedLedgerHandle>(); private final BookKeeperClient bkc; private final String digestpw; private final OpStatsLogger openStats; private final OpStatsLogger openNoRecoveryStats; private LedgerHandleCache(BookKeeperClient bkc, String digestpw, StatsLogger statsLogger) { this.bkc = bkc; this.digestpw = digestpw; // Stats openStats = statsLogger.getOpStatsLogger("open_ledger"); openNoRecoveryStats = statsLogger.getOpStatsLogger("open_ledger_no_recovery"); } /** * Open the given ledger <i>ledgerDesc</i>. * * @param ledgerDesc * ledger description * @param callback * open callback. * @param ctx * callback context */ private void asyncOpenLedger(LedgerDescriptor ledgerDesc, AsyncCallback.OpenCallback callback, Object ctx) { try { if (!ledgerDesc.isFenced()) { bkc.get().asyncOpenLedgerNoRecovery(ledgerDesc.getLedgerId(), BookKeeper.DigestType.CRC32, digestpw.getBytes(UTF_8), callback, ctx); } else { bkc.get().asyncOpenLedger(ledgerDesc.getLedgerId(), BookKeeper.DigestType.CRC32, digestpw.getBytes(UTF_8), callback, ctx); } } catch (IOException ace) { // :) when we can't get bkc, it means bookie handle not available callback.openComplete(BKException.Code.BookieHandleNotAvailableException, null, ctx); } } /** * Open the log segment. * * @param metadata * the log segment metadata * @param fence * whether to fence the log segment during open * @return a future presenting the open result. */ public Future<LedgerDescriptor> asyncOpenLedger(LogSegmentMetadata metadata, boolean fence) { final Stopwatch stopwatch = Stopwatch.createStarted(); final OpStatsLogger openStatsLogger = fence ? openStats : openNoRecoveryStats; final Promise<LedgerDescriptor> promise = new Promise<LedgerDescriptor>(); final LedgerDescriptor ledgerDesc = new LedgerDescriptor(metadata.getLedgerId(), metadata.getLogSegmentSequenceNumber(), fence); RefCountedLedgerHandle refhandle = handlesMap.get(ledgerDesc); if (null == refhandle) { asyncOpenLedger(ledgerDesc, new AsyncCallback.OpenCallback() { @Override public void openComplete(int rc, LedgerHandle lh, Object ctx) { if (BKException.Code.OK != rc) { promise.setException(BKException.create(rc)); return; } RefCountedLedgerHandle newRefHandle = new RefCountedLedgerHandle(lh); RefCountedLedgerHandle oldRefHandle = handlesMap.putIfAbsent(ledgerDesc, newRefHandle); if (null != oldRefHandle) { oldRefHandle.addRef(); if (newRefHandle.removeRef()) { newRefHandle.handle.asyncClose(new AsyncCallback.CloseCallback() { @Override public void closeComplete(int i, LedgerHandle ledgerHandle, Object o) { // No action necessary } }, null); } } promise.setValue(ledgerDesc); } }, null); } else { refhandle.addRef(); promise.setValue(ledgerDesc); } return promise.addEventListener(new FutureEventListener<LedgerDescriptor>() { @Override public void onSuccess(LedgerDescriptor value) { openStatsLogger.registerSuccessfulEvent(stopwatch.elapsed(TimeUnit.MICROSECONDS)); } @Override public void onFailure(Throwable cause) { openStatsLogger.registerFailedEvent(stopwatch.elapsed(TimeUnit.MICROSECONDS)); } }); } /** * Open a ledger synchronously. * * @param metadata * log segment metadata * @param fence * whether to fence the log segment during open * @return ledger descriptor * @throws BKException */ public LedgerDescriptor openLedger(LogSegmentMetadata metadata, boolean fence) throws BKException { return FutureUtils.bkResult(asyncOpenLedger(metadata, fence)); } private RefCountedLedgerHandle getLedgerHandle(LedgerDescriptor ledgerDescriptor) { return null == ledgerDescriptor ? null : handlesMap.get(ledgerDescriptor); } /** * Close the ledger asynchronously. * * @param ledgerDesc * ledger descriptor. * @return future presenting the closing result. */ public Future<Void> asyncCloseLedger(LedgerDescriptor ledgerDesc) { final Promise<Void> promise = new Promise<Void>(); RefCountedLedgerHandle refhandle = getLedgerHandle(ledgerDesc); if ((null != refhandle) && (refhandle.removeRef())) { refhandle = handlesMap.remove(ledgerDesc); if (refhandle.getRefCount() > 0) { // In the rare race condition that a ref count was added immediately // after the close de-refed it and the remove was called // Try to put the handle back in the map handlesMap.putIfAbsent(ledgerDesc, refhandle); // ReadOnlyLedgerHandles don't have much overhead, so lets just leave // the handle open even if it had already been replaced promise.setValue(null); } else { refhandle.handle.asyncClose(new AsyncCallback.CloseCallback() { @Override public void closeComplete(int rc, LedgerHandle ledgerHandle, Object ctx) { if (BKException.Code.OK == rc) { promise.setValue(null); } else { promise.setException(BKException.create(rc)); } } }, null); } } else { promise.setValue(null); } return promise; } /** * Close the ledger synchronously. * * @param ledgerDesc * ledger descriptor. * @throws BKException */ public void closeLedger(LedgerDescriptor ledgerDesc) throws BKException { FutureUtils.bkResult(asyncCloseLedger(ledgerDesc)); } /** * Get the last add confirmed of <code>ledgerDesc</code>. * * @param ledgerDesc * ledger descriptor. * @return last add confirmed of <code>ledgerDesc</code> * @throws BKException */ public long getLastAddConfirmed(LedgerDescriptor ledgerDesc) throws BKException { RefCountedLedgerHandle refhandle = getLedgerHandle(ledgerDesc); if (null == refhandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); throw BKException.create(BKException.Code.UnexpectedConditionException); } return refhandle.handle.getLastAddConfirmed(); } /** * Whether a ledger is closed or not. * * @param ledgerDesc * ledger descriptor. * @return true if a ledger is closed, otherwise false. * @throws BKException */ public boolean isLedgerHandleClosed(LedgerDescriptor ledgerDesc) throws BKException { RefCountedLedgerHandle refhandle = getLedgerHandle(ledgerDesc); if (null == refhandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); throw BKException.create(BKException.Code.UnexpectedConditionException); } return refhandle.handle.isClosed(); } /** * Async try read last confirmed. * * @param ledgerDesc * ledger descriptor * @return future presenting read last confirmed result. */ public Future<Long> asyncTryReadLastConfirmed(LedgerDescriptor ledgerDesc) { RefCountedLedgerHandle refHandle = handlesMap.get(ledgerDesc); if (null == refHandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); return Future.exception(BKException.create(BKException.Code.UnexpectedConditionException)); } final Promise<Long> promise = new Promise<Long>(); refHandle.handle.asyncTryReadLastConfirmed(new AsyncCallback.ReadLastConfirmedCallback() { @Override public void readLastConfirmedComplete(int rc, long lastAddConfirmed, Object ctx) { if (BKException.Code.OK == rc) { promise.setValue(lastAddConfirmed); } else { promise.setException(BKException.create(rc)); } } }, null); return promise; } /** * Try read last confirmed. * * @param ledgerDesc * ledger descriptor * @return last confirmed * @throws BKException */ public long tryReadLastConfirmed(LedgerDescriptor ledgerDesc) throws BKException { return FutureUtils.bkResult(asyncTryReadLastConfirmed(ledgerDesc)); } /** * Async read last confirmed and entry * * @param ledgerDesc * ledger descriptor * @param entryId * entry id to read * @param timeOutInMillis * time out if no newer entry available * @param parallel * whether to read from replicas in parallel */ public Future<Pair<Long, LedgerEntry>> asyncReadLastConfirmedAndEntry( LedgerDescriptor ledgerDesc, long entryId, long timeOutInMillis, boolean parallel) { RefCountedLedgerHandle refHandle = handlesMap.get(ledgerDesc); if (null == refHandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); return Future.exception(BKException.create(BKException.Code.UnexpectedConditionException)); } final Promise<Pair<Long, LedgerEntry>> promise = new Promise<Pair<Long, LedgerEntry>>(); refHandle.handle.asyncReadLastConfirmedAndEntry(entryId, timeOutInMillis, parallel, new AsyncCallback.ReadLastConfirmedAndEntryCallback() { @Override public void readLastConfirmedAndEntryComplete(int rc, long lac, LedgerEntry ledgerEntry, Object ctx) { if (BKException.Code.OK == rc) { promise.setValue(Pair.of(lac, ledgerEntry)); } else { promise.setException(BKException.create(rc)); } } }, null); return promise; } /** * Async Read Entries * * @param ledgerDesc * ledger descriptor * @param first * first entry * @param last * second entry */ public Future<Enumeration<LedgerEntry>> asyncReadEntries( LedgerDescriptor ledgerDesc, long first, long last) { RefCountedLedgerHandle refHandle = handlesMap.get(ledgerDesc); if (null == refHandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); return Future.exception(BKException.create(BKException.Code.UnexpectedConditionException)); } final Promise<Enumeration<LedgerEntry>> promise = new Promise<Enumeration<LedgerEntry>>(); refHandle.handle.asyncReadEntries(first, last, new AsyncCallback.ReadCallback() { @Override public void readComplete(int rc, LedgerHandle lh, Enumeration<LedgerEntry> entries, Object ctx) { if (BKException.Code.OK == rc) { promise.setValue(entries); } else { promise.setException(BKException.create(rc)); } } }, null); return promise; } public Enumeration<LedgerEntry> readEntries(LedgerDescriptor ledgerDesc, long first, long last) throws BKException { return FutureUtils.bkResult(asyncReadEntries(ledgerDesc, first, last)); } public long getLength(LedgerDescriptor ledgerDesc) throws BKException { RefCountedLedgerHandle refhandle = getLedgerHandle(ledgerDesc); if (null == refhandle) { LOG.error("Accessing ledger {} without opening.", ledgerDesc); throw BKException.create(BKException.Code.UnexpectedConditionException); } return refhandle.handle.getLength(); } public void clear() { if (null != handlesMap) { Iterator<Map.Entry<LedgerDescriptor, RefCountedLedgerHandle>> handlesMapIter = handlesMap.entrySet().iterator(); while (handlesMapIter.hasNext()) { Map.Entry<LedgerDescriptor, RefCountedLedgerHandle> entry = handlesMapIter.next(); // Make it inaccessible through the map handlesMapIter.remove(); // now close the ledger entry.getValue().forceClose(); } } } static class RefCountedLedgerHandle { public final LedgerHandle handle; final AtomicLong refcount = new AtomicLong(0); RefCountedLedgerHandle(LedgerHandle lh) { this.handle = lh; addRef(); } long getRefCount() { return refcount.get(); } public void addRef() { refcount.incrementAndGet(); } public boolean removeRef() { return (refcount.decrementAndGet() == 0); } public void forceClose() { try { handle.close(); } catch (BKException.BKLedgerClosedException exc) { // Ignore } catch (Exception exc) { LOG.warn("Exception while closing ledger {}", handle, exc); } } } }