/* * Copyright © 2014 Cask Data, Inc. * * Licensed 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 co.cask.cdap.data2.transaction.stream; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.data.stream.StreamFileOffset; import co.cask.cdap.data.stream.StreamUtils; import co.cask.cdap.proto.Id; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.SortedMap; /** * Represents storage for {@link ConsumerState} for stream consumers. */ public abstract class StreamConsumerStateStore implements ConsumerStateStore<StreamConsumerState, Iterable<StreamFileOffset>> { protected final StreamConfig streamConfig; protected final Id.Stream streamId; protected StreamConsumerStateStore(StreamConfig streamConfig) { this.streamConfig = streamConfig; this.streamId = streamConfig.getStreamId(); } @Override public final void getAll(Collection<? super StreamConsumerState> result) throws IOException { SortedMap<byte[], byte[]> states = Maps.newTreeMap(Bytes.BYTES_COMPARATOR); fetchAll(streamId.toBytes(), states); for (Map.Entry<byte[], byte[]> entry : states.entrySet()) { byte[] column = entry.getKey(); byte[] value = entry.getValue(); if (value != null) { result.add(new StreamConsumerState(getGroupId(column), getInstanceId(column), decodeOffsets(value))); } } } @Override public final void getByGroup(long groupId, Collection<? super StreamConsumerState> result) throws IOException { SortedMap<byte[], byte[]> states = Maps.newTreeMap(Bytes.BYTES_COMPARATOR); fetchAll(streamId.toBytes(), Bytes.toBytes(groupId), states); for (Map.Entry<byte[], byte[]> entry : states.entrySet()) { byte[] column = entry.getKey(); if (getGroupId(column) != groupId) { continue; } byte[] value = entry.getValue(); if (value != null) { result.add(new StreamConsumerState(groupId, getInstanceId(column), decodeOffsets(value))); } } } @Override public final StreamConsumerState get(long groupId, int instanceId) throws IOException { byte[] value = fetch(streamId.toBytes(), getColumn(groupId, instanceId)); return value == null ? null : new StreamConsumerState(groupId, instanceId, decodeOffsets(value)); } @Override public final void save(StreamConsumerState state) throws IOException { store(streamId.toBytes(), getColumn(state.getGroupId(), state.getInstanceId()), encodeOffsets(state.getState())); } @Override public final void save(Iterable<? extends StreamConsumerState> states) throws IOException { ImmutableSortedMap.Builder<byte[], byte[]> values = ImmutableSortedMap.orderedBy(Bytes.BYTES_COMPARATOR); ByteArrayOutputStream os = new ByteArrayOutputStream(); DataOutput output = new DataOutputStream(os); for (StreamConsumerState state : states) { os.reset(); encodeOffsets(state.getState(), output); values.put(getColumn(state.getGroupId(), state.getInstanceId()), os.toByteArray()); } store(streamId.toBytes(), values.build()); } @Override public final void remove(Iterable<? extends StreamConsumerState> states) throws IOException { Set<byte[]> columns = Sets.newTreeSet(Bytes.BYTES_COMPARATOR); for (StreamConsumerState state : states) { columns.add(getColumn(state.getGroupId(), state.getInstanceId())); } delete(streamId.toBytes(), columns); } /** * Fetches the cell value for the given row and column. * If no such value exists, {@code null} should be returned. */ protected abstract byte[] fetch(byte[] row, byte[] column) throws IOException; /** * Fetches all values for the given row. * * @param row the row to fetch from. * @param result a map from column to value. */ protected abstract void fetchAll(byte[] row, Map<byte[], byte[]> result) throws IOException; /** * Fetches all values for the given row. Children can optionally use the columnPrefix to do more efficient retrieval. * * @param row the row to fetch from. * @param columnPrefix the column prefix. * @param result a map from column to value. It's valid to contains columns that don't start with columnPrefix. */ protected abstract void fetchAll(byte[] row, byte[] columnPrefix, Map<byte[], byte[]> result) throws IOException; /** * Stores the given value to cell identified by the given row and column. */ protected abstract void store(byte[] row, byte[] column, byte[] value) throws IOException; /** * Stores all the values to the given row. * @param row the row to store to. * @param values Map from column name to value. */ protected abstract void store(byte[] row, Map<byte[], byte[]> values) throws IOException; /** * Deletes the set of columns from the given row. * @param row the row to act on. * @param columns columns to get deleted. */ protected abstract void delete(byte[] row, Set<byte[]> columns) throws IOException; /** * Encodes list of {@link StreamFileOffset} into bytes. */ private byte[] encodeOffsets(Iterable<StreamFileOffset> offsets) throws IOException { // Assumption: Each offset encoded into ~40 bytes and there are 8 offsets (number of live files) ByteArrayDataOutput output = ByteStreams.newDataOutput(320); encodeOffsets(offsets, output); return output.toByteArray(); } private void encodeOffsets(Iterable<StreamFileOffset> offsets, DataOutput output) throws IOException { for (StreamFileOffset offset : offsets) { StreamUtils.encodeOffset(output, offset); } } /** * Decodes encoded bytes back to list of {@link StreamFileOffset}. */ private Iterable<StreamFileOffset> decodeOffsets(byte[] encoded) throws IOException { ImmutableList.Builder<StreamFileOffset> offsets = ImmutableList.builder(); if (encoded != null && encoded.length > 0) { DataInputStream input = new DataInputStream(new ByteArrayInputStream(encoded)); while (input.available() > 0) { offsets.add(StreamUtils.decodeOffset(streamConfig, input)); } } return offsets.build(); } private byte[] getColumn(long groupId, int instanceId) { byte[] column = new byte[Longs.BYTES + Ints.BYTES]; Bytes.putLong(column, 0, groupId); Bytes.putInt(column, Longs.BYTES, instanceId); return column; } /** * Decodes the group id from the column name. */ private long getGroupId(byte[] columnName) { return Bytes.toLong(columnName); } /** * Decodes the instance id from the column name. */ private int getInstanceId(byte[] columnName) { return Bytes.toInt(columnName, Longs.BYTES); } }