/*
* Copyright © 2012-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.tephra.persist;
import co.cask.tephra.ChangeId;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Set;
/**
* Represents a transaction state change in the {@link TransactionLog}.
*/
public class TransactionEdit implements Writable {
// provides serde for current version
private static final TransactionEditCodec CODEC_V2 = new TransactionEditCodecV2();
private static final byte V2 = -2;
// provides serde for old but still supported version, should not be used for writing
private static final TransactionEditCodec CODEC_V1 = new TransactionEditCodecV1();
private static final byte V1 = -1;
/**
* The possible state changes for a transaction.
*/
public enum State {
INPROGRESS, COMMITTING, COMMITTED, INVALID, ABORTED, MOVE_WATERMARK
}
private long writePointer;
/**
* stores the value of visibility upper bound
* (see {@link com.cask.tephra.inmemory
* .InMemoryTransactionManager.InProgressTx#getVisibilityUpperBound()}) for edit of {@link State#INPROGRESS} only
*/
private long visibilityUpperBound;
private long commitPointer;
private long expirationDate;
private State state;
private Set<ChangeId> changes = Sets.newHashSet();
/** Whether or not the COMMITTED change should be fully committed. */
private boolean canCommit;
// for Writable
public TransactionEdit() {
}
private TransactionEdit(long writePointer, long visibilityUpperBound, State state, long expirationDate,
Set<ChangeId> changes, long commitPointer, boolean canCommit) {
this.writePointer = writePointer;
this.visibilityUpperBound = visibilityUpperBound;
this.state = state;
this.expirationDate = expirationDate;
if (changes != null) {
this.changes = changes;
}
this.commitPointer = commitPointer;
this.canCommit = canCommit;
}
/**
* Returns the transaction write pointer assigned for the state change.
*/
public long getWritePointer() {
return writePointer;
}
public long getVisibilityUpperBound() {
return visibilityUpperBound;
}
/**
* Returns the type of state change represented.
*/
public State getState() {
return state;
}
/**
* Returns any expiration timestamp (in milliseconds) associated with the state change. This should only
* be populated for changes of type {@link State#INPROGRESS}.
*/
public long getExpiration() {
return expirationDate;
}
/**
* @return the set of changed row keys associated with the state change. This is only populated for edits
* of type {@link State#COMMITTING} or {@link State#COMMITTED}.
*/
public Set<ChangeId> getChanges() {
return changes;
}
/**
* Returns the write pointer used to commit the row key change set. This is only populated for edits of type
* {@link State#COMMITTED}.
*/
public long getCommitPointer() {
return commitPointer;
}
/**
* Returns whether or not the transaction should be moved to the committed set. This is only populated for edits
* of type {@link State#COMMITTED}.
*/
public boolean getCanCommit() {
return canCommit;
}
/**
* Creates a new instance in the {@link State#INPROGRESS} state.
*/
public static TransactionEdit createStarted(long writePointer, long visibilityUpperBound,
long expirationDate) {
return new TransactionEdit(writePointer, visibilityUpperBound, State.INPROGRESS,
expirationDate, null, 0L, false);
}
/**
* Creates a new instance in the {@link State#COMMITTING} state.
*/
public static TransactionEdit createCommitting(long writePointer, Set<ChangeId> changes) {
return new TransactionEdit(writePointer, 0L, State.COMMITTING, 0L, changes, 0L, false);
}
/**
* Creates a new instance in the {@link State#COMMITTED} state.
*/
public static TransactionEdit createCommitted(long writePointer, Set<ChangeId> changes, long nextWritePointer,
boolean canCommit) {
return new TransactionEdit(writePointer, 0L, State.COMMITTED, 0L, changes, nextWritePointer, canCommit);
}
/**
* Creates a new instance in the {@link State#ABORTED} state.
*/
public static TransactionEdit createAborted(long writePointer) {
return new TransactionEdit(writePointer, 0L, State.ABORTED, 0L, null, 0L, false);
}
/**
* Creates a new instance in the {@link State#INVALID} state.
*/
public static TransactionEdit createInvalid(long writePointer) {
return new TransactionEdit(writePointer, 0L, State.INVALID, 0L, null, 0L, false);
}
/**
* Creates a new instance in the {@link State#MOVE_WATERMARK} state.
*/
public static TransactionEdit createMoveWatermark(long writePointer) {
return new TransactionEdit(writePointer, 0L, State.MOVE_WATERMARK, 0L, null, 0L, false);
}
@Override
public void write(DataOutput out) throws IOException {
CODEC_V2.encode(this, out);
}
@Override
public void readFields(DataInput in) throws IOException {
byte version = in.readByte();
if (V2 == version) {
CODEC_V2.decode(this, in);
} else if (V1 == version) {
CODEC_V1.decode(this, in);
} else {
throw new IOException("Unexpected version for edit: " + version);
}
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TransactionEdit)) {
return false;
}
TransactionEdit other = (TransactionEdit) obj;
return Objects.equal(writePointer, other.writePointer) &&
Objects.equal(commitPointer, other.commitPointer) &&
Objects.equal(expirationDate, other.expirationDate) &&
Objects.equal(visibilityUpperBound, other.visibilityUpperBound) &&
Objects.equal(state, other.state) &&
Objects.equal(changes, other.changes) &&
Objects.equal(canCommit, other.canCommit);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("writePointer", writePointer)
.add("visibilityUpperBound", visibilityUpperBound)
.add("commitPointer", commitPointer)
.add("expiration", expirationDate)
.add("state", state)
.add("changesSize", changes != null ? changes.size() : 0)
.add("canCommit", canCommit)
.toString();
}
private static interface TransactionEditCodec {
// doesn't read version field
void decode(TransactionEdit dest, DataInput in) throws IOException;
// writes version field
void encode(TransactionEdit src, DataOutput out) throws IOException;
}
// package-private for unit-test access
static class TransactionEditCodecV1 implements TransactionEditCodec {
@Override
public void decode(TransactionEdit src, DataInput in) throws IOException {
if (src.changes == null) {
src.changes = Sets.newHashSet();
} else {
src.changes.clear();
}
src.writePointer = in.readLong();
// 1st version did not store this info. It is safe to set firstInProgress to 0, it may decrease performance until
// this tx is finished, but correctness will be preserved.
src.visibilityUpperBound = 0;
int stateIdx = in.readInt();
try {
src.state = TransactionEdit.State.values()[stateIdx];
} catch (ArrayIndexOutOfBoundsException e) {
throw new IOException("State enum ordinal value is out of range: " + stateIdx);
}
src.expirationDate = in.readLong();
src.commitPointer = in.readLong();
src.canCommit = in.readBoolean();
int changeSize = in.readInt();
for (int i = 0; i < changeSize; i++) {
int currentLength = in.readInt();
byte[] currentBytes = new byte[currentLength];
in.readFully(currentBytes);
src.changes.add(new ChangeId(currentBytes));
}
}
/** @deprecated use {@link TransactionEditCodecV2} instead, it is still here for unit-tests only */
@Override
@Deprecated
public void encode(TransactionEdit src, DataOutput out) throws IOException {
out.writeByte(V1);
out.writeLong(src.writePointer);
// use ordinal for predictable size, though this does not support evolution
out.writeInt(src.state.ordinal());
out.writeLong(src.expirationDate);
out.writeLong(src.commitPointer);
out.writeBoolean(src.canCommit);
if (src.changes == null) {
out.writeInt(0);
} else {
out.writeInt(src.changes.size());
for (ChangeId c : src.changes) {
byte[] cKey = c.getKey();
out.writeInt(cKey.length);
out.write(cKey);
}
}
// NOTE: we didn't have visibilityUpperBound in V1, it was added later
}
}
// package-private for unit-test access
static class TransactionEditCodecV2 implements TransactionEditCodec {
@Override
public void decode(TransactionEdit dest, DataInput in) throws IOException {
if (dest.changes == null) {
dest.changes = Sets.newHashSet();
} else {
dest.changes.clear();
}
dest.writePointer = in.readLong();
int stateIdx = in.readInt();
try {
dest.state = TransactionEdit.State.values()[stateIdx];
} catch (ArrayIndexOutOfBoundsException e) {
throw new IOException("State enum ordinal value is out of range: " + stateIdx);
}
dest.expirationDate = in.readLong();
dest.commitPointer = in.readLong();
dest.canCommit = in.readBoolean();
int changeSize = in.readInt();
for (int i = 0; i < changeSize; i++) {
int currentLength = in.readInt();
byte[] currentBytes = new byte[currentLength];
in.readFully(currentBytes);
dest.changes.add(new ChangeId(currentBytes));
}
dest.visibilityUpperBound = in.readLong();
}
@Override
public void encode(TransactionEdit src, DataOutput out) throws IOException {
out.writeByte(V2);
out.writeLong(src.writePointer);
// use ordinal for predictable size, though this does not support evolution
out.writeInt(src.state.ordinal());
out.writeLong(src.expirationDate);
out.writeLong(src.commitPointer);
out.writeBoolean(src.canCommit);
if (src.changes == null) {
out.writeInt(0);
} else {
out.writeInt(src.changes.size());
for (ChangeId c : src.changes) {
byte[] cKey = c.getKey();
out.writeInt(cKey.length);
out.write(cKey);
}
}
out.writeLong(src.visibilityUpperBound);
}
}
}