All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.easimart.EasimartPinningEventuallyQueue Maven / Gradle / Ivy

package co.easimart;


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;

/**
 * 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 EasimartOperationSet is ready in
 *   {@link EasimartObject#taskQueue}.
 * - All rules apply on start from reboot.
 */
/** package */ class EasimartPinningEventuallyQueue extends EasimartEventuallyQueue {
  private static final String TAG = "EasimartPinningEventuallyQueue";

  /**
   * TCS that is held until a {@link EasimartOperationSet} is completed.
   */
  private HashMap.TaskCompletionSource> 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 EasimartObject#taskQueue} until they are both ready to process the same
   * EasimartOperationSet.
   */
  private TaskQueue operationSetTaskQueue = new TaskQueue();

  /**
   * List of {@link EasimartOperationSet#uuid} that are currently queued in
   * {@link EasimartPinningEventuallyQueue#operationSetTaskQueue}.
   */
  private ArrayList 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 Task.TaskCompletionSource connectionTaskCompletionSource = Task.create();
  private final Object connectionLock = new Object();
  private final EasimartHttpClient 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 EasimartPinningEventuallyQueue(Context context, EasimartHttpClient 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 EasimartTaskUtils.wait(pendingCountAsync());
    } catch (EasimartException e) {
      throw new IllegalStateException(e);
    }
  }

  public Task pendingCountAsync() {
    final Task.TaskCompletionSource tcs = Task.create();

    taskQueue.enqueue(new Continuation>() {
      @Override
      public Task then(Task toAwait) throws Exception {
        return pendingCountAsync(toAwait).continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            int count = task.getResult();
            tcs.setResult(count);
            return Task.forResult(null);
          }
        });
      }
    });

    return tcs.getTask();
  }

  public Task pendingCountAsync(Task toAwait) {
    return toAwait.continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        return EventuallyPin.findAllPinned().continueWithTask(new Continuation, Task>() {
          @Override
          public Task then(Task> task) throws Exception {
            List 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 {
      EasimartTaskUtils.wait(whenAll(Arrays.asList(taskQueue, operationSetTaskQueue)));
    } catch (EasimartException 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 waitForConnectionAsync() {
    synchronized (connectionLock) {
      return connectionTaskCompletionSource.getTask();
    }
  }

  /**
   * Pins the eventually operation on {@link EasimartPinningEventuallyQueue#taskQueue}.
   *
   * @return Returns a Task that will be resolved when the command completes.
   */
  @Override
  public Task enqueueEventuallyAsync(final EasimartRESTCommand command,
      final EasimartObject object) {
    Easimart.requirePermission(Manifest.permission.ACCESS_NETWORK_STATE);
    final Task.TaskCompletionSource tcs = Task.create();

    taskQueue.enqueue(new Continuation>() {
      @Override
      public Task then(Task toAwait) throws Exception {
        return enqueueEventuallyAsync(command, object, toAwait, tcs);
      }
    });

    return tcs.getTask();
  }

  private Task enqueueEventuallyAsync(final EasimartRESTCommand command,
      final EasimartObject object, Task toAwait, final Task.TaskCompletionSource tcs) {
    return toAwait.continueWithTask(new Continuation>() {
      @Override
      public Task then(Task toAwait) throws Exception {
        Task pinTask = EventuallyPin.pinEventuallyCommand(object, command);

        return pinTask.continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            EventuallyPin pin = task.getResult();
            Exception error = task.getError();
            if (error != null) {
              if (Easimart.LOG_LEVEL_WARNING >= Easimart.getLogLevel()) {
                EasimartLog.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>() {
              @Override
              public Task then(Task 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 EasimartPinningEventuallyQueue#taskQueue}.
   *
   * @return Returns a Task that is resolved when all EventuallyPins are enqueued in the
   * operationSetTaskQueue.
   */
  private Task populateQueueAsync() {
    return taskQueue.enqueue(new Continuation>() {
      @Override
      public Task then(Task toAwait) throws Exception {
        return populateQueueAsync(toAwait);
      }
    });
  }

  private Task populateQueueAsync(Task toAwait) {
    return toAwait.continueWithTask(new Continuation>>() {
      @Override
      public Task> then(Task task) throws Exception {
        // We don't want to enqueue any EventuallyPins that are already queued.
        return EventuallyPin.findAllPinned(eventuallyPinUUIDQueue);
      }
    }).onSuccessTask(new Continuation, Task>() {
      @Override
      public Task then(Task> task) throws Exception {
        List 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 EasimartPinningEventuallyQueue#operationSetTaskQueue}.
   *
   * Each eventually operation is run synchronously to maintain the order in which they were
   * enqueued.
   */
  private Task 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>() {
      @Override
      public Task then(final Task toAwait) throws Exception {
        return runEventuallyAsync(eventuallyPin, toAwait).continueWithTask(new Continuation>() {
          @Override
          public Task then(Task 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 EasimartObject to be ready.
   *
   * @return A task that is resolved when the eventually operation completes.
   */
  private Task runEventuallyAsync(final EventuallyPin eventuallyPin, final Task toAwait) {
    return toAwait.continueWithTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        return waitForConnectionAsync();
      }
    }).onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        return waitForOperationSetAndEventuallyPin(null, eventuallyPin).continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            Exception error = task.getError();
            if (error != null) {
              if (error instanceof PauseException) {
                // Bubble up the PauseException.
                return task.makeVoid();
              }

              if (Easimart.LOG_LEVEL_ERROR >= Easimart.getLogLevel()) {
                EasimartLog.e(TAG, "Failed to run command.", error);
              }

              notifyTestHelper(TestHelper.COMMAND_FAILED, error);
            } else {
              notifyTestHelper(TestHelper.COMMAND_SUCCESSFUL);
            }

            Task.TaskCompletionSource 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.TaskCompletionSource> pendingEventuallyTasks =
      new HashMap<>();

  /**
   * Map of eventually operation UUID to matching EasimartOperationSet.
   */
  private HashMap uuidToOperationSet = new HashMap<>();

  /**
   * Map of eventually operation UUID to matching EventuallyPin.
   */
  private HashMap uuidToEventuallyPin = new HashMap<>();

  /**
   * Synchronizes EasimartObject#taskQueue (Many) and EasimartCommandCache#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 EasimartObject}
   * @param eventuallyPin
   *          From {@link EasimartPinningEventuallyQueue}
   */
  //TODO (grantland): We can probably generalize this to synchronize/join more than 2 taskQueues
  @Override
  /* package */ Task waitForOperationSetAndEventuallyPin(EasimartOperationSet 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 Task.TaskCompletionSource 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>() {
      @Override
      public Task then(Task 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 process(final EventuallyPin eventuallyPin,
      final EasimartOperationSet operationSet) {

    return waitForConnectionAsync().onSuccessTask(new Continuation>() {
      @Override
      public Task then(Task task) throws Exception {
        final int type = eventuallyPin.getType();
        final EasimartObject object = eventuallyPin.getObject();
        String sessionToken = eventuallyPin.getSessionToken();

        Task 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) {
          EasimartRESTCommand 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>() {
          @Override
          public Task then(final Task executeTask) throws Exception {
            Exception error = executeTask.getError();
            if (error != null) {
              if (error instanceof EasimartException
                  && ((EasimartException) error).getCode() == EasimartException.CONNECTION_FAILED) {
                // We did our retry logic in EasimartRequest, 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>() {
              @Override
              public Task then(Task 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>() {
              @Override
              public Task then(Task 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 task = taskQueue.enqueue(new Continuation>() {
      @Override
      public Task then(Task toAwait) throws Exception {
        return toAwait.continueWithTask(new Continuation>() {
          @Override
          public Task then(Task task) throws Exception {
            return EventuallyPin.findAllPinned().onSuccessTask(new Continuation, Task>() {
                  @Override
                  public Task then(Task> task) throws Exception {
                    List pins = task.getResult();

                    List> tasks = new ArrayList<>();
                    for (EventuallyPin pin : pins) {
                      tasks.add(pin.unpinInBackground(EventuallyPin.PIN_NAME));
                    }
                    return Task.whenAll(tasks);
                  }
                });
          }
        });
      }
    });

    try {
      EasimartTaskUtils.wait(task);
    } catch (EasimartException 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 whenAll(Collection taskQueues) {
    List> tasks = new ArrayList<>();

    for (TaskQueue taskQueue : taskQueues) {
      Task task = taskQueue.enqueue(new Continuation>() {
        @Override
        public Task then(Task toAwait) throws Exception {
          return toAwait;
        }
      });

      tasks.add(task);
    }

    return Task.whenAll(tasks);
  }

  private static class PauseException extends Exception {
    // This class was intentionally left blank.
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy