Persistent Queues

Persistent Queues

Offloading tasks to the background is a great way to simulate the feeling of responsiveness. But a lot can go wrong. This talk takes a deep dive into the implementation of Tape, a library that lets your app reliably process tasks in the background. It explores the API, suggests patterns for common use cases, and suggest alternatives for when Tape's simplicity isn't enough.

Af62a2814aed79f5beacd0670f3eab99?s=128

Prateek Srivastava

October 27, 2015
Tweet

Transcript

  1. Persistent Queues

  2. None
  3. None
  4. None
  5. -Murphy’s Law “Anything that can go wrong, will go wrong.”

  6. None
  7. Networks are flaky

  8. None
  9. None
  10. Batteries are limited

  11. None
  12. None
  13. Memory is constrained

  14. None
  15. RAM is volatile

  16. Tasks must be durable

  17. Non-Volatile Storage

  18. Files java.io.File AtomicFile

  19. java.io.File lacks atomicity

  20. AtomicFile burdens caller with recovery

  21. if (backupFile.exists()) { file.delete(); backupFile.renameTo(file); }

  22. SharedPreferences loads entire queue into memory swallows errors

  23. SQLite complex

  24. Tape https://github.com/square/tape

  25. QueueFile https://www.parleys.com/tutorial/ 5148922b0364bc17fc56c97b/chapter15

  26. atomic durable efficient

  27. What do we start with?

  28. File System renaming is atomic fsync is durable segment writes

    are atomic
  29. class RandomAccessFile { void seek(long pos); <T> read{<T>}(); void write{<T>}(T

    data); }
  30. new RandomAccessFile(file, "rwd");

  31. “rwd” Open for reading and writing, as with "rw", and

    also require that every update to the file's content be written synchronously to the underlying storage device.
  32. Putting it all together

  33. None
  34. None
  35. None
  36. Adding

  37. queueFile.add(data); *not to scale

  38. int next = lastPosition + 4 + lastLength;

  39. raf.seek(next); raf.write(data.length);

  40. raf.seek(next + 4); raf.write(data);

  41. raf.seek(0); writeHeader(fileLength, elementCount + 1, firstPosition, newLastPosition);

  42. = =

  43. Removing

  44. queueFile.remove();

  45. int newFirstPosition = firstPosition + 4 + firstLength;

  46. writeHeader(fileLength, elementCount - 1, newFirstPosition, lastPosition);

  47. raf.seek(firstPosition); byte[] empty = new byte[firstLength + 4]; raf.write(empty);

  48. =

  49. API

  50. class QueueFile { void add(byte[] data); byte[] peek(); void remove();

    void clear(); int size(); void forEach(ElementReader reader); } @1.2.3
  51. interface ElementReader { void read(InputStream in, int length); }

  52. // Read the data at the head of the queue.

    byte[] element = queueFile.peek(); // Process it. process(element); // Remove if from the queue. queueFile.remove();
  53. example byte[] message = queueFile.peek(); if (message != null) {

    return; } HttpURLConnection connection = open("https://segment.com/import"); connection.getOutputStream().write(message);
  54. Handling Retries

  55. retry server and network errors

  56. ignore client errors

  57. try { connection.close(); } catch (IOException e) { // Network

    error, retry later. return; } int responseCode = connection.getResponseCode(); if (responseCode >= 500) { // Server error, retry later. return; } // Client error, e.g. invalid json. Don’t retry. if (responseCode >= 400) { log("server rejected message"); } queueFile.remove(); example
  58. Idempotent Tasks

  59. UUID.randomUUID().toString()

  60. Batch

  61. example if (queueFile.size() < 0) { return; } HttpURLConnection connection

    = open("https://segment.com/import"); OutputStream out = connection.getOutputStream(); queueFile.forEach((in, length) -> { Utils.copy(in, out); });
  62. example for (int i = 0; i < n; i++)

    { queueFile.remove(); }
  63. Enhanced Batch API

  64. class QueueFile { void remove(int n); int forEach(ElementVisitor visitor); }

    @master
  65. interface ElementVisitor { boolean read(InputStream in, int length); }

  66. interface ElementVisitor { boolean read(InputStream in, int length); }

  67. List<byte[]> elements = read(queueFile); process(elements); queueFile.remove(elements.size());

  68. example HttpURLConnection connection = open("https://segment.com/import"); OutputStream out = connection.getOutputStream(); *int

    count = 0; // simplified queueFile.forEach((in, length) -> { if (count + length > MAX_LIMIT) { return; } count += Utils.copy(in, out); return; });
  69. example HttpURLConnection connection = open("https://segment.com/import"); OutputStream out = connection.getOutputStream(); *int

    count = 0; // simplified queueFile.forEach((in, length) -> { if (count + length > MAX_LIMIT) { return false; } count += Utils.copy(in, out); return true; });
  70. example HttpURLConnection connection = open("https://segment.com/import"); OutputStream out = connection.getOutputStream(); *int

    count = 0; // simplified queueFile.forEach((in, length) -> { if (count + length > MAX_LIMIT) { return false; } count += Utils.copy(in, out); return true; });
  71. Queue<T> API

  72. @1.2.3 interface ObjectQueue<T> { void add(T entry); T peek(); void

    remove(); int size(); void setListener(Listener<T> listener); }
  73. interface Listener<T> { void onAdd(ObjectQueue<T> queue, T entry); void onRemove(ObjectQueue<T>

    queue); }
  74. @master class FileObjectQueue<T> { List<T> peek(int n); List<T> asList(); void

    remove(int n); }
  75. @1.2.3 interface Converter<T> { T from(byte[] bytes); void toStream(T o,

    OutputStream os); }
  76. FileObjectQueue Converter QueueFile

  77. Beyond Tape

  78. backoff priority *dependencies *battery, network

  79. Priority JobQueue https://github.com/path/android-priority-jobqueue

  80. JobScheduler https://developer.android.com/reference/android/app/job/ JobScheduler.html

  81. GCM Network Manager https://developers.google.com/cloud-messaging/network- manager

  82. Android Job https://github.com/evernote/android-job

  83. Cassette* https://github.com/segmentio/cassette

  84. Questions? @f2prateek f2prateek.com