/* * Copyright (c) 2015-present, Parse, LLC. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.parse; import android.Manifest; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import org.json.JSONObject; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import bolts.Continuation; import bolts.Task; import bolts.TaskCompletionSource; /** * Manages all *Eventually calls when the local datastore is enabled. * * Constraints: * - *Eventually calls must be executed in the same order they were queued. * - *Eventually calls must only be executed when it's ParseOperationSet is ready in * {@link ParseObject#taskQueue}. * - All rules apply on start from reboot. */ /** package */ class ParsePinningEventuallyQueue extends ParseEventuallyQueue { private static final String TAG = "ParsePinningEventuallyQueue"; /** * TCS that is held until a {@link ParseOperationSet} is completed. */ private HashMap<String, TaskCompletionSource<JSONObject>> pendingOperationSetUUIDTasks = new HashMap<>(); /** * Queue for reading/writing eventually operations. Makes all reads/writes atomic operations. */ private TaskQueue taskQueue = new TaskQueue(); /** * Queue for running *Eventually operations. It uses waitForOperationSetAndEventuallyPin to * synchronize {@link ParseObject#taskQueue} until they are both ready to process the same * ParseOperationSet. */ private TaskQueue operationSetTaskQueue = new TaskQueue(); /** * List of {@link ParseOperationSet#uuid} that are currently queued in * {@link ParsePinningEventuallyQueue#operationSetTaskQueue}. */ private ArrayList<String> eventuallyPinUUIDQueue = new ArrayList<>(); /** * TCS that is created when there is no internet connection and isn't resolved until connectivity * is achieved. * * If an error is set, it means that we are trying to clear out the taskQueues. */ private TaskCompletionSource<Void> connectionTaskCompletionSource = new TaskCompletionSource<>(); private final Object connectionLock = new Object(); private final ParseHttpClient httpClient; private ConnectivityNotifier notifier; private ConnectivityNotifier.ConnectivityListener listener = new ConnectivityNotifier.ConnectivityListener() { @Override public void networkConnectivityStatusChanged(Context context, Intent intent) { boolean connectionLost = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); if (connectionLost) { setConnected(false); } else { setConnected(ConnectivityNotifier.isConnected(context)); } } }; public ParsePinningEventuallyQueue(Context context, ParseHttpClient client) { setConnected(ConnectivityNotifier.isConnected(context)); httpClient = client; notifier = ConnectivityNotifier.getNotifier(context); notifier.addListener(listener); resume(); } @Override public void onDestroy() { //TODO (grantland): pause #6484855 notifier.removeListener(listener); } @Override public void setConnected(boolean connected) { synchronized (connectionLock) { if (isConnected() != connected) { super.setConnected(connected); if (connected) { connectionTaskCompletionSource.trySetResult(null); connectionTaskCompletionSource = Task.create(); connectionTaskCompletionSource.trySetResult(null); } else { connectionTaskCompletionSource = Task.create(); } } } } @Override public int pendingCount() { try { return ParseTaskUtils.wait(pendingCountAsync()); } catch (ParseException e) { throw new IllegalStateException(e); } } public Task<Integer> pendingCountAsync() { final TaskCompletionSource<Integer> tcs = new TaskCompletionSource<>(); taskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { return pendingCountAsync(toAwait).continueWithTask(new Continuation<Integer, Task<Void>>() { @Override public Task<Void> then(Task<Integer> task) throws Exception { int count = task.getResult(); tcs.setResult(count); return Task.forResult(null); } }); } }); return tcs.getTask(); } public Task<Integer> pendingCountAsync(Task<Void> toAwait) { return toAwait.continueWithTask(new Continuation<Void, Task<Integer>>() { @Override public Task<Integer> then(Task<Void> task) throws Exception { return EventuallyPin.findAllPinned().continueWithTask(new Continuation<List<EventuallyPin>, Task<Integer>>() { @Override public Task<Integer> then(Task<List<EventuallyPin>> task) throws Exception { List<EventuallyPin> pins = task.getResult(); return Task.forResult(pins.size()); } }); } }); } @Override public void pause() { synchronized (connectionLock) { // Error out tasks waiting on waitForConnectionAsync. connectionTaskCompletionSource.trySetError(new PauseException()); connectionTaskCompletionSource = Task.create(); connectionTaskCompletionSource.trySetError(new PauseException()); } synchronized (taskQueueSyncLock) { for (String key : pendingEventuallyTasks.keySet()) { // Error out tasks waiting on waitForOperationSetAndEventuallyPin. pendingEventuallyTasks.get(key).trySetError(new PauseException()); } pendingEventuallyTasks.clear(); uuidToOperationSet.clear(); uuidToEventuallyPin.clear(); } try { ParseTaskUtils.wait(whenAll(Arrays.asList(taskQueue, operationSetTaskQueue))); } catch (ParseException e) { throw new IllegalStateException(e); } } @Override public void resume() { // Reset waitForConnectionAsync. if (isConnected()) { connectionTaskCompletionSource.trySetResult(null); connectionTaskCompletionSource = Task.create(); connectionTaskCompletionSource.trySetResult(null); } else { connectionTaskCompletionSource = Task.create(); } populateQueueAsync(); } private Task<Void> waitForConnectionAsync() { synchronized (connectionLock) { return connectionTaskCompletionSource.getTask(); } } /** * Pins the eventually operation on {@link ParsePinningEventuallyQueue#taskQueue}. * * @return Returns a Task that will be resolved when the command completes. */ @Override public Task<JSONObject> enqueueEventuallyAsync(final ParseRESTCommand command, final ParseObject object) { Parse.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE); final TaskCompletionSource<JSONObject> tcs = new TaskCompletionSource<>(); taskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { return enqueueEventuallyAsync(command, object, toAwait, tcs); } }); return tcs.getTask(); } private Task<Void> enqueueEventuallyAsync(final ParseRESTCommand command, final ParseObject object, Task<Void> toAwait, final TaskCompletionSource<JSONObject> tcs) { return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { Task<EventuallyPin> pinTask = EventuallyPin.pinEventuallyCommand(object, command); return pinTask.continueWithTask(new Continuation<EventuallyPin, Task<Void>>() { @Override public Task<Void> then(Task<EventuallyPin> task) throws Exception { EventuallyPin pin = task.getResult(); Exception error = task.getError(); if (error != null) { if (Parse.LOG_LEVEL_WARNING >= Parse.getLogLevel()) { PLog.w(TAG, "Unable to save command for later.", error); } notifyTestHelper(TestHelper.COMMAND_NOT_ENQUEUED); return Task.forResult(null); } pendingOperationSetUUIDTasks.put(pin.getUUID(), tcs); // We don't need to wait for this. populateQueueAsync().continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { /* * We need to wait until after we populated the operationSetTaskQueue to notify * that we've enqueued this command. */ notifyTestHelper(TestHelper.COMMAND_ENQUEUED); return task; } }); return task.makeVoid(); } }); } }); } /** * Queries for pinned eventually operations on {@link ParsePinningEventuallyQueue#taskQueue}. * * @return Returns a Task that is resolved when all EventuallyPins are enqueued in the * operationSetTaskQueue. */ private Task<Void> populateQueueAsync() { return taskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { return populateQueueAsync(toAwait); } }); } private Task<Void> populateQueueAsync(Task<Void> toAwait) { return toAwait.continueWithTask(new Continuation<Void, Task<List<EventuallyPin>>>() { @Override public Task<List<EventuallyPin>> then(Task<Void> task) throws Exception { // We don't want to enqueue any EventuallyPins that are already queued. return EventuallyPin.findAllPinned(eventuallyPinUUIDQueue); } }).onSuccessTask(new Continuation<List<EventuallyPin>, Task<Void>>() { @Override public Task<Void> then(Task<List<EventuallyPin>> task) throws Exception { List<EventuallyPin> pins = task.getResult(); for (final EventuallyPin pin : pins) { // We don't need to wait for this. runEventuallyAsync(pin); } return task.makeVoid(); } }); } /** * Queues an eventually operation on {@link ParsePinningEventuallyQueue#operationSetTaskQueue}. * * Each eventually operation is run synchronously to maintain the order in which they were * enqueued. */ private Task<Void> runEventuallyAsync(final EventuallyPin eventuallyPin) { final String uuid = eventuallyPin.getUUID(); if (eventuallyPinUUIDQueue.contains(uuid)) { // We don't want to enqueue the same operation more than once. return Task.forResult(null); } eventuallyPinUUIDQueue.add(uuid); operationSetTaskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(final Task<Void> toAwait) throws Exception { return runEventuallyAsync(eventuallyPin, toAwait).continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { eventuallyPinUUIDQueue.remove(uuid); return task; } }); } }); return Task.forResult(null); } /** * Runs the eventually operation. It first waits for a valid connection and if it's a save, it * also waits for the ParseObject to be ready. * * @return A task that is resolved when the eventually operation completes. */ private Task<Void> runEventuallyAsync(final EventuallyPin eventuallyPin, final Task<Void> toAwait) { return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { return waitForConnectionAsync(); } }).onSuccessTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { return waitForOperationSetAndEventuallyPin(null, eventuallyPin).continueWithTask(new Continuation<JSONObject, Task<Void>>() { @Override public Task<Void> then(Task<JSONObject> task) throws Exception { Exception error = task.getError(); if (error != null) { if (error instanceof PauseException) { // Bubble up the PauseException. return task.makeVoid(); } if (Parse.LOG_LEVEL_ERROR >= Parse.getLogLevel()) { PLog.e(TAG, "Failed to run command.", error); } notifyTestHelper(TestHelper.COMMAND_FAILED, error); } else { notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL); } TaskCompletionSource<JSONObject> tcs = pendingOperationSetUUIDTasks.remove(eventuallyPin.getUUID()); if (tcs != null) { if (error != null) { tcs.setError(error); } else { tcs.setResult(task.getResult()); } } return task.makeVoid(); } }); } }); } /** * Lock to make sure all changes to the below parameters happen atomically. */ private final Object taskQueueSyncLock = new Object(); /** * Map of eventually operation UUID to TCS that is resolved when the operation is complete. */ private HashMap<String, TaskCompletionSource<JSONObject>> pendingEventuallyTasks = new HashMap<>(); /** * Map of eventually operation UUID to matching ParseOperationSet. */ private HashMap<String, ParseOperationSet> uuidToOperationSet = new HashMap<>(); /** * Map of eventually operation UUID to matching EventuallyPin. */ private HashMap<String, EventuallyPin> uuidToEventuallyPin = new HashMap<>(); /** * Synchronizes ParseObject#taskQueue (Many) and ParseCommandCache#taskQueue (One). Each queue * will be held until both are ready, matched on operationSetUUID. Once both are ready, the * eventually task will be run. * * @param operationSet * From {@link ParseObject} * @param eventuallyPin * From {@link ParsePinningEventuallyQueue} */ //TODO (grantland): We can probably generalize this to synchronize/join more than 2 taskQueues @Override /* package */ Task<JSONObject> waitForOperationSetAndEventuallyPin(ParseOperationSet operationSet, EventuallyPin eventuallyPin) { if (eventuallyPin != null && eventuallyPin.getType() != EventuallyPin.TYPE_SAVE) { return process(eventuallyPin, null); } final String uuid; // The key we use to join the taskQueues final TaskCompletionSource<JSONObject> tcs; synchronized (taskQueueSyncLock) { if (operationSet != null && eventuallyPin == null) { uuid = operationSet.getUUID(); uuidToOperationSet.put(uuid, operationSet); } else if (operationSet == null && eventuallyPin != null) { uuid = eventuallyPin.getOperationSetUUID(); uuidToEventuallyPin.put(uuid, eventuallyPin); } else { throw new IllegalStateException("Either operationSet or eventuallyPin must be set."); } eventuallyPin = uuidToEventuallyPin.get(uuid); operationSet = uuidToOperationSet.get(uuid); if (eventuallyPin == null || operationSet == null) { if (pendingEventuallyTasks.containsKey(uuid)) { tcs = pendingEventuallyTasks.get(uuid); } else { tcs = Task.create(); pendingEventuallyTasks.put(uuid, tcs); } return tcs.getTask(); } else { tcs = pendingEventuallyTasks.get(uuid); } } return process(eventuallyPin, operationSet).continueWithTask(new Continuation<JSONObject, Task<JSONObject>>() { @Override public Task<JSONObject> then(Task<JSONObject> task) throws Exception { synchronized (taskQueueSyncLock) { pendingEventuallyTasks.remove(uuid); uuidToOperationSet.remove(uuid); uuidToEventuallyPin.remove(uuid); } Exception error = task.getError(); if (error != null) { tcs.trySetError(error); } else if (task.isCancelled()) { tcs.trySetCancelled(); } else { tcs.trySetResult(task.getResult()); } return tcs.getTask(); } }); } /** * Invokes the eventually operation. */ private Task<JSONObject> process(final EventuallyPin eventuallyPin, final ParseOperationSet operationSet) { return waitForConnectionAsync().onSuccessTask(new Continuation<Void, Task<JSONObject>>() { @Override public Task<JSONObject> then(Task<Void> task) throws Exception { final int type = eventuallyPin.getType(); final ParseObject object = eventuallyPin.getObject(); String sessionToken = eventuallyPin.getSessionToken(); Task<JSONObject> executeTask; if (type == EventuallyPin.TYPE_SAVE) { executeTask = object.saveAsync(httpClient, operationSet, sessionToken); } else if (type == EventuallyPin.TYPE_DELETE) { executeTask = object.deleteAsync(sessionToken).cast(); } else { // else if (type == EventuallyPin.TYPE_COMMAND) { ParseRESTCommand command = eventuallyPin.getCommand(); if (command == null) { executeTask = Task.forResult(null); notifyTestHelper(TestHelper.COMMAND_OLD_FORMAT_DISCARDED); } else { executeTask = command.executeAsync(httpClient); } } return executeTask.continueWithTask(new Continuation<JSONObject, Task<JSONObject>>() { @Override public Task<JSONObject> then(final Task<JSONObject> executeTask) throws Exception { Exception error = executeTask.getError(); if (error != null) { if (error instanceof ParseException && ((ParseException) error).getCode() == ParseException.CONNECTION_FAILED) { // We did our retry logic in ParseRequest, so just mark as not connected // and move on. setConnected(false); notifyTestHelper(TestHelper.NETWORK_DOWN); return process(eventuallyPin, operationSet); } } // Delete the command regardless, even if it failed. Otherwise, we'll just keep // trying it forever. // We don't have to wait for taskQueue since it will not be enqueued again // since this EventuallyPin is still in eventuallyPinUUIDQueue. return eventuallyPin.unpinInBackground(EventuallyPin.PIN_NAME).continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { JSONObject result = executeTask.getResult(); if (type == EventuallyPin.TYPE_SAVE) { return object.handleSaveEventuallyResultAsync(result, operationSet); } else if (type == EventuallyPin.TYPE_DELETE) { if (executeTask.isFaulted()) { return task; } else { return object.handleDeleteEventuallyResultAsync(); } } else { // else if (type == EventuallyPin.TYPE_COMMAND) { return task; } } }).continueWithTask(new Continuation<Void, Task<JSONObject>>() { @Override public Task<JSONObject> then(Task<Void> task) throws Exception { return executeTask; } }); } }); } }); } @Override /* package */ void simulateReboot() { pause(); pendingOperationSetUUIDTasks.clear(); pendingEventuallyTasks.clear(); uuidToOperationSet.clear(); uuidToEventuallyPin.clear(); resume(); } @Override public void clear() { pause(); Task<Void> task = taskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> task) throws Exception { return EventuallyPin.findAllPinned().onSuccessTask(new Continuation<List<EventuallyPin>, Task<Void>>() { @Override public Task<Void> then(Task<List<EventuallyPin>> task) throws Exception { List<EventuallyPin> pins = task.getResult(); List<Task<Void>> tasks = new ArrayList<>(); for (EventuallyPin pin : pins) { tasks.add(pin.unpinInBackground(EventuallyPin.PIN_NAME)); } return Task.whenAll(tasks); } }); } }); } }); try { ParseTaskUtils.wait(task); } catch (ParseException e) { throw new IllegalStateException(e); } simulateReboot(); resume(); } /** * Creates a Task that is resolved when all the TaskQueues are "complete". * * "Complete" is when all the TaskQueues complete the queue of Tasks that were in it before * whenAll was invoked. This will not keep track of tasks that are added on after whenAll * was invoked. */ private Task<Void> whenAll(Collection<TaskQueue> taskQueues) { List<Task<Void>> tasks = new ArrayList<>(); for (TaskQueue taskQueue : taskQueues) { Task<Void> task = taskQueue.enqueue(new Continuation<Void, Task<Void>>() { @Override public Task<Void> then(Task<Void> toAwait) throws Exception { return toAwait; } }); tasks.add(task); } return Task.whenAll(tasks); } private static class PauseException extends Exception { // This class was intentionally left blank. } }