/** * 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 org.apache.hadoop.yarn.server.timeline.recovery; import static org.apache.hadoop.yarn.server.timeline.util.LeveldbUtils.prefixMatches; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.security.token.delegation.DelegationKey; import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.proto.YarnServerCommonProtos; import org.apache.hadoop.yarn.security.client.TimelineDelegationTokenIdentifier; import org.apache.hadoop.yarn.server.records.Version; import org.apache.hadoop.yarn.server.records.impl.pb.VersionPBImpl; import org.apache.hadoop.yarn.server.timeline.recovery.records.TimelineDelegationTokenIdentifierData; import org.apache.hadoop.yarn.server.timeline.util.LeveldbUtils.KeyBuilder; import org.apache.hadoop.yarn.server.utils.LeveldbIterator; import org.fusesource.leveldbjni.JniDBFactory; import org.fusesource.leveldbjni.internal.NativeDB; import org.iq80.leveldb.DB; import org.iq80.leveldb.DBException; import org.iq80.leveldb.Options; import org.iq80.leveldb.WriteBatch; import static org.fusesource.leveldbjni.JniDBFactory.bytes; /** * A timeline service state storage implementation that supports any persistent * storage that adheres to the LevelDB interface. */ public class LeveldbTimelineStateStore extends TimelineStateStore { public static final Log LOG = LogFactory.getLog(LeveldbTimelineStateStore.class); private static final String DB_NAME = "timeline-state-store.ldb"; private static final FsPermission LEVELDB_DIR_UMASK = FsPermission .createImmutable((short) 0700); private static final byte[] TOKEN_ENTRY_PREFIX = bytes("t"); private static final byte[] TOKEN_MASTER_KEY_ENTRY_PREFIX = bytes("k"); private static final byte[] LATEST_SEQUENCE_NUMBER_KEY = bytes("s"); private static final Version CURRENT_VERSION_INFO = Version.newInstance(1, 0); private static final byte[] TIMELINE_STATE_STORE_VERSION_KEY = bytes("v"); private DB db; public LeveldbTimelineStateStore() { super(LeveldbTimelineStateStore.class.getName()); } @Override protected void initStorage(Configuration conf) throws IOException { } @Override protected void startStorage() throws IOException { Options options = new Options(); Path dbPath = new Path( getConfig().get( YarnConfiguration.TIMELINE_SERVICE_LEVELDB_STATE_STORE_PATH), DB_NAME); FileSystem localFS = null; try { localFS = FileSystem.getLocal(getConfig()); if (!localFS.exists(dbPath)) { if (!localFS.mkdirs(dbPath)) { throw new IOException("Couldn't create directory for leveldb " + "timeline store " + dbPath); } localFS.setPermission(dbPath, LEVELDB_DIR_UMASK); } } finally { IOUtils.cleanup(LOG, localFS); } JniDBFactory factory = new JniDBFactory(); try { options.createIfMissing(false); db = factory.open(new File(dbPath.toString()), options); LOG.info("Loading the existing database at th path: " + dbPath.toString()); checkVersion(); } catch (NativeDB.DBException e) { if (e.isNotFound() || e.getMessage().contains(" does not exist ")) { try { options.createIfMissing(true); db = factory.open(new File(dbPath.toString()), options); LOG.info("Creating a new database at th path: " + dbPath.toString()); storeVersion(CURRENT_VERSION_INFO); } catch (DBException ex) { throw new IOException(ex); } } else { throw new IOException(e); } } catch (DBException e) { throw new IOException(e); } } @Override protected void closeStorage() throws IOException { IOUtils.cleanup(LOG, db); } @Override public TimelineServiceState loadState() throws IOException { LOG.info("Loading timeline service state from leveldb"); TimelineServiceState state = new TimelineServiceState(); int numKeys = loadTokenMasterKeys(state); int numTokens = loadTokens(state); loadLatestSequenceNumber(state); LOG.info("Loaded " + numKeys + " master keys and " + numTokens + " tokens from leveldb, and latest sequence number is " + state.getLatestSequenceNumber()); return state; } @Override public void storeToken(TimelineDelegationTokenIdentifier tokenId, Long renewDate) throws IOException { DataOutputStream ds = null; WriteBatch batch = null; try { byte[] k = createTokenEntryKey(tokenId.getSequenceNumber()); if (db.get(k) != null) { throw new IOException(tokenId + " already exists"); } byte[] v = buildTokenData(tokenId, renewDate); ByteArrayOutputStream bs = new ByteArrayOutputStream(); ds = new DataOutputStream(bs); ds.writeInt(tokenId.getSequenceNumber()); batch = db.createWriteBatch(); batch.put(k, v); batch.put(LATEST_SEQUENCE_NUMBER_KEY, bs.toByteArray()); db.write(batch); } catch (DBException e) { throw new IOException(e); } finally { IOUtils.cleanup(LOG, ds); IOUtils.cleanup(LOG, batch); } } @Override public void updateToken(TimelineDelegationTokenIdentifier tokenId, Long renewDate) throws IOException { try { byte[] k = createTokenEntryKey(tokenId.getSequenceNumber()); if (db.get(k) == null) { throw new IOException(tokenId + " doesn't exist"); } byte[] v = buildTokenData(tokenId, renewDate); db.put(k, v); } catch (DBException e) { throw new IOException(e); } } @Override public void removeToken(TimelineDelegationTokenIdentifier tokenId) throws IOException { try { byte[] key = createTokenEntryKey(tokenId.getSequenceNumber()); db.delete(key); } catch (DBException e) { throw new IOException(e); } } @Override public void storeTokenMasterKey(DelegationKey key) throws IOException { try { byte[] k = createTokenMasterKeyEntryKey(key.getKeyId()); if (db.get(k) != null) { throw new IOException(key + " already exists"); } byte[] v = buildTokenMasterKeyData(key); db.put(k, v); } catch (DBException e) { throw new IOException(e); } } @Override public void removeTokenMasterKey(DelegationKey key) throws IOException { try { byte[] k = createTokenMasterKeyEntryKey(key.getKeyId()); db.delete(k); } catch (DBException e) { throw new IOException(e); } } private static byte[] buildTokenData( TimelineDelegationTokenIdentifier tokenId, Long renewDate) throws IOException { TimelineDelegationTokenIdentifierData data = new TimelineDelegationTokenIdentifierData(tokenId, renewDate); return data.toByteArray(); } private static byte[] buildTokenMasterKeyData(DelegationKey key) throws IOException { ByteArrayOutputStream memStream = new ByteArrayOutputStream(); DataOutputStream dataStream = new DataOutputStream(memStream); try { key.write(dataStream); dataStream.close(); } finally { IOUtils.cleanup(LOG, dataStream); } return memStream.toByteArray(); } private static void loadTokenMasterKeyData(TimelineServiceState state, byte[] keyData) throws IOException { DelegationKey key = new DelegationKey(); DataInputStream in = new DataInputStream(new ByteArrayInputStream(keyData)); try { key.readFields(in); } finally { IOUtils.cleanup(LOG, in); } state.tokenMasterKeyState.add(key); } private static void loadTokenData(TimelineServiceState state, byte[] tokenData) throws IOException { TimelineDelegationTokenIdentifierData data = new TimelineDelegationTokenIdentifierData(); DataInputStream in = new DataInputStream(new ByteArrayInputStream(tokenData)); try { data.readFields(in); } finally { IOUtils.cleanup(LOG, in); } state.tokenState.put(data.getTokenIdentifier(), data.getRenewDate()); } private int loadTokenMasterKeys(TimelineServiceState state) throws IOException { byte[] base = KeyBuilder.newInstance().add(TOKEN_MASTER_KEY_ENTRY_PREFIX) .getBytesForLookup(); int numKeys = 0; LeveldbIterator iterator = null; try { for (iterator = new LeveldbIterator(db), iterator.seek(base); iterator.hasNext(); iterator.next()) { byte[] k = iterator.peekNext().getKey(); if (!prefixMatches(base, base.length, k)) { break; } byte[] v = iterator.peekNext().getValue(); loadTokenMasterKeyData(state, v); ++numKeys; } } finally { IOUtils.cleanup(LOG, iterator); } return numKeys; } private int loadTokens(TimelineServiceState state) throws IOException { byte[] base = KeyBuilder.newInstance().add(TOKEN_ENTRY_PREFIX) .getBytesForLookup(); int numTokens = 0; LeveldbIterator iterator = null; try { for (iterator = new LeveldbIterator(db), iterator.seek(base); iterator.hasNext(); iterator.next()) { byte[] k = iterator.peekNext().getKey(); if (!prefixMatches(base, base.length, k)) { break; } byte[] v = iterator.peekNext().getValue(); loadTokenData(state, v); ++numTokens; } } catch (DBException e) { throw new IOException(e); } finally { IOUtils.cleanup(LOG, iterator); } return numTokens; } private void loadLatestSequenceNumber(TimelineServiceState state) throws IOException { byte[] data = null; try { data = db.get(LATEST_SEQUENCE_NUMBER_KEY); } catch (DBException e) { throw new IOException(e); } if (data != null) { DataInputStream in = new DataInputStream(new ByteArrayInputStream(data)); try { state.latestSequenceNumber = in.readInt(); } finally { IOUtils.cleanup(LOG, in); } } } /** * Creates a domain entity key with column name suffix, of the form * TOKEN_ENTRY_PREFIX + sequence number. */ private static byte[] createTokenEntryKey(int seqNum) throws IOException { return KeyBuilder.newInstance().add(TOKEN_ENTRY_PREFIX) .add(Integer.toString(seqNum)).getBytes(); } /** * Creates a domain entity key with column name suffix, of the form * TOKEN_MASTER_KEY_ENTRY_PREFIX + sequence number. */ private static byte[] createTokenMasterKeyEntryKey(int keyId) throws IOException { return KeyBuilder.newInstance().add(TOKEN_MASTER_KEY_ENTRY_PREFIX) .add(Integer.toString(keyId)).getBytes(); } @VisibleForTesting Version loadVersion() throws IOException { try { byte[] data = db.get(TIMELINE_STATE_STORE_VERSION_KEY); // if version is not stored previously, treat it as CURRENT_VERSION_INFO. if (data == null || data.length == 0) { return getCurrentVersion(); } Version version = new VersionPBImpl( YarnServerCommonProtos.VersionProto.parseFrom(data)); return version; } catch (DBException e) { throw new IOException(e); } } @VisibleForTesting void storeVersion(Version state) throws IOException { byte[] data = ((VersionPBImpl) state).getProto().toByteArray(); try { db.put(TIMELINE_STATE_STORE_VERSION_KEY, data); } catch (DBException e) { throw new IOException(e); } } @VisibleForTesting Version getCurrentVersion() { return CURRENT_VERSION_INFO; } /** * 1) Versioning timeline state store: * major.minor. For e.g. 1.0, 1.1, 1.2...1.25, 2.0 etc. * 2) Any incompatible change of TS-store is a major upgrade, and any * compatible change of TS-store is a minor upgrade. * 3) Within a minor upgrade, say 1.1 to 1.2: * overwrite the version info and proceed as normal. * 4) Within a major upgrade, say 1.2 to 2.0: * throw exception and indicate user to use a separate upgrade tool to * upgrade timeline store or remove incompatible old state. */ private void checkVersion() throws IOException { Version loadedVersion = loadVersion(); LOG.info("Loaded timeline state store version info " + loadedVersion); if (loadedVersion.equals(getCurrentVersion())) { return; } if (loadedVersion.isCompatibleTo(getCurrentVersion())) { LOG.info("Storing timeline state store version info " + getCurrentVersion()); storeVersion(CURRENT_VERSION_INFO); } else { String incompatibleMessage = "Incompatible version for timeline state store: expecting version " + getCurrentVersion() + ", but loading version " + loadedVersion; LOG.fatal(incompatibleMessage); throw new IOException(incompatibleMessage); } } }