Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2017 Google 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 com.google.firebase.database.core;
import static com.google.firebase.database.utilities.Utilities.hardAssert;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.InternalHelpers;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.database.annotations.NotNull;
import com.google.firebase.database.connection.HostInfo;
import com.google.firebase.database.connection.ListenHashProvider;
import com.google.firebase.database.connection.PersistentConnection;
import com.google.firebase.database.connection.RequestResultCallback;
import com.google.firebase.database.core.persistence.NoopPersistenceManager;
import com.google.firebase.database.core.persistence.PersistenceManager;
import com.google.firebase.database.core.utilities.Tree;
import com.google.firebase.database.core.view.Event;
import com.google.firebase.database.core.view.EventRaiser;
import com.google.firebase.database.core.view.QuerySpec;
import com.google.firebase.database.snapshot.ChildKey;
import com.google.firebase.database.snapshot.EmptyNode;
import com.google.firebase.database.snapshot.IndexedNode;
import com.google.firebase.database.snapshot.Node;
import com.google.firebase.database.snapshot.NodeUtilities;
import com.google.firebase.database.snapshot.RangeMerge;
import com.google.firebase.database.utilities.DefaultClock;
import com.google.firebase.database.utilities.OffsetClock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Repo implements PersistentConnection.Delegate {
private static final String INTERRUPT_REASON = "repo_interrupt";
/**
* If a transaction does not succeed after 25 retries, we abort it. Among other things this ensure
* that if there's ever a bug causing a mismatch between client / server hashes for some data, we
* won't retry indefinitely.
*/
private static final int TRANSACTION_MAX_RETRIES = 25;
private static final String TRANSACTION_TOO_MANY_RETRIES = "maxretries";
private static final String TRANSACTION_OVERRIDE_BY_SET = "overriddenBySet";
private static final Logger logger = LoggerFactory.getLogger(Repo.class);
private final RepoInfo repoInfo;
private final OffsetClock serverClock = new OffsetClock(new DefaultClock(), 0);
private final PersistentConnection connection;
private final EventRaiser eventRaiser;
private final Context ctx;
private SnapshotHolder infoData;
private SparseSnapshotTree onDisconnect;
private Tree> transactionQueueTree;
private boolean hijackHash = false;
private long nextWriteId = 1;
private SyncTree infoSyncTree;
private SyncTree serverSyncTree;
private final FirebaseDatabase database;
private boolean loggedTransactionPersistenceWarning = false;
private long transactionOrder = 0;
Repo(RepoInfo repoInfo, Context ctx, FirebaseDatabase database) {
this.repoInfo = repoInfo;
this.ctx = ctx;
this.database = database;
this.eventRaiser = new EventRaiser(this.ctx);
HostInfo hostInfo = new HostInfo(repoInfo.host, repoInfo.namespace, repoInfo.secure);
connection = ctx.newPersistentConnection(hostInfo, this);
// Kick off any expensive additional initialization
scheduleNow(
new Runnable() {
@Override
public void run() {
deferredInitialization();
}
});
}
private static DatabaseError fromErrorCode(String optErrorCode, String optErrorReason) {
if (optErrorCode != null) {
return DatabaseError.fromStatus(optErrorCode, optErrorReason);
} else {
return null;
}
}
// Regarding the next three methods: scheduleNow, schedule, and postEvent:
// Please use these methods rather than accessing the context directly. This ensures that the
// context is correctly re-initialized if it was previously shut down. In practice, this means
// that when a task is submitted, we will guarantee at least one thread in the core pool for the
// run loop.
/**
* Defers any initialization that is potentially expensive (e.g. disk access) and must be run on
* the run loop
*/
private void deferredInitialization() {
this.ctx.getAuthTokenProvider()
.addTokenChangeListener(
new AuthTokenProvider.TokenChangeListener() {
@Override
public void onTokenChange(String token) {
logger.debug("Auth token changed, triggering auth token refresh");
connection.refreshAuthToken(token);
}
});
// Open connection now so that by the time we are connected the deferred init has run
// This relies on the fact that all callbacks run on repo's runloop.
connection.initialize();
PersistenceManager persistenceManager = ctx.getPersistenceManager(repoInfo.host);
infoData = new SnapshotHolder();
onDisconnect = new SparseSnapshotTree();
transactionQueueTree = new Tree<>();
infoSyncTree = new SyncTree(new NoopPersistenceManager(),
new SyncTree.ListenProvider() {
@Override
public void startListening(
final QuerySpec query,
Tag tag,
final ListenHashProvider hash,
final SyncTree.CompletionListener onComplete) {
scheduleNow(new Runnable() {
@Override
public void run() {
// This is possibly a hack, but we have different semantics for .info
// endpoints. We don't raise null events on initial data...
final Node node = infoData.getNode(query.getPath());
if (!node.isEmpty()) {
List extends Event> infoEvents =
infoSyncTree.applyServerOverwrite(query.getPath(), node);
postEvents(infoEvents);
onComplete.onListenComplete(null);
}
}
});
}
@Override
public void stopListening(QuerySpec query, Tag tag) {}
});
serverSyncTree = new SyncTree(persistenceManager,
new SyncTree.ListenProvider() {
@Override
public void startListening(
QuerySpec query,
Tag tag,
ListenHashProvider hash,
final SyncTree.CompletionListener onListenComplete) {
connection.listen(
query.getPath().asList(),
query.getParams().getWireProtocolParams(),
hash,
tag != null ? tag.getTagNumber() : null,
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
List extends Event> events = onListenComplete.onListenComplete(error);
postEvents(events);
}
});
}
@Override
public void stopListening(QuerySpec query, Tag tag) {
connection.unlisten(
query.getPath().asList(), query.getParams().getWireProtocolParams());
}
});
restoreWrites(persistenceManager);
updateInfo(Constants.DOT_INFO_AUTHENTICATED, false);
updateInfo(Constants.DOT_INFO_CONNECTED, false);
}
private void restoreWrites(PersistenceManager persistenceManager) {
// No-op (Admin SDK does not support persistence)
}
public FirebaseDatabase getDatabase() {
return this.database;
}
@Override
public String toString() {
return repoInfo.toString();
}
public RepoInfo getRepoInfo() {
return this.repoInfo;
}
public void scheduleNow(Runnable r) {
InternalHelpers.checkNotDestroyed(this);
ctx.requireStarted();
ctx.getRunLoop().scheduleNow(r);
}
private void postEvent(Runnable r) {
InternalHelpers.checkNotDestroyed(this);
ctx.requireStarted();
ctx.getEventTarget().postEvent(r);
}
private void postEvents(final List extends Event> events) {
if (!events.isEmpty()) {
this.eventRaiser.raiseEvents(events);
}
}
public long getServerTime() {
return serverClock.millis();
}
boolean hasListeners() {
return !(this.infoSyncTree.isEmpty() && this.serverSyncTree.isEmpty());
}
// PersistentConnection.Delegate methods
@SuppressWarnings("unchecked") // For the cast on rawMergedData
@Override
public void onDataUpdate(
List pathSegments, Object message, boolean isMerge, Long optTag) {
Path path = new Path(pathSegments);
logger.debug("onDataUpdate: {} {}", path, message);
List extends Event> events;
try {
if (optTag != null) {
Tag tag = new Tag(optTag);
if (isMerge) {
Map taggedChildren = new HashMap<>();
Map rawMergeData = (Map) message;
for (Map.Entry entry : rawMergeData.entrySet()) {
Node newChildNode = NodeUtilities.NodeFromJSON(entry.getValue());
taggedChildren.put(new Path(entry.getKey()), newChildNode);
}
events = this.serverSyncTree.applyTaggedQueryMerge(path, taggedChildren, tag);
} else {
Node taggedSnap = NodeUtilities.NodeFromJSON(message);
events = this.serverSyncTree.applyTaggedQueryOverwrite(path, taggedSnap, tag);
}
} else if (isMerge) {
Map changedChildren = new HashMap<>();
Map rawMergeData = (Map) message;
for (Map.Entry entry : rawMergeData.entrySet()) {
Node newChildNode = NodeUtilities.NodeFromJSON(entry.getValue());
changedChildren.put(new Path(entry.getKey()), newChildNode);
}
events = this.serverSyncTree.applyServerMerge(path, changedChildren);
} else {
Node snap = NodeUtilities.NodeFromJSON(message);
events = this.serverSyncTree.applyServerOverwrite(path, snap);
}
if (events.size() > 0) {
// Since we have a listener outstanding for each transaction, receiving any events
// is a proxy for some change having occurred.
this.rerunTransactions(path);
}
postEvents(events);
} catch (DatabaseException e) {
logger.error("Firebase internal error", e);
}
}
@Override
public void onRangeMergeUpdate(
List pathSegments,
List merges,
Long tagNumber) {
Path path = new Path(pathSegments);
logger.debug("onRangeMergeUpdate: {} {}", path, merges);
List parsedMerges = new ArrayList<>(merges.size());
for (com.google.firebase.database.connection.RangeMerge merge : merges) {
parsedMerges.add(new RangeMerge(merge));
}
List extends Event> events;
if (tagNumber != null) {
events = this.serverSyncTree.applyTaggedRangeMerges(path, parsedMerges, new Tag(tagNumber));
} else {
events = this.serverSyncTree.applyServerRangeMerges(path, parsedMerges);
}
if (events.size() > 0) {
// Since we have a listener outstanding for each transaction, receiving any events
// is a proxy for some change having occurred.
this.rerunTransactions(path);
}
postEvents(events);
}
void callOnComplete(
final DatabaseReference.CompletionListener onComplete,
final DatabaseError error,
final Path path) {
if (onComplete != null) {
final DatabaseReference ref;
ChildKey last = path.getBack();
if (last != null && last.isPriorityChildName()) {
ref = InternalHelpers.createReference(this, path.getParent());
} else {
ref = InternalHelpers.createReference(this, path);
}
postEvent(
new Runnable() {
@Override
public void run() {
onComplete.onComplete(error, ref);
}
});
}
}
private void ackWriteAndRerunTransactions(long writeId, Path path, DatabaseError error) {
if (error != null && error.getCode() == DatabaseError.WRITE_CANCELED) {
// This write was already removed, we just need to ignore it...
} else {
boolean success = error == null;
List extends Event> clearEvents =
serverSyncTree.ackUserWrite(writeId, !success, /*persist=*/ true, serverClock);
if (clearEvents.size() > 0) {
rerunTransactions(path);
}
postEvents(clearEvents);
}
}
public void setValue(
final Path path,
Node newValueUnresolved,
final DatabaseReference.CompletionListener onComplete) {
logger.debug("set: {} {}", path, newValueUnresolved);
Map serverValues = ServerValues.generateServerValues(serverClock);
Node newValue = ServerValues.resolveDeferredValueSnapshot(newValueUnresolved, serverValues);
final long writeId = this.getNextWriteId();
List extends Event> events =
this.serverSyncTree.applyUserOverwrite(
path, newValueUnresolved, newValue, writeId, /*visible=*/ true, /*persist=*/ true);
this.postEvents(events);
connection.put(
path.asList(),
newValueUnresolved.getValue(true),
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
warnIfWriteFailed("setValue", path, error);
ackWriteAndRerunTransactions(writeId, path, error);
callOnComplete(onComplete, error, path);
}
});
Path affectedPath = abortTransactions(path, DatabaseError.OVERRIDDEN_BY_SET);
this.rerunTransactions(affectedPath);
}
public void updateChildren(
final Path path,
CompoundWrite updates,
final DatabaseReference.CompletionListener onComplete,
Map unParsedUpdates) {
logger.debug("update: {} {}", path, unParsedUpdates);
if (updates.isEmpty()) {
logger.debug("update called with no changes. No-op");
// dispatch on complete
callOnComplete(onComplete, null, path);
return;
}
// Start with our existing data and merge each child into it.
Map serverValues = ServerValues.generateServerValues(serverClock);
CompoundWrite resolved = ServerValues.resolveDeferredValueMerge(updates, serverValues);
final long writeId = this.getNextWriteId();
List extends Event> events =
this.serverSyncTree.applyUserMerge(path, updates, resolved, writeId, /*persist=*/ true);
this.postEvents(events);
// TODO: DatabaseReference.CompleteionListener isn't really appropriate (the DatabaseReference
// param is meaningless).
connection.merge(
path.asList(),
unParsedUpdates,
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
warnIfWriteFailed("updateChildren", path, error);
ackWriteAndRerunTransactions(writeId, path, error);
callOnComplete(onComplete, error, path);
}
});
for (Entry update : updates) {
Path pathFromRoot = path.child(update.getKey());
Path affectedPath = abortTransactions(pathFromRoot, DatabaseError.OVERRIDDEN_BY_SET);
rerunTransactions(affectedPath);
}
}
public void purgeOutstandingWrites() {
logger.debug("Purging writes");
List extends Event> events = serverSyncTree.removeAllWrites();
postEvents(events);
// Abort any transactions
abortTransactions(Path.getEmptyPath(), DatabaseError.WRITE_CANCELED);
// Remove outstanding writes from connection
connection.purgeOutstandingWrites();
}
public void removeEventCallback(@NotNull EventRegistration eventRegistration) {
// These are guaranteed not to raise events, since we're not passing in a cancelError. However,
// we can future-proof a little bit by handling the return values anyways.
List events;
if (Constants.DOT_INFO.equals(eventRegistration.getQuerySpec().getPath().getFront())) {
events = infoSyncTree.removeEventRegistration(eventRegistration);
} else {
events = serverSyncTree.removeEventRegistration(eventRegistration);
}
this.postEvents(events);
}
public void onDisconnectSetValue(
final Path path, final Node newValue, final DatabaseReference.CompletionListener onComplete) {
connection.onDisconnectPut(
path.asList(),
newValue.getValue(true),
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
warnIfWriteFailed("onDisconnect().setValue", path, error);
if (error == null) {
onDisconnect.remember(path, newValue);
}
callOnComplete(onComplete, error, path);
}
});
}
public void onDisconnectUpdate(
final Path path,
final Map newChildren,
final DatabaseReference.CompletionListener listener,
Map unParsedUpdates) {
connection.onDisconnectMerge(
path.asList(),
unParsedUpdates,
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
warnIfWriteFailed("onDisconnect().updateChildren", path, error);
if (error == null) {
for (Map.Entry entry : newChildren.entrySet()) {
onDisconnect.remember(path.child(entry.getKey()), entry.getValue());
}
}
callOnComplete(listener, error, path);
}
});
}
public void onDisconnectCancel(
final Path path, final DatabaseReference.CompletionListener onComplete) {
connection.onDisconnectCancel(
path.asList(),
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
if (error == null) {
onDisconnect.forget(path);
}
callOnComplete(onComplete, error, path);
}
});
}
@Override
public void onConnect() {
onServerInfoUpdate(Constants.DOT_INFO_CONNECTED, true);
}
@Override
public void onDisconnect() {
onServerInfoUpdate(Constants.DOT_INFO_CONNECTED, false);
runOnDisconnectEvents();
}
@Override
public void onAuthStatus(boolean authOk) {
onServerInfoUpdate(Constants.DOT_INFO_AUTHENTICATED, authOk);
}
private void onServerInfoUpdate(ChildKey key, Object value) {
updateInfo(key, value);
}
@Override
public void onServerInfoUpdate(Map updates) {
for (Map.Entry entry : updates.entrySet()) {
updateInfo(ChildKey.fromString(entry.getKey()), entry.getValue());
}
}
void interrupt() {
connection.interrupt(INTERRUPT_REASON);
}
void resume() {
InternalHelpers.checkNotDestroyed(this);
connection.resume(INTERRUPT_REASON);
}
public void addEventCallback(@NotNull EventRegistration eventRegistration) {
List extends Event> events;
ChildKey front = eventRegistration.getQuerySpec().getPath().getFront();
if (front != null && front.equals(Constants.DOT_INFO)) {
events = this.infoSyncTree.addEventRegistration(eventRegistration);
} else {
events = this.serverSyncTree.addEventRegistration(eventRegistration);
}
this.postEvents(events);
}
public void keepSynced(QuerySpec query, boolean keep) {
assert query.getPath().isEmpty() || !query.getPath().getFront().equals(Constants.DOT_INFO);
serverSyncTree.keepSynced(query, keep);
}
// Transaction code
PersistentConnection getConnection() {
return connection;
}
private void updateInfo(ChildKey childKey, Object value) {
if (childKey.equals(Constants.DOT_INFO_SERVERTIME_OFFSET)) {
serverClock.setOffset((Long) value);
}
Path path = new Path(Constants.DOT_INFO, childKey);
try {
Node node = NodeUtilities.NodeFromJSON(value);
infoData.update(path, node);
List extends Event> events = this.infoSyncTree.applyServerOverwrite(path, node);
this.postEvents(events);
} catch (DatabaseException e) {
logger.error("Failed to parse info update", e);
}
}
private long getNextWriteId() {
return this.nextWriteId++;
}
private void runOnDisconnectEvents() {
Map serverValues = ServerValues.generateServerValues(serverClock);
SparseSnapshotTree resolvedTree =
ServerValues.resolveDeferredValueTree(this.onDisconnect, serverValues);
final List events = new ArrayList<>();
resolvedTree.forEachTree(
Path.getEmptyPath(),
new SparseSnapshotTree.SparseSnapshotTreeVisitor() {
@Override
public void visitTree(Path prefixPath, Node node) {
events.addAll(serverSyncTree.applyServerOverwrite(prefixPath, node));
Path affectedPath = abortTransactions(prefixPath, DatabaseError.OVERRIDDEN_BY_SET);
rerunTransactions(affectedPath);
}
});
onDisconnect = new SparseSnapshotTree();
this.postEvents(events);
}
private void warnIfWriteFailed(String writeType, Path path, DatabaseError error) {
// DATA_STALE is a normal, expected error during transaction processing.
if (error != null
&& !(error.getCode() == DatabaseError.DATA_STALE
|| error.getCode() == DatabaseError.WRITE_CANCELED)) {
logger.warn(writeType + " at " + path.toString() + " failed: " + error.toString());
}
}
public void startTransaction(Path path, final Transaction.Handler handler, boolean applyLocally) {
logger.debug("transaction: {}", path);
if (this.ctx.isPersistenceEnabled() && !loggedTransactionPersistenceWarning) {
loggedTransactionPersistenceWarning = true;
logger.info(
"runTransaction() usage detected while persistence is enabled. Please be aware that "
+ "transactions *will not* be persisted across database restarts. See "
+ "https://www.firebase.com/docs/android/guide/offline-capabilities.html"
+ "#section-handling-transactions-offline for more details.");
}
// make sure we're listening on this node
// Note: we can't do this asynchronously. To preserve event ordering,
// it has to be done in this block. This is ok, this block is
// guaranteed to be our own event loop
DatabaseReference watchRef = InternalHelpers.createReference(this, path);
ValueEventListener listener =
new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
// No-op. We don't care, this is just to make sure we have a listener outstanding
}
@Override
public void onCancelled(DatabaseError error) {
// Also a no-op? We'll cancel the transaction in this case
}
};
addEventCallback(new ValueEventRegistration(this, listener, watchRef.getSpec()));
TransactionData transaction =
new TransactionData(
path,
handler,
listener,
TransactionStatus.INITIALIZING,
applyLocally,
nextTransactionOrder());
// Run transaction initially.
Node currentState = this.getLatestState(path);
transaction.currentInputSnapshot = currentState;
MutableData mutableCurrent = InternalHelpers.createMutableData(currentState);
DatabaseError error = null;
Transaction.Result result;
try {
result = handler.doTransaction(mutableCurrent);
if (result == null) {
throw new NullPointerException("Transaction returned null as result");
}
} catch (Throwable e) {
error = DatabaseError.fromException(e);
result = Transaction.abort();
}
if (!result.isSuccess()) {
// Abort the transaction
transaction.currentOutputSnapshotRaw = null;
transaction.currentOutputSnapshotResolved = null;
final DatabaseError innerClassError = error;
final DataSnapshot snap =
InternalHelpers.createDataSnapshot(
watchRef, IndexedNode.from(transaction.currentInputSnapshot));
postEvent(
new Runnable() {
@Override
public void run() {
runTransactionOnComplete(handler, innerClassError, false, snap);
}
});
} else {
// Mark as run and add to our queue.
transaction.status = TransactionStatus.RUN;
Tree> queueNode = transactionQueueTree.subTree(path);
List nodeQueue = queueNode.getValue();
if (nodeQueue == null) {
nodeQueue = new ArrayList<>();
}
nodeQueue.add(transaction);
queueNode.setValue(nodeQueue);
Map serverValues = ServerValues.generateServerValues(serverClock);
Node newNodeUnresolved = result.getNode();
Node newNode = ServerValues.resolveDeferredValueSnapshot(newNodeUnresolved, serverValues);
transaction.currentOutputSnapshotRaw = newNodeUnresolved;
transaction.currentOutputSnapshotResolved = newNode;
transaction.currentWriteId = this.getNextWriteId();
List extends Event> events =
this.serverSyncTree.applyUserOverwrite(
path,
newNodeUnresolved,
newNode,
transaction.currentWriteId, /*visible=*/
applyLocally, /*persist=*/
false);
this.postEvents(events);
sendAllReadyTransactions();
}
}
private Node getLatestState(Path path) {
return this.getLatestState(path, new ArrayList());
}
private Node getLatestState(Path path, List excudeSets) {
Node state = this.serverSyncTree.calcCompleteEventCache(path, excudeSets);
if (state == null) {
state = EmptyNode.Empty();
}
return state;
}
public void setHijackHash(boolean hijackHash) {
this.hijackHash = hijackHash;
}
private void sendAllReadyTransactions() {
Tree> node = transactionQueueTree;
pruneCompletedTransactions(node);
sendReadyTransactions(node);
}
private void sendReadyTransactions(Tree> node) {
List queue = node.getValue();
if (queue != null) {
queue = buildTransactionQueue(node);
assert queue.size() > 0; // Sending zero length transaction queue
Boolean allRun = true;
for (TransactionData transaction : queue) {
if (transaction.status != TransactionStatus.RUN) {
allRun = false;
break;
}
}
// If they're all run (and not sent), we can send them. Else, we must wait.
if (allRun) {
sendTransactionQueue(queue, node.getPath());
}
} else if (node.hasChildren()) {
node.forEachChild(
new Tree.TreeVisitor>() {
@Override
public void visitTree(Tree> tree) {
sendReadyTransactions(tree);
}
});
}
}
private void sendTransactionQueue(final List queue, final Path path) {
// Mark transactions as sent and increment retry count!
List setsToIgnore = new ArrayList<>();
for (TransactionData txn : queue) {
setsToIgnore.add(txn.currentWriteId);
}
Node latestState = this.getLatestState(path, setsToIgnore);
Node snapToSend = latestState;
String latestHash = "badhash";
if (!hijackHash) {
latestHash = latestState.getHash();
}
for (TransactionData txn : queue) {
assert txn.status
== TransactionStatus.RUN; // sendTransactionQueue: items in queue should all be run.'
txn.status = TransactionStatus.SENT;
txn.retryCount++;
Path relativePath = Path.getRelative(path, txn.path);
// If we've gotten to this point, the output snapshot must be defined.
snapToSend = snapToSend.updateChild(relativePath, txn.currentOutputSnapshotRaw);
}
Object dataToSend = snapToSend.getValue(true);
final Repo repo = this;
// Send the put.
connection.compareAndPut(
path.asList(),
dataToSend,
latestHash,
new RequestResultCallback() {
@Override
public void onRequestResult(String optErrorCode, String optErrorMessage) {
DatabaseError error = fromErrorCode(optErrorCode, optErrorMessage);
warnIfWriteFailed("Transaction", path, error);
List events = new ArrayList<>();
if (error == null) {
List callbacks = new ArrayList<>();
for (final TransactionData txn : queue) {
txn.status = TransactionStatus.COMPLETED;
events.addAll(
serverSyncTree.ackUserWrite(
txn.currentWriteId, /*revert=*/ false, /*persist=*/ false, serverClock));
// We never unset the output snapshot, and given that this
// transaction is complete, it should be set
Node node = txn.currentOutputSnapshotResolved;
final DataSnapshot snap =
InternalHelpers.createDataSnapshot(
InternalHelpers.createReference(repo, txn.path), IndexedNode.from(node));
callbacks.add(
new Runnable() {
@Override
public void run() {
runTransactionOnComplete(txn.handler, null, true, snap);
}
});
// Remove the outstanding value listener that we added
removeEventCallback(
new ValueEventRegistration(
Repo.this,
txn.outstandingListener,
QuerySpec.defaultQueryAtPath(txn.path)));
}
// Now remove the completed transactions
pruneCompletedTransactions(transactionQueueTree.subTree(path));
// There may be pending transactions that we can now send
sendAllReadyTransactions();
repo.postEvents(events);
// Finally, run the callbacks
for (Runnable callback : callbacks) {
postEvent(callback);
}
} else {
// transactions are no longer sent. Update their status appropriately
if (error.getCode() == DatabaseError.DATA_STALE) {
for (TransactionData transaction : queue) {
if (transaction.status == TransactionStatus.SENT_NEEDS_ABORT) {
transaction.status = TransactionStatus.NEEDS_ABORT;
} else {
transaction.status = TransactionStatus.RUN;
}
}
} else {
for (TransactionData transaction : queue) {
transaction.status = TransactionStatus.NEEDS_ABORT;
transaction.abortReason = error;
}
}
// since we reverted mergedData, we should re-run any remaining
// transactions and raise events
rerunTransactions(path);
}
}
});
}
private void pruneCompletedTransactions(Tree> node) {
List queue = node.getValue();
if (queue != null) {
int i = 0;
while (i < queue.size()) {
TransactionData transaction = queue.get(i);
if (transaction.status == TransactionStatus.COMPLETED) {
queue.remove(i);
} else {
i++;
}
}
if (queue.size() > 0) {
node.setValue(queue);
} else {
node.setValue(null);
}
}
node.forEachChild(
new Tree.TreeVisitor>() {
@Override
public void visitTree(Tree> tree) {
pruneCompletedTransactions(tree);
}
});
}
private long nextTransactionOrder() {
return transactionOrder++;
}
private Path rerunTransactions(Path changedPath) {
Tree> rootMostTransactionNode = getAncestorTransactionNode(changedPath);
Path path = rootMostTransactionNode.getPath();
List queue = buildTransactionQueue(rootMostTransactionNode);
rerunTransactionQueue(queue, path);
return path;
}
private void rerunTransactionQueue(List queue, Path path) {
if (queue.isEmpty()) {
return; // Nothing to do!
}
// Queue up the callbacks and fire them after cleaning up all of our transaction state, since
// the callback could trigger more transactions or sets
List callbacks = new ArrayList<>();
// Ignore, by default, all of the sets in this queue, since we're re-running all of them.
// However, we want to include the results of new sets triggered as part of this re-run, so we
// don't want to ignore a range, just these specific sets.
List setsToIgnore = new ArrayList<>();
for (TransactionData transaction : queue) {
setsToIgnore.add(transaction.currentWriteId);
}
for (final TransactionData transaction : queue) {
Path relativePath = Path.getRelative(path, transaction.path);
boolean abortTransaction = false;
DatabaseError abortReason = null;
List events = new ArrayList<>();
assert relativePath != null; // rerunTransactionQueue: relativePath should not be null.
if (transaction.status == TransactionStatus.NEEDS_ABORT) {
abortTransaction = true;
abortReason = transaction.abortReason;
if (abortReason.getCode() != DatabaseError.WRITE_CANCELED) {
events.addAll(
serverSyncTree.ackUserWrite(
transaction.currentWriteId, /*revert=*/ true, /*persist=*/ false, serverClock));
}
} else if (transaction.status == TransactionStatus.RUN) {
if (transaction.retryCount >= TRANSACTION_MAX_RETRIES) {
abortTransaction = true;
abortReason = DatabaseError.fromStatus(TRANSACTION_TOO_MANY_RETRIES);
events.addAll(
serverSyncTree.ackUserWrite(
transaction.currentWriteId, /*revert=*/ true, /*persist=*/ false, serverClock));
} else {
// This code reruns a transaction
Node currentNode = this.getLatestState(transaction.path, setsToIgnore);
transaction.currentInputSnapshot = currentNode;
MutableData mutableCurrent = InternalHelpers.createMutableData(currentNode);
DatabaseError error = null;
Transaction.Result result;
try {
result = transaction.handler.doTransaction(mutableCurrent);
} catch (Throwable e) {
error = DatabaseError.fromException(e);
result = Transaction.abort();
}
if (result.isSuccess()) {
final Long oldWriteId = transaction.currentWriteId;
Map serverValues = ServerValues.generateServerValues(serverClock);
Node newDataNode = result.getNode();
Node newNodeResolved =
ServerValues.resolveDeferredValueSnapshot(newDataNode, serverValues);
transaction.currentOutputSnapshotRaw = newDataNode;
transaction.currentOutputSnapshotResolved = newNodeResolved;
transaction.currentWriteId = this.getNextWriteId();
// Mutates setsToIgnore in place
setsToIgnore.remove(oldWriteId);
events.addAll(
serverSyncTree.applyUserOverwrite(
transaction.path,
newDataNode,
newNodeResolved,
transaction.currentWriteId,
transaction.applyLocally, /*persist=*/
false));
events.addAll(
serverSyncTree.ackUserWrite(
oldWriteId, /*revert=*/ true, /*persist=*/ false, serverClock));
} else {
// The user aborted the transaction. It's not an error, so we don't need to send them
// one
abortTransaction = true;
abortReason = error;
events.addAll(
serverSyncTree.ackUserWrite(
transaction.currentWriteId, /*revert=*/ true, /*persist=*/ false, serverClock));
}
}
}
this.postEvents(events);
if (abortTransaction) {
// Abort
transaction.status = TransactionStatus.COMPLETED;
final DatabaseReference ref = InternalHelpers.createReference(this, transaction.path);
// We set this field immediately, so it's safe to cast to an actual snapshot
Node lastInput = transaction.currentInputSnapshot;
// TODO: In the future, perhaps this should just be KeyIndex?
final DataSnapshot snapshot =
InternalHelpers.createDataSnapshot(ref, IndexedNode.from(lastInput));
// Removing a callback can trigger pruning which can muck with mergedData/visibleData (as it
// prunes data). So defer removing the callback until later.
this.scheduleNow(
new Runnable() {
@Override
public void run() {
removeEventCallback(
new ValueEventRegistration(
Repo.this,
transaction.outstandingListener,
QuerySpec.defaultQueryAtPath(transaction.path)));
}
});
final DatabaseError callbackError = abortReason;
callbacks.add(
new Runnable() {
@Override
public void run() {
runTransactionOnComplete(transaction.handler, callbackError, false, snapshot);
}
});
}
}
// Clean up completed transactions.
pruneCompletedTransactions(transactionQueueTree);
// Now fire callbacks, now that we're in a good, known state.
for (Runnable callback : callbacks) {
postEvent(callback);
}
// Try to send the transaction result to the server.
sendAllReadyTransactions();
}
private Tree> getAncestorTransactionNode(Path path) {
Tree> transactionNode = transactionQueueTree;
while (!path.isEmpty() && transactionNode.getValue() == null) {
transactionNode = transactionNode.subTree(new Path(path.getFront()));
path = path.popFront();
}
return transactionNode;
}
private List buildTransactionQueue(Tree> transactionNode) {
List queue = new ArrayList<>();
aggregateTransactionQueues(queue, transactionNode);
Collections.sort(queue);
return queue;
}
private void aggregateTransactionQueues(
final List queue, Tree> node) {
List childQueue = node.getValue();
if (childQueue != null) {
queue.addAll(childQueue);
}
node.forEachChild(
new Tree.TreeVisitor>() {
@Override
public void visitTree(Tree> tree) {
aggregateTransactionQueues(queue, tree);
}
});
}
private Path abortTransactions(Path path, final int reason) {
Path affectedPath = getAncestorTransactionNode(path).getPath();
logger.debug("Aborting transactions for path: {}. Affected: {}", path, affectedPath);
Tree> transactionNode = transactionQueueTree.subTree(path);
transactionNode.forEachAncestor(
new Tree.TreeFilter>() {
@Override
public boolean filterTreeNode(Tree> tree) {
abortTransactionsAtNode(tree, reason);
return false;
}
});
abortTransactionsAtNode(transactionNode, reason);
transactionNode.forEachDescendant(
new Tree.TreeVisitor>() {
@Override
public void visitTree(Tree> tree) {
abortTransactionsAtNode(tree, reason);
}
});
return affectedPath;
}
private void abortTransactionsAtNode(Tree> node, int reason) {
List queue = node.getValue();
List events = new ArrayList<>();
if (queue != null) {
List callbacks = new ArrayList<>();
final DatabaseError abortError;
if (reason == DatabaseError.OVERRIDDEN_BY_SET) {
abortError = DatabaseError.fromStatus(TRANSACTION_OVERRIDE_BY_SET);
} else {
hardAssert(
reason == DatabaseError.WRITE_CANCELED, "Unknown transaction abort reason: " + reason);
abortError = DatabaseError.fromCode(DatabaseError.WRITE_CANCELED);
}
int lastSent = -1;
for (int i = 0; i < queue.size(); ++i) {
final TransactionData transaction = queue.get(i);
if (transaction.status == TransactionStatus.SENT_NEEDS_ABORT) {
// No-op. Already marked
} else if (transaction.status == TransactionStatus.SENT) {
assert lastSent == i - 1; // All SENT items should be at beginning of queue.
lastSent = i;
// Mark transaction for abort when it comes back.
transaction.status = TransactionStatus.SENT_NEEDS_ABORT;
transaction.abortReason = abortError;
} else {
assert transaction.status
== TransactionStatus.RUN; // Unexpected transaction status in abort
// We can abort this immediately.
removeEventCallback(
new ValueEventRegistration(
Repo.this,
transaction.outstandingListener,
QuerySpec.defaultQueryAtPath(transaction.path)));
if (reason == DatabaseError.OVERRIDDEN_BY_SET) {
events.addAll(
serverSyncTree.ackUserWrite(
transaction.currentWriteId, /*revert=*/ true, /*persist=*/ false, serverClock));
} else {
hardAssert(
reason == DatabaseError.WRITE_CANCELED,
"Unknown transaction abort reason: " + reason);
// If it was cancelled, it was already removed from the sync tree
}
callbacks.add(
new Runnable() {
@Override
public void run() {
runTransactionOnComplete(transaction.handler, abortError, false, null);
}
});
}
}
if (lastSent == -1) {
// We're not waiting for any sent transactions. We can clear the queue
node.setValue(null);
} else {
// Remove the transactions we aborted
node.setValue(queue.subList(0, lastSent + 1));
}
// Now fire the callbacks.
this.postEvents(events);
for (Runnable r : callbacks) {
postEvent(r);
}
}
}
private void runTransactionOnComplete(Transaction.Handler handler, DatabaseError error,
boolean committed, DataSnapshot snapshot) {
try {
handler.onComplete(error, committed, snapshot);
} catch (Exception e) {
logger.error("Exception in transaction onComplete callback", e);
}
}
// Package private for testing purposes only
SyncTree getServerSyncTree() {
return serverSyncTree;
}
// Package private for testing purposes only
SyncTree getInfoSyncTree() {
return infoSyncTree;
}
private enum TransactionStatus {
INITIALIZING,
// We've run the transaction and updated transactionResultData_ with the result, but it isn't
// currently sent to the server.
// A transaction will go from RUN -> SENT -> RUN if it comes back from the server as rejected
// due to mismatched hash.
RUN,
// We've run the transaction and sent it to the server and it's currently outstanding (hasn't
// come back as accepted or rejected yet).
SENT,
// Temporary state used to mark completed transactions (whether successful or aborted). The
// transaction will be removed when we get a chance to prune completed ones.
COMPLETED,
// Used when an already-sent transaction needs to be aborted (e.g. due to a conflicting set()
// call that was made). If it comes back as unsuccessful, we'll abort it.
SENT_NEEDS_ABORT,
// Temporary state used to mark transactions that need to be aborted.
NEEDS_ABORT
}
private static class TransactionData implements Comparable {
private Path path;
private Transaction.Handler handler;
private ValueEventListener outstandingListener;
private TransactionStatus status;
private long order;
private boolean applyLocally;
private int retryCount;
private DatabaseError abortReason;
private long currentWriteId;
private Node currentInputSnapshot;
private Node currentOutputSnapshotRaw;
private Node currentOutputSnapshotResolved;
private TransactionData(
Path path,
Transaction.Handler handler,
ValueEventListener outstandingListener,
TransactionStatus status,
boolean applyLocally,
long order) {
this.path = path;
this.handler = handler;
this.outstandingListener = outstandingListener;
this.status = status;
this.retryCount = 0;
this.applyLocally = applyLocally;
this.order = order;
this.abortReason = null;
this.currentInputSnapshot = null;
this.currentOutputSnapshotRaw = null;
this.currentOutputSnapshotResolved = null;
}
@Override
public int compareTo(TransactionData o) {
return Long.compare(order, o.order);
}
}
}