[
  {
    "path": ".gitignore",
    "content": "/target\n/classes\n/checkouts\npom.xml\npom.xml.asc\n*.jar\n*.class\n/.lein-*\n/.nrepl-port\n.hgignore\n.hg/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\nAll notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).\n\n## [Unreleased]\n### Changed\n- Add a new arity to `make-widget-async` to provide a different widget shape.\n\n## [0.1.1] - 2018-04-02\n### Changed\n- Documentation on how to make the widgets.\n\n### Removed\n- `make-widget-sync` - we're all async, all the time.\n\n### Fixed\n- Fixed widget maker to keep working when daylight savings switches over.\n\n## 0.1.0 - 2018-04-02\n### Added\n- Files from the new template.\n- Widget maker public API - `make-widget-sync`.\n\n[Unreleased]: https://github.com/your-name/tea-time/compare/0.1.1...HEAD\n[0.1.1]: https://github.com/your-name/tea-time/compare/0.1.0...0.1.1\n"
  },
  {
    "path": "LICENSE",
    "content": "THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC\nLICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM\nCONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\na) in the case of the initial Contributor, the initial code and\ndocumentation distributed under this Agreement, and\n\nb) in the case of each subsequent Contributor:\n\ni) changes to the Program, and\n\nii) additions to the Program;\n\nwhere such changes and/or additions to the Program originate from and are\ndistributed by that particular Contributor. A Contribution 'originates' from\na Contributor if it was added to the Program by such Contributor itself or\nanyone acting on such Contributor's behalf. Contributions do not include\nadditions to the Program which: (i) are separate modules of software\ndistributed in conjunction with the Program under their own license\nagreement, and (ii) are not derivative works of the Program.\n\n\"Contributor\" means any person or entity that distributes the Program.\n\n\"Licensed Patents\" mean patent claims licensable by a Contributor which are\nnecessarily infringed by the use or sale of its Contribution alone or when\ncombined with the Program.\n\n\"Program\" means the Contributions distributed in accordance with this\nAgreement.\n\n\"Recipient\" means anyone who receives the Program under this Agreement,\nincluding all Contributors.\n\n2. GRANT OF RIGHTS\n\na) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free copyright license to\nreproduce, prepare derivative works of, publicly display, publicly perform,\ndistribute and sublicense the Contribution of such Contributor, if any, and\nsuch derivative works, in source code and object code form.\n\nb) Subject to the terms of this Agreement, each Contributor hereby grants\nRecipient a non-exclusive, worldwide, royalty-free patent license under\nLicensed Patents to make, use, sell, offer to sell, import and otherwise\ntransfer the Contribution of such Contributor, if any, in source code and\nobject code form.  This patent license shall apply to the combination of the\nContribution and the Program if, at the time the Contribution is added by the\nContributor, such addition of the Contribution causes such combination to be\ncovered by the Licensed Patents. The patent license shall not apply to any\nother combinations which include the Contribution. No hardware per se is\nlicensed hereunder.\n\nc) Recipient understands that although each Contributor grants the licenses\nto its Contributions set forth herein, no assurances are provided by any\nContributor that the Program does not infringe the patent or other\nintellectual property rights of any other entity. Each Contributor disclaims\nany liability to Recipient for claims brought by any other entity based on\ninfringement of intellectual property rights or otherwise. As a condition to\nexercising the rights and licenses granted hereunder, each Recipient hereby\nassumes sole responsibility to secure any other intellectual property rights\nneeded, if any. For example, if a third party patent license is required to\nallow Recipient to distribute the Program, it is Recipient's responsibility\nto acquire that license before distributing the Program.\n\nd) Each Contributor represents that to its knowledge it has sufficient\ncopyright rights in its Contribution, if any, to grant the copyright license\nset forth in this Agreement.\n\n3. REQUIREMENTS\n\nA Contributor may choose to distribute the Program in object code form under\nits own license agreement, provided that:\n\na) it complies with the terms and conditions of this Agreement; and\n\nb) its license agreement:\n\ni) effectively disclaims on behalf of all Contributors all warranties and\nconditions, express and implied, including warranties or conditions of title\nand non-infringement, and implied warranties or conditions of merchantability\nand fitness for a particular purpose;\n\nii) effectively excludes on behalf of all Contributors all liability for\ndamages, including direct, indirect, special, incidental and consequential\ndamages, such as lost profits;\n\niii) states that any provisions which differ from this Agreement are offered\nby that Contributor alone and not by any other party; and\n\niv) states that source code for the Program is available from such\nContributor, and informs licensees how to obtain it in a reasonable manner on\nor through a medium customarily used for software exchange.\n\nWhen the Program is made available in source code form:\n\na) it must be made available under this Agreement; and\n\nb) a copy of this Agreement must be included with each copy of the Program.\n\nContributors may not remove or alter any copyright notices contained within\nthe Program.\n\nEach Contributor must identify itself as the originator of its Contribution,\nif any, in a manner that reasonably allows subsequent Recipients to identify\nthe originator of the Contribution.\n\n4. COMMERCIAL DISTRIBUTION\n\nCommercial distributors of software may accept certain responsibilities with\nrespect to end users, business partners and the like. While this license is\nintended to facilitate the commercial use of the Program, the Contributor who\nincludes the Program in a commercial product offering should do so in a\nmanner which does not create potential liability for other Contributors.\nTherefore, if a Contributor includes the Program in a commercial product\noffering, such Contributor (\"Commercial Contributor\") hereby agrees to defend\nand indemnify every other Contributor (\"Indemnified Contributor\") against any\nlosses, damages and costs (collectively \"Losses\") arising from claims,\nlawsuits and other legal actions brought by a third party against the\nIndemnified Contributor to the extent caused by the acts or omissions of such\nCommercial Contributor in connection with its distribution of the Program in\na commercial product offering.  The obligations in this section do not apply\nto any claims or Losses relating to any actual or alleged intellectual\nproperty infringement. In order to qualify, an Indemnified Contributor must:\na) promptly notify the Commercial Contributor in writing of such claim, and\nb) allow the Commercial Contributor to control, and cooperate with the\nCommercial Contributor in, the defense and any related settlement\nnegotiations. The Indemnified Contributor may participate in any such claim\nat its own expense.\n\nFor example, a Contributor might include the Program in a commercial product\noffering, Product X. That Contributor is then a Commercial Contributor. If\nthat Commercial Contributor then makes performance claims, or offers\nwarranties related to Product X, those performance claims and warranties are\nsuch Commercial Contributor's responsibility alone. Under this section, the\nCommercial Contributor would have to defend claims against the other\nContributors related to those performance claims and warranties, and if a\ncourt requires any other Contributor to pay any damages as a result, the\nCommercial Contributor must pay those damages.\n\n5. NO WARRANTY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON\nAN \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER\nEXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR\nCONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A\nPARTICULAR PURPOSE. Each Recipient is solely responsible for determining the\nappropriateness of using and distributing the Program and assumes all risks\nassociated with its exercise of rights under this Agreement , including but\nnot limited to the risks and costs of program errors, compliance with\napplicable laws, damage to or loss of data, programs or equipment, and\nunavailability or interruption of operations.\n\n6. DISCLAIMER OF LIABILITY\n\nEXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY\nCONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION\nLOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE\nEXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY\nOF SUCH DAMAGES.\n\n7. GENERAL\n\nIf any provision of this Agreement is invalid or unenforceable under\napplicable law, it shall not affect the validity or enforceability of the\nremainder of the terms of this Agreement, and without further action by the\nparties hereto, such provision shall be reformed to the minimum extent\nnecessary to make such provision valid and enforceable.\n\nIf Recipient institutes patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Program itself\n(excluding combinations of the Program with other software or hardware)\ninfringes such Recipient's patent(s), then such Recipient's rights granted\nunder Section 2(b) shall terminate as of the date such litigation is filed.\n\nAll Recipient's rights under this Agreement shall terminate if it fails to\ncomply with any of the material terms or conditions of this Agreement and\ndoes not cure such failure in a reasonable period of time after becoming\naware of such noncompliance. If all Recipient's rights under this Agreement\nterminate, Recipient agrees to cease use and distribution of the Program as\nsoon as reasonably practicable. However, Recipient's obligations under this\nAgreement and any licenses granted by Recipient relating to the Program shall\ncontinue and survive.\n\nEveryone is permitted to copy and distribute copies of this Agreement, but in\norder to avoid inconsistency the Agreement is copyrighted and may only be\nmodified in the following manner. The Agreement Steward reserves the right to\npublish new versions (including revisions) of this Agreement from time to\ntime. No one other than the Agreement Steward has the right to modify this\nAgreement. The Eclipse Foundation is the initial Agreement Steward. The\nEclipse Foundation may assign the responsibility to serve as the Agreement\nSteward to a suitable separate entity. Each new version of the Agreement will\nbe given a distinguishing version number. The Program (including\nContributions) may always be distributed subject to the version of the\nAgreement under which it was received. In addition, after a new version of\nthe Agreement is published, Contributor may elect to distribute the Program\n(including its Contributions) under the new version. Except as expressly\nstated in Sections 2(a) and 2(b) above, Recipient receives no rights or\nlicenses to the intellectual property of any Contributor under this\nAgreement, whether expressly, by implication, estoppel or otherwise. All\nrights in the Program not expressly granted under this Agreement are\nreserved.\n\nThis Agreement is governed by the laws of the State of New York and the\nintellectual property laws of the United States of America. No party to this\nAgreement will bring a legal action under this Agreement more than one year\nafter the cause of action arose. Each party waives its rights to a jury trial\nin any resulting litigation.\n"
  },
  {
    "path": "README.md",
    "content": "# Tea-Time\n\n> There was a disaster hanging silently in the air around him waiting for him to\n> notice it. His knees tingled.\n>\n> What he needed, he had been thinking, was a client. He had been thinking that\n> as a matter of habit. It was what he always thought at this time of the\n> morning. What he had forgotten was that he had one.\n>\n> He stared wildly at his watch. Nearly eleven-thirty. He shook his head to try\n> and clear the silent ringing between his ears, then made a hysterical lunge\n> for his hat and his great leather coat that hung behind the door.\n>\n> Fifteen seconds later he left the house, five hours late but moving fast.\n>\n>  --Douglas Adams, \"The Long Dark Tea-Time of the Soul\"\n\nMany programs need to interact with clocks: reading the current time,\nscheduling some operation to be done at a particular time or a few seconds from\nnow, or performing some housekeeping task every few seconds. They may need to\ndynamically create new tasks, push them back to a later time, and cancel them\nwhen they are no longer needed. Moreover, testing these real-time behaviors for\nside effects is notoriously slow and buggy. Tea-Time is a minimal Clojure\nlibrary which provides a global, lightweight, and testable scheduler for\nexactly these purposes.\n\nTea-Time is adapted from the scheduler in Riemann, a distributed systems\nmonitoring server, where it has served for several years in\nmoderate-performance, long-running deployments. It's not perfect, but its API\nand functionality have proven useful and stable.\n\nConsistent use of Tea-Time can make it easier to write and test programs which\ninteract with wall clocks. With one call, you can switch from using wall clocks\nto a virtual time, which advances only when you tell it to; scheduled tasks\nevaluate synchronously, appearing to execute exactly at their target times.\nCallers callers will read the virtual clock rather than the system clock. This\nallows you to write tests for hours of \"real-time\" behavior which execute deterministically, in milliseconds.\n\nTea-Time is not for working with dates or human times; it works purely in\nmicroseconds and the posix timescale. Tea-Time is not a parser or formatter.\nThere's no notion of intervals or calendars. These are all admirable goals,\nbetter served by Joda Time, Juxt's Tick, et al.\n\n## Installation\n\n[![Clojars Project](https://img.shields.io/clojars/v/tea-time.svg)](https://clojars.org/tea-time)\n\n## Quick Tour\n\n```clj\nuser=> (require '[tea-time.core :as tt])\nnil\nuser=> (tt/unix-time-micros)   ; Wall clock in microseconds\n1522776026066000\nuser=> (tt/linear-time-micros) ; Monotonic clock in microseconds\n128572305580\nuser=> (tt/start!)             ; Start threadpool\n\n; Say hi after 1 second\nuser=> (tt/after! 1 (bound-fn [] (prn :hi)))\n#tea_time.core.Once\n{:cancelled #<Atom@6f21520e false>\n :f #<Fn@4919b682 clojure.core/bound_fn_STAR_[fn]>\n :id 1\n :t 128599825869}\n\n; One second later...\n:hi\n\n; Every 10 seconds (after a 2 second wait)...\nuser=> (def dirk (tt/every! 10 2 (bound-fn [] (prn \"THAT is a thing\"))))\n\"THAT is a thing\"\n\"THAT is a thing\"\n\"THAT is a thing\"\n...\n\n; Defer the next execution until 30 seconds from now\nuser=> (tt/defer! dirk 30)\n129800.514632\n... Ah, a breather ...\n\"THAT is a thing\"\n\n; That's over, it's cancelled\nuser=> (tt/cancel! dirk)\ntrue\n```\n\n## Working with Clocks\n\nThe core library is one namespace:\n\n```clj\nuser=> (require '[tea-time.core :as tt])\nnil\n```\n\nInternally, Tea-time uses microseconds, represented as 64-bit signed longs, for\na balance of speed, representability, and precision. There are two timescales.\nThe Unix timescale, which is derived from System/currentTimeMillis,\napproximately tracks \"wall clock time\", and can flow unevenly or even\nbackwards.\n\n```clj\nuser=> (tt/unix-time-micros)\n1522772393355000\n```\n\nFor convenience and where precision is not critical, we also provide times in seconds, represented as 64-bit doubles.\n\n```clj\nuser=> (tt/unix-time)\n1.522772450458E9\nuser=> (long (tt/unix-time))\n1522772475\n```\n\nYou can convert back and forth:\n\n```clj\nuser=> (tt/seconds->micros 1.2)\n1200000\nuser=> (tt/micros->seconds 200)\n2.0E-4\n```\n\nThe linear timescale is derived from System/nanoTime, and advances\nmonotonically. However, it is not synchronized to any thing in particular, and\ncan only be used within a single JVM.\n\n```clj\nuser=> (tt/linear-time)\n125261.653199\nuser=> (tt/linear-time-micros)\n125266476873\n```\n\nUse the linear timescale to measure relative times, e.g. the time it takes to\nperform something in a single JVM. Use the Unix timescale to schedule things\nthat should be roughly synchronized across multiple JVMs. Do not use any time\nfor safety-critical applications: the list of ways clocks can go wrong is\neffectively unbounded.\n\n```clj\nuser=> (let [t1 (tt/linear-time)]\n         (Thread/sleep 1000)\n         (- (tt/linear-time) t1))\n1.0001519999932498\n```\n\n## One-time Tasks\n\nFirst, start the Tea-Time threadpool. This is a global set of worker threads which will evaluate scheduled tasks.\n\n```clj\nuser=> (tt/start!)\n```\n\nYou can stop the threadpool later with `tt/stop!`, which will politely finish\nexecution of any tasks currently being evaluated, and block until all threads\nhave exited.\n\nTo schedule a task after n seconds, use `after!`\n\n```clj\nuser=> (def task (tt/after! 2 (bound-fn [] (prn \"I took two seconds\"))))\n#'user/task\n... wait two seconds...\nuser=> \"I took two seconds\"\n```\n\nWe use `bound-fn` here to retain a handle to the repl's stdout, so `prn` works.\nRegular `fn` works fine in most cases, and if you use a logger like\n`clojure.tools.logging`, it'll work fine with plain old `fn` too.\n\nYou can *cancel* a task: if it hasn't been executed yet, it won't be when it\ncomes due. Canceling an already completed task is legal, but does nothing.\n\n```clj\nuser=> (def task (tt/after! 10 (bound-fn [] (prn \"I took ten seconds\"))))\n#'user/task\nuser=> (tt/cancel! task)\ntrue\n; ... nothing happens ...\n```\n\n## Recurring tasks\n\nTo schedule a recurring task, which should execute every n seconds, use\n`every!`. Every takes an interval in seconds, and starts immediately.\n\n```clj\n(def task (tt/every! 2 (bound-fn [] (prn :hi))))\n:hi\n#'user/task\nuser=> :hi\n:hi\n:hi\nuser=> (tt/cancel! task)\ntrue\n; ... no more :hi's\n```\n\nYou can also defer the first execution by providing an initial delay. To run\nevery 2 seconds, starting 5 seconds from now, say `(tt/every! 2 5 (bound-fn\n(prn :hi)))`.\n\nRecurrent tasks are also *deferrable*: you can push back the execution time to\nto 10 seconds *from now*.\n\n```clj\nuser=> (def task (tt/every! 2 (bound-fn [] (prn :hi))))\n:hi\n:hi\nuser=> (tt/defer! task 10)\n126565647078\n; Ahhh, a brief respite\n:hi\n:hi\n```\n\nThis is particularly helpful for streaming or batching systems that accrue\nevents over time, and if nothing transpires for a few seconds, should flush\ntheir state. Tea-Time makes `defer!` cheap, so you can call it on every event.\n\n## Testing with Virtual Time\n\nTesting real-time systems is *hard*: you usually wind up with a morass of sleep\nstatements, barriers, and weird race conditions. Tea-Time includes a hook to\nrun time-based tests *deterministically*.\n\nFirst, make sure the scheduler is stopped, and pull in the virtual namespace.\n\n```clj\nuser=> (tt/stop!)\n[]\nuser=> (require '[tea-time.virtual :as tv])\nnil\n```\n\nUse the `with-virtual-time!` macro to evaluate code with a virtual clock and\nscheduler.\n\n```clj\nuser=> (tv/with-virtual-time! (tt/unix-time))\n0.0\nuser=> (tv/with-virtual-time! (tt/unix-time))\n0.0\nuser=> (tv/with-virtual-time! (tt/linear-time))\n0.0\nuser=> (tv/with-virtual-time! (tt/linear-time))\n0.0\n```\n\nTime is *frozen* at 0 microseconds. Let's schedule some tasks.\n\n```clj\nuser=> (tv/with-virtual-time!\n          (tt/after! 2500 (bound-fn []\n            (prn \"I'm task 1, clocks are\" (tt/unix-time) (tt/linear-time)))))\n          (tt/after! 1.23 (bound-fn []\n            (prn \"I'm task 2, clocks are\" (tt/unix-time) (tt/linear-time))))))\n```\n\nNothing will happen. Time is still frozen. Let's jump forward to one second:\n\n```clj\nuser=> (tv/advance! 1)\n1.0\nuser=> (tv/with-virtual-time! (tt/unix-time))\n1.0\n```\n\nThe clock is now 1, but nothing has happened. Let's jump ahead to an hour:\n\n```clj\nuser=> (tv/with-virtual-time! (tv/advance! 3600))\n\"I'm task 2, clocks are\" 1.23 1.23\n\"I'm task 1, clocks are\" 2500.0 2500.0\n3600.0\n```\n\nNote that the tasks evaluated in their scheduled order--t2 before t1--and each\ntask observed the correct linear and unix times. So long as code uses\ntea-time's wrappers, we can test hours of \"real-time\" behavior in a few\nmilliseconds, and obtain *deterministic* execution.\n\nTo reset the virtual clock to zero and clear all tasks, use\n\n```clj\nuser=> (tv/reset-time!)\nnil\nuser=> (tv/with-virtual-time! (tt/unix-time))\n0.0\n```\n\nWe provide a pair of handy fixtures for writing clojure.tests using virtualized\ntime:\n\n```clj\n(use-fixtures :once tv/call-with-virtual-time!)\n(use-fixtures :each tv/reset-time!)\n```\n\nSee `tests/` for additional examples.\n\n## License\n\nCopyright © 2018 Kyle Kingsbury\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n"
  },
  {
    "path": "doc/intro.md",
    "content": "# Introduction to tea-time\n\nTODO: write [great documentation](http://jacobian.org/writing/what-to-write/)\n"
  },
  {
    "path": "project.clj",
    "content": "(defproject tea-time \"1.0.1\"\n  :description \"A simple, testable scheduler for asynchronous, cancellable, possibly periodic computation.\"\n  :url \"https://github.com/aphyr/tea-time\"\n  :license {:name \"Eclipse Public License\"\n            :url \"http://www.eclipse.org/legal/epl-v10.html\"}\n  :dependencies [[org.clojure/tools.logging \"0.4.1\"]]\n  :profiles {:dev {:dependencies [[org.clojure/clojure \"1.9.0\"]]}})\n"
  },
  {
    "path": "src/tea_time/core.clj",
    "content": "(ns tea-time.core\n  \"Clocks and scheduled tasks. Provides functions for getting the current time\n  and running functions (Tasks) at specific times and periods. Includes a\n  threadpool for task execution, controlled by (start!) and (stop!).\"\n  (:import [java.util.concurrent ConcurrentSkipListSet]\n           [java.util.concurrent.locks LockSupport])\n  (:require [clojure.stacktrace    :refer [print-stack-trace]]\n            [clojure.tools.logging :refer [warn info]]))\n\n(defprotocol Task\n  (succ [task]\n    \"The successive task to this one.\")\n  (run [task]\n    \"Executes this task.\")\n  (cancel! [task]\n    \"Cancel this task.\"))\n\n(defprotocol Deferrable\n  (defer! [this delay]\n    \"Schedule a task for a new time measured in seconds from now\")\n\n  (defer-micros! [this delay]\n    \"Schedule a task for a new time measured in microseconds from now\"))\n\n\n;; The clock implementation ;;;;;;;;;;;;;;;;;;;;;\n\n(defn real-unix-time-micros\n  \"The current unix epoch time in microseconds, taken from\n  System/currentTimeMillis\"\n  ^long\n  []\n  (* (System/currentTimeMillis) 1000))\n\n(defn real-linear-time-micros\n  \"A current time on a linear scale with no fixed epoch; counts in\n  microseconds. Unlike unix-time, which can pause, skip, or flow backwards,\n  advances mostly monotonically at (close) to physical time, one second per\n  second.\"\n  ^long\n  []\n  (long (/ (System/nanoTime) 1000)))\n\n(defn micros->seconds\n  \"Convert microseconds to seconds, as doubles.\"\n  ^double\n  [t]\n  (/ t 1000000.0))\n\n(defn seconds->micros\n  \"Convert seconds to microseconds, as longs.\"\n  ^long\n  [t]\n  (long (* t 1000000)))\n\n(defn ^double real-unix-time\n  \"The current unix epoch time in seconds, taken from System/currentTimeMillis\"\n  ^double\n  []\n  (micros->seconds (real-unix-time-micros)))\n\n(defn real-linear-time\n  \"The current linear time in seconds, taken from System/nanoTime\"\n  ^double\n  []\n  (micros->seconds (real-linear-time-micros)))\n\n;; The clock API ;;;;;;;;;;;;;;;;;;;;;;;;;\n\n(def unix-time-micros\n  \"Rebindable alias for real-unix-time-micros\"\n  real-unix-time-micros)\n\n(def linear-time-micros\n  \"Rebindable alias for real-linear-time-micros\"\n  real-linear-time-micros)\n\n(def unix-time\n  \"Rebindable alias for real-unix-time\"\n  real-unix-time)\n\n(def linear-time\n  \"Rebindable alias for real-linear-time\"\n  real-linear-time)\n\n;; More conversions ;;;;;;;;;;;;;;;;;;;;;\n\n(defn unix-micros->linear-micros\n  \"Converts an instant in the unix timescale to an instant on the linear\n  timescale, approximately.\"\n  ^long\n  [^long unix]\n  (+ (linear-time-micros) (- unix (unix-time-micros))))\n\n;; Global state ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n; TODO: pull this stuff out into some sort of configurable Scheduler datatype,\n; and provide a global default?\n\n(def max-task-id\n  (atom 0))\n\n(def ^ConcurrentSkipListSet tasks\n  \"Scheduled operations.\"\n  (ConcurrentSkipListSet.\n    (fn [a b] (compare [(:t a) (:id a)]\n                       [(:t b) (:id b)]))))\n\n(def thread-count\n  \"Number of threads in the threadpool\"\n  4)\n\n(def park-interval-micros\n  \"Time we might sleep when nothing is scheduled, in micros.\"\n  10000)\n\n(def threadpool\n  (atom []))\n\n(def running\n  \"Whether the threadpool is currently supposed to be alive.\"\n  (atom false))\n\n;; Scheduling guts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n(defn ceil\n  \"Ceiling. For integers, identity. For other things, uses Math/ceil and\n  coerces to long.\"\n  [x]\n  (if (integer? x)\n    x\n    (long (Math/ceil x))))\n\n(defn task-id\n  \"Return a new task ID.\"\n  []\n  (swap! max-task-id inc))\n\n(defn next-tick\n  \"Given a period dt, beginning at some point in time anchor, finds the next\n  tick after time now, such that the next tick is separate from anchor by an\n  exact multiple of dt. If now is omitted, defaults to (linear-time), and both\n  anchor and dt are in seconds. If now is passed, anchor, dt, and now can be in\n  any unit, so long as they all agree.\"\n  ([anchor dt]\n   (next-tick anchor dt (linear-time)))\n  ([anchor dt now]\n   (+ now (- dt (mod (- now anchor) dt)))))\n\n; Look at all these bang! methods! Mutability is SO EXCITING!\n\n(defn reset-tasks!\n  \"Resets the task queue to empty, without triggering side effects.\"\n  []\n  (.clear tasks))\n\n(defn poll-task!\n  \"Removes the next task from the queue.\"\n  []\n  (.pollFirst tasks))\n\n(defn schedule-sneaky!\n  \"Schedules a task. Does *not* awaken any threads.\"\n  [task]\n  (.add tasks task)\n  task)\n\n(defn schedule!\n  \"Schedule a task. May awaken a thread from the threadpool to investigate.\"\n  [task]\n  (schedule-sneaky! task)\n  (when @running\n    (LockSupport/unpark (rand-nth @threadpool)))\n  task)\n\n;; Task datatypes ;;;;;;;;;;;;;;;;;;;;;;;\n\n(defrecord Once [id f ^long t cancelled]\n  Task\n  (succ [this] nil)\n  (run [this] (when-not @cancelled (f)))\n  (cancel! [this]\n          (reset! cancelled true)))\n\n(defrecord Every [id f ^long t ^long interval deferred-t cancelled]\n  Task\n  (succ [this]\n        (when-not @cancelled\n          (let [next-time (or @deferred-t (+ t interval))]\n            (reset! deferred-t nil)\n            (assoc this :t next-time))))\n\n  (run [this]\n       (when-not (or @deferred-t @cancelled) (f)))\n\n  (cancel! [this]\n          (reset! cancelled true))\n\n  Deferrable\n  (defer! [this delay]\n    (micros->seconds (defer-micros! this (seconds->micros delay))))\n\n  (defer-micros! [this delay]\n    (reset! deferred-t (+ (linear-time-micros) delay))))\n\n(defn at-linear-micros!\n  \"Calls f at t microseconds on the linear timescale.\"\n  [t f]\n  (schedule! (Once. (task-id) f t (atom false))))\n\n(defn at-unix-micros!\n  \"Calls f at t microseconds on the unix timescale. We convert this time to the\n  linear timescale, so it may behave oddly across leap seconds.\"\n  [t f]\n  (at-linear-micros! (unix-micros->linear-micros t) f))\n\n(defn at-unix!\n  \"Calls f at t seconds on the unix timescale. We convert this time to\n  the linear timescale, so it may behave oddly across leap seconds.\"\n  [t f]\n  (at-unix-micros! (seconds->micros t) f))\n\n(defn after!\n  \"Calls f after delay seconds.\"\n  [delay f]\n  (schedule! (Once. (task-id)\n                    f\n                    (+ (linear-time-micros) (seconds->micros delay))\n                    (atom false))))\n\n(defn every!\n  \"Calls f every interval seconds, after delay, also in seconds. If no delay is\n  provided, starts immediately.\"\n  ([interval f]\n   (every! interval 0 f))\n  ([interval delay f]\n   (assert (not (neg? delay)))\n   (schedule! (Every. (task-id)\n                      f\n                      (+ (linear-time-micros) (seconds->micros delay))\n                      (seconds->micros interval)\n                      (atom nil)\n                      (atom false)))))\n\n(defn run-tasks!\n  \"While running, takes tasks from the queue and executes them when ready. Will\n  park the current thread when no tasks are available.\"\n  [i]\n  (while @running\n    (try\n      (if-let [task (poll-task!)]\n        ; We've acquired a task.\n        (do\n          ; (info \"Task acquired\")\n          (if (<= (:t task) (linear-time-micros))\n            ; This task is ready to run\n            (do\n              ;(info :task task :time (linear-time-micros))\n              ; Run task\n              (try\n                (run task)\n                (catch Exception e\n                  (warn e \"Tea-Time task\" task \"threw\"))\n                (catch AssertionError t\n                  (warn t \"Tea-Time task\" task \"threw\")))\n              (when-let [task' (succ task)]\n                ; Schedule the next task.\n                (schedule-sneaky! task')))\n            (do\n              ; Return task.\n              (schedule-sneaky! task)\n              ; Park until that task comes up next. We can't use parkUntil cuz\n              ; it uses posix time which is non-monotonic. WHYYYYYY Note that\n              ; we're sleeping 100 microseconds minimum, and aiming to wake up\n              ; 1 ms before, so we have a better chance of actually executing\n              ; on time.\n              (->> (- (:t task) (linear-time-micros) 1000)\n                   (max 10)\n                   (min park-interval-micros)\n                   (* 1000)\n                   LockSupport/parkNanos))))\n\n        ; No task available; park for a bit and try again.\n        (LockSupport/parkNanos (* 1000 park-interval-micros)))\n      (catch Exception e\n      (warn e \"tea-time task threw\"))\n    (catch AssertionError t\n      (warn t \"tea-time task threw\")))))\n\n(defn stop!\n  \"Stops the task threadpool. Waits for threads to exit. Repeated calls to stop\n  are noops.\"\n  []\n  (locking threadpool\n    (when @running\n      (reset! running false)\n      (while (some #(.isAlive ^Thread %) @threadpool)\n        ; Allow at most 1/10th park-interval to pass after all threads exit.\n        (Thread/sleep (/ park-interval-micros 10000)))\n      (reset! threadpool []))))\n\n(defn start!\n  \"Starts the threadpool to execute tasks on the queue automatically. Repeated\n  calls to start are noops.\"\n  []\n  (locking threadpool\n    (when-not @running\n      (reset! running true)\n      (reset! threadpool\n              (map (fn [i]\n                     (let [^Runnable f (bound-fn [] (run-tasks! i))]\n                       (doto (Thread. f (str \"Tea-Time \" i))\n                         (.start))))\n                   (range thread-count))))))\n\n(def threadpool-users\n  \"Number of callers who would like a threadpool open right now\"\n  (atom 0))\n\n(defmacro with-threadpool\n  \"Ensures the threadpool is running within `body`, which is evaluated in an\n  implicit `do`. Multiple threads can call with-threadpool\n  concurrently. If any thread is within `with-threadpool`, the pool will run,\n  and when no threads are within `with-threadpool`, the pool will shut down.\n\n  You'll probably put this in the main entry points to your program, so the\n  threadpool runs for the entire life of the program.\"\n  [& body]\n  `(try (when (= 1 (swap! threadpool-users inc))\n          (start!))\n        ~@body\n        (finally\n          (when (= 0 (swap! threadpool-users dec))\n            (stop!)))))\n"
  },
  {
    "path": "src/tea_time/virtual.clj",
    "content": "(ns tea-time.virtual\n  \"Provides controllable periodic and deferred execution. Calling (advance!\n  target-time-in-seconds) moves the clock forward, triggering events that would\n  have occurred, in sequence. Each task executes exactly at its target time.\"\n  (:require [tea-time.core :refer :all]\n            [clojure.tools.logging :refer [info]]))\n\n(def clock\n  \"Reference to the current time, in microseconds.\"\n  (atom 0))\n\n(defn reset-clock!\n  []\n  (reset! clock 0))\n\n(defn reset-time!\n  \"Resets the clock and task queue. If a function is given, calls f after\n  resetting the time and task list.\"\n  ([f] (reset-time!) (f))\n  ([]\n   (reset-clock!)\n   (reset-tasks!)))\n\n(defn set-time!\n  \"Sets the current time in seconds, without triggering callbacks.\"\n  [t]\n  (reset! clock (seconds->micros t)))\n\n(defn virtual-unix-time-micros\n  []\n  @clock)\n\n(defn virtual-linear-time-micros\n  []\n  @clock)\n\n(defn virtual-unix-time\n  []\n  (micros->seconds @clock))\n\n(defn virtual-linear-time\n  []\n  (micros->seconds @clock))\n\n(defn advance!\n  \"Advances the clock to t seconds, triggering side effects. Tasks are run\n  synchronously on this thread, and their exceptions will be thrown here.\"\n  [t]\n  (let [t (seconds->micros t)]\n    (when (< @clock t)\n      (loop []\n        (when-let [task (poll-task!)]\n          (if (<= (:t task) t)\n            (do\n              ; Consume task\n              (swap! clock max (:t task))\n              (run task)\n              (when-let [task' (succ task)]\n                (schedule-sneaky! task'))\n              (recur))\n            ; Return task\n            (schedule-sneaky! task))))\n      (micros->seconds (swap! clock max t)))))\n\n(defmacro with-virtual-time!\n  \"Switches time functions to virtual counterparts, evaluates body, and\n  returns. Not at all threadsafe; bindings take effect globally. This is only\n  for testing.\"\n  [& body]\n  ; Please forgive me\n  `(with-redefs [tea-time.core/unix-time          virtual-unix-time\n                 tea-time.core/unix-time-micros   virtual-unix-time-micros\n                 tea-time.core/linear-time        virtual-linear-time\n                 tea-time.core/linear-time-micros virtual-linear-time-micros]\n     ~@body))\n\n(defn call-with-virtual-time!\n  \"Switches time functions to time.controlled counterparts, invokes f,\n  then restores them. Definitely not threadsafe. Not safe by any standard,\n  come to think of it. Only for testing purposes.\"\n  [f]\n  (with-virtual-time! (f)))\n"
  },
  {
    "path": "test/tea_time/core_test.clj",
    "content": "(ns tea-time.core-test\n  (:require [tea-time.core :refer :all]\n            [clojure.test :refer :all]\n            [clojure.tools.logging :refer [info]]))\n\n\n(defn reset-time!\n  [f]\n  (stop!)\n  (reset-tasks!)\n  (start!)\n  (f)\n  (stop!)\n  (reset-tasks!))\n(use-fixtures :each reset-time!)\n\n(deftest next-tick-test\n         (are [anchor dt now next] (= (next-tick anchor dt now) next)\n              0 1 0 1\n              0 2 0 2\n              1 1 0 1\n              2 1 0 1\n              0 2 0 2\n              0 2 1 2\n              0 2 2 4\n              2 2 2 4\n              4 2 2 4\n              1 2 1 3\n              1 2 2 3\n              1 2 3 5))\n\n(deftest ^:time clock-test\n         (is (< -1\n                (- (/ (System/currentTimeMillis) 1000) (unix-time))\n                1)))\n\n(deftest ^:time after-test\n         \"Run a function once, to verify that the threadpool works at all.\"\n         (let [t0 (unix-time)\n               results (atom [])]\n           (after! 0.1 #(swap! results conj (- (unix-time) t0)))\n           (Thread/sleep 300)\n           (prn :dt @results)\n           (is (<= 0.09 (first @results) 0.11))))\n\n; LMAO if this test becomes hilariously unstable and/or exhibits genuine\n; heisenbugs for any unit of time smaller than 250ms.\n(deftest ^:time defer-cancel-test\n         (let [x1 (atom 0)\n               x2 (atom 0)\n               t1 (every! 1 (fn [] (swap! x1 inc)))\n               t2 (every! 1 1 #(swap! x2 inc))]\n           (Thread/sleep 500)\n           (is (= 1 @x1))\n           (is (= 0 @x2))\n\n           (Thread/sleep 1000)\n           (is (= 2 @x1))\n           (is (= 1 @x2))\n\n           ; Defer\n           (defer! t1 1.5)\n           (Thread/sleep 1000)\n           (is (= 2 @x1))\n           (is (= 2 @x2))\n\n           (Thread/sleep 1000)\n           (is (= 3 @x1))\n           (is (= 3 @x2))\n\n           ; Cancel\n           (cancel! t2)\n           (Thread/sleep 1000)\n           (is (= 4 @x1))\n           (is (= 3 @x2))))\n\n(deftest ^:time exception-recovery-test\n         (let [x (atom 0)]\n           (every! 0.1 (fn []\n                         (swap! x inc)\n                         (throw (IllegalStateException. \"Test Exception\"))))\n           (Thread/sleep 150)\n           (is (= 2 @x))))\n\n(defn mapvals\n  [f kv]\n  (into {} (map (fn [[k v]] [k (f v)]) kv)))\n\n(defn pairs\n  [coll]\n  (partition 2 1 coll))\n\n(defn differences\n  [coll]\n  (map (fn [[x y]] (- y x)) (pairs coll)))\n\n(deftest ^:time periodic-test\n         \"Run one function periodically.\"\n         (let [results (atom [])]\n           ; For a wide variety of intervals, start periodic jobs to record\n           ; the time.\n           (doseq [interval (range 1/10 5 1/10)]\n             (every! interval #(swap! results conj [interval (unix-time)])))\n\n           (Thread/sleep 20000)\n           (stop!)\n\n           (let [groups (mapvals (fn [vs] (map second vs))\n                                 (group-by first @results))\n                 differences (mapvals differences groups)]\n             (doseq [[interval deltas] differences]\n               ; First delta will be slightly smaller because the scheduler\n               ; computed an absolute time in the *past*\n               ; (is (<= -0.025 (- (first deltas) interval) 0))\n\n               (let [deltas (drop 0 deltas)]\n                 ; Remaining deltas should be accurate to within 5ms.\n                 (is (every? (fn [delta]\n                               (< -0.01 (- delta interval) 0.01)) deltas))\n                 ; and moreover, there should be no cumulative drift.\n                 (is (< -0.005\n                        (- (/ (reduce + deltas) (count deltas)) interval)\n                        0.005)))))))\n"
  },
  {
    "path": "test/tea_time/virtual_test.clj",
    "content": "(ns tea-time.virtual-test\n  (:require [tea-time.core :refer :all]\n            [tea-time.virtual :refer :all]\n            [clojure.test :refer :all]))\n\n(use-fixtures :once call-with-virtual-time!)\n(use-fixtures :each reset-time!)\n\n(deftest clock-test\n         (is (= (virtual-unix-time) 0.0))\n         (advance! -1)\n         (is (= (virtual-unix-time) 0.0))\n         (advance! 4.5)\n         (is (= (virtual-unix-time) 4.5))\n         (reset-time!)\n         (is (= (virtual-unix-time) 0.0)))\n\n(deftest at-test\n         (let [x (atom 0)\n               once1 (at-unix! 1 #(swap! x inc))\n               once2 (at-unix! 2 #(swap! x inc))\n               once3 (at-unix! 3 #(swap! x inc))]\n\n           (advance! 0.5)\n           (is (= @x 0))\n\n           (advance! 2)\n           (is (= @x 2))\n\n           (cancel! once3)\n           (advance! 3)\n           (is (= @x 2))))\n\n(deftest every-test\n         (let [x (atom 0)\n               bump #(swap! x inc)\n               task (every! 1 2 bump)]\n\n           (is (= @x 0))\n\n           (advance! 1)\n           (is (= @x 0))\n\n           (advance! 2)\n           (is (= @x 1))\n\n           (advance! 3)\n           (is (= @x 2))\n\n           (advance! 4)\n           (is (= @x 3))\n\n           ; Double-down\n           (defer! task -3)\n           (is (= @x 3))\n           (advance! 5)\n           (is (= @x 8))\n\n           ; Into the future!\n           (defer! task 4)\n           (advance! 8)\n           (is (= @x 8))\n           (advance! 9)\n           (is (= @x 9))\n           (advance! 10)\n           (is (= @x 10))))\n"
  }
]