Giter VIP home page Giter VIP logo

chime's Introduction

Chime

Chime is a really lightweight Clojure scheduler.

Dependency

Add the following to your project.clj/deps.edn file:

[jarohen/chime "0.3.3"]
{jarohen/chime {:mvn/version "0.3.3"}}

The ‘Big Idea’™ behind Chime

The main goal of Chime was to create the simplest possible scheduler. Many scheduling libraries have gone before, most attempting to either mimic cron-style syntax, or creating whole DSLs of their own. This is all well and good, until your scheduling needs cannot be (easily) expressed using these syntaxes.

When returning to the grass roots of a what a scheduler actually is, we realised that a scheduler is really just a promise to execute a function at a (possibly infinite) sequence of times. So, that is exactly what Chime is - and no more!

Chime doesn’t really mind how you generate this sequence of times - in the spirit of composability you are free to choose whatever method you like! (yes, even including other cron-style/scheduling DSLs!)

When using Chime in other projects, I have settled on a couple of patterns (mainly involving the rather excellent time functions provided by java.time - more on this below.)

Usage

Chime consists of one main function, chime.core/chime-at.

chime-at is called with the sequence of times, and a callback function:

(require '[chime.core :as chime])
(import '[java.time Instant])

(let [now (Instant/now)]
  (chime/chime-at [(.plusSeconds now 2)
                   (.plusSeconds now 4)]
                  (fn [time]
                    (println "Chiming at" time))))

With chime-at, it is the caller’s responsibility to handle over-running jobs. chime-at will never execute jobs of the same scheduler in parallel or drop jobs. If a schedule is cancelled before a job is started, the job will not run.

chime-at returns an AutoCloseable that can be closed to cancel the schedule.

You can also pass an on-finished parameter to chime-at to run a callback when the schedule has finished (if it’s a finite schedule, of course!):

(let [now (Instant/now)]
  (chime/chime-at [(.plusSeconds now 2)
                   (.plusSeconds now 4)]

                  (fn [time]
                    (println "Chiming at" time))

                  {:on-finished (fn []
                                  (println "Schedule finished."))}))

Recurring schedules

To achieve recurring schedules, we can lazily generate an infinite sequence of times. This example runs every 5 minutes from now:

(import '[java.time Instant Duration])

(-> (chime/periodic-seq (Instant/now) (Duration/ofMinutes 5))
    rest) ; excludes *right now*

To start a recurring schedule at a particular time, you can combine this example with some standard Clojure functions. Let’s say you want to run a function at 8pm New York time every day. To generate the sequence of times, you’ll need to seed the call to periodic-seq with the next time you want the function to run:

(import '[java.time LocalTime ZonedDateTime ZoneId Period])

(chime/periodic-seq (-> (LocalTime/of 20 0 0)
                        (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                        .toInstant)
                    (Period/ofDays 1))

For example, to say hello once per second:

(chime/chime-at (chime/periodic-seq (Instant/now) (Duration/ofSeconds 1))
  ;; note that the function needs to take an argument.
  (fn [time]
    (println "hello")))

Complex schedules

Because there is no scheduling DSL included with Chime, the sorts of schedules that you can achieve are not limited to the scope of the DSL.

Instead, complex schedules can be expressed with liberal use of standard Clojure sequence-manipulation functions:

(import '[java.time ZonedDateTime ZoneId Period LocalTime DayOfWeek])

;; Every Tuesday and Friday:
(->> (chime/periodic-seq (-> (LocalTime/of 0 0)
                             (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                             .toInstant)
                         (Period/ofDays 1))

     (map #(.atZone % (ZoneId/of "America/New_York")))

     (filter (comp #{DayOfWeek/TUESDAY DayOfWeek/FRIDAY}
                   #(.getDayOfWeek %))))

;; Week-days
(->> (chime/periodic-seq ...)
     (map #(.atZone % (ZoneId/of "America/New_York")))
     (remove (comp #{DayOfWeek/SATURDAY DayOfWeek/SUNDAY}
                   #(.getDayOfWeek %))))

;; Last Monday of the month:
(->> (chime/periodic-seq ...)

     (map #(.atZone % (ZoneId/of "America/New_York")))

     ;; Get all the Mondays
     (filter (comp #{DayOfWeek/MONDAY}
                   #(.getDayOfWeek %)))

     ;; Split into months
     ;; (Make sure you use partition-by, not group-by -
     ;;  it's an infinite series!)
     (partition-by #(.getMonth %))

     ;; Only keep the last one in each month
     (map last))

;; 'Triple witching days':
;; (The third Fridays in March, June, September and December)
;; (see http://en.wikipedia.org/wiki/Triple_witching_day)

;; Here we have to revert the start day to the first day of the month
;; so that when we split by month, we know which Friday is the third
;; Friday.

(->> (chime/periodic-seq (-> (LocalTime/of 0 0)
                             (.adjustInto (-> (ZonedDateTime/now (ZoneId/of "America/New_York"))
                                              (.withDayOfMonth 1)))
                             .toInstant)
                         (Period/ofDays 1))

     (map #(.atZone % (ZoneId/of "America/New_York")))

     (filter (comp #{DayOfWeek/FRIDAY}
                   #(.getDayOfWeek %)))

     (filter (comp #{3 6 9 12}
                   #(.getMonthValue %)))

     ;; Split into months
     (partition-by #(.getMonthValue %))

     ;; Only keep the third one in each month
     (map #(nth % 2))

     (chime/without-past-times)))

Error handling

You can pass an error-handler to chime-at - a function that takes the exception as an argument. Return truthy from this function to continue the schedule, falsy to cancel it. By default, Chime will log the error and continue the schedule.

(chime-at [times...]
          do-task-fn
          {:error-handler (fn [e]
                            ;; log, alert, notify etc?
                            )})

core.async

If you already have Clojure’s core.async in your project, you may prefer chime.core-async/chime-ch

chime-ch is called with an ordered sequence of instants, and returns a channel that sends an event at each time in the sequence.

(require '[chime.core-async :refer [chime-ch]]
         '[clojure.core.async :as a :refer [<! go-loop]])

(let [now (Instant/now)
      chimes (chime-ch [(.plusSeconds now 2)
                        (.plusSeconds now 3)])]
  (a/<!! (go-loop []
           (when-let [msg (<! chimes)]
             (prn "Chiming at:" msg)
             (recur)))))

chime-ch uses an unbuffered channel, so cancelling a schedule is achieved simply by not reading from the channel.

You can also pass chime-ch a buffered channel as an optional argument. This is particularly useful if you need to specify the behaviour of the scheduler if one job overruns.

core.async has three main types of buffers: sliding, dropping and fixed. In these examples, imagining an hourly schedule, let’s say the 3pm run finishes at 5:10pm.

  • With a sliding-buffer (example below), the 4pm job would be cancelled, and the 5pm job started at 5:10.

  • With a dropping-buffer, the 4pm job would start at 5:10, but the 5pm job would be cancelled.

  • In the unbuffered example, above, the 4pm job would have been started at 5:10pm, and the 5pm job starting whenever that finished.

(require '[chime.core-async :refer [chime-ch]]
         '[clojure.core.async :as a :refer [<! go-loop]])

(let [chimes (chime-ch times {:ch (a/chan (a/sliding-buffer 1))})]
  (go-loop []
    (when-let [time (<! chimes)]
      ;; ...
      (recur))))

You can close! the channel returned by chime-ch to cancel the schedule.

Testing your integration with Chime

Testing time-dependent applications is always more challenging than other non-time-dependent systems. Chime makes this easier by allowing you to test the sequence of times independently from the execution of the scheduled job.

(Although, don’t forget to wrap your infinite sequences with (take x …​) when debugging!)

Bugs/thoughts/ideas/suggestions/patches etc

Please feel free to submit these through Github in the usual way!

Thanks!

Contributors

A big thanks to all of Chime’s contributors, a full list of whom are detailed in the Changelog.

License

Copyright © 2013+ James Henderson

Distributed under the Eclipse Public License, the same as Clojure.

Big thanks to Malcolm Sparks for providing the initial idea, as well as his other contributions and discussions.

chime's People

Contributors

bartadv avatar cassiel avatar cloojure avatar dexterminator avatar holyjak avatar jarohen avatar jimpil avatar krastins avatar rockolo avatar samhedin avatar vemv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

chime's Issues

Violation of pre-condition not caught by error handler

Hi!

I am using chime 0.3.2 with Clojure 1.10.1 and have found that the :error-handler function currently does not catch violations of pre-conditions.

From looking at the code, chime seems to only catch java.lang.Exception:

(chime/chime-at
    (take 3 (rest (chime/periodic-seq (java.time.Instant/now)
                                      (java.time.Duration/ofSeconds 1))))
    (fn [timestamp] (throw (Exception. (str timestamp))))
    {:error-handler (fn [e] (println (str e)))})

but not java.lang.Error:

(chime/chime-at
    (take 3 (rest (chime/periodic-seq (java.time.Instant/now)
                                      (java.time.Duration/ofSeconds 1))))
    (fn [timestamp] (throw (AssertionError. (str timestamp))))
    {:error-handler (fn [e] (println (str e)))})

Could you please change the code and catch java.lang.Throwable, the common ancestor of errors and exceptions?

Thanks a lot! 😄

Martin

periodic-seq example and DST changes

Thank you for this nice little library.

The README has an example of using periodic-seq for local times:

(chime/periodic-seq (-> (LocalTime/of 20 0 0)
                        (.adjustInto (ZonedDateTime/now (ZoneId/of "America/New_York")))
                        .toInstant)
                    (Period/ofDays 1))

I don't get this to work properly across DST changes. It seems to me that this example converts to instant before adding the period, which means that the period is always from UTC and not local time.

This shows the problem:

(binding [chime.core/*clock* (java.time.Clock/fixed (.toInstant #inst "2022-10-28T12:12:12Z") (java.time.ZoneId/of "UTC"))]
  (take 2
        (->> (chime.core/periodic-seq
              (-> (java.time.LocalTime/of 7 30)
                  (.adjustInto (-> (java.time.ZonedDateTime/now chime.core/*clock*)
                                   (.withZoneSameLocal (java.time.ZoneId/of "Europe/Oslo"))))
                  .toInstant)
              (java.time.Period/ofDays 1))
             (chime.core/without-past-times))))

Here we see that the result is the same UTC hour before before and after the recent DST change, which results in an incorrect local time for the second instant:

(#object[java.time.Instant 0x300c7735 "2022-10-29T05:30:00Z"]
 #object[java.time.Instant 0x54e5895a "2022-10-30T05:30:00Z"])

The problem seems to be that periodic-seq works with instants, but that the conversion to instant should instead be the final step. Not yet sure how to solve this in a nice way. A separate function for periodic-local-date-time-seq, and let the caller map to instants?

Or perhaps add an arity to periodic-seq for [LocalDateTime ZoneId duration-or-period] for calculating using LocalDateTimes, while still returning instants? (If so, maybe also include another arity for a final xform for doing complex schedules on the local date times, before returning the instants?)

In the mean time, here's a possible not-so-nice solution:

(binding [chime.core/*clock* (java.time.Clock/fixed (.toInstant #inst "2022-10-28T12:12:12Z") (java.time.ZoneId/of "UTC"))]
  (take 2
        (->> (map #(.toInstant (.atZone % (java.time.ZoneId/of "Europe/Oslo")))
                  (iterate
                   #(.plusDays % 1)
                   (-> (java.time.LocalTime/of 7 30)
                       (.adjustInto (java.time.LocalDateTime/now chime.core/*clock*)))))
             (chime.core/without-past-times))))

Here we see that the instants change as expected after DST change:

(#object[java.time.Instant 0x221b9091 "2022-10-29T05:30:00Z"]
 #object[java.time.Instant 0x49e3eb99 "2022-10-30T06:30:00Z"])

Examples assume Dates instead of Instants?

I'm trying to use a filter on the result of periodic-seq like in this example in the README:

(->> (chime/periodic-seq (-> (.adjustInto (LocalTime/of 0 0)
                                          (ZonedDateTime/now (ZoneId/of "America/New_York")))
                             .toInstant)
                         (Period/ofDays 1))

     (filter (comp #{DayOfWeek/TUESDAY DayOfWeek/FRIDAY}
                   #(.getDayOfWeek %))))

However when I try try example, I get this error:

No matching field found: getDayOfWeek for class java.time.Instant

The function seems to return Instants while the example assumes Dates. Am I doing something wrong or is there a problem with the example? Thanks!

How to schedule multiple recurring jobs, called with a specific context?

Coming from Quartzite (Quartz), how should I schedule multiple recurring "triggers", each with a particular context? Need to be able to modify or cancel these periodic schedules.

It's not clear from the documentation whether I can call chime-at with an infinite sequence of times. Perhaps you could add some example usages?

Specifically, I need to trigger multiple recurring jobs at regular time on certain days of the week, e.g. 9am every Tuesday, and/or 11am on Tuesdays and Wednesdays.

chime-at API changes

Currently the function being called by chime-at just takes the time as argument, and you can just pass one function.

It might be nice if chime-at would allow you to pass the time intervals and either:

  • allow you to pass the function and a list with all the arguments to use at each call
  • allow you to pass just a list of functions, which would solve the above as well I guess if you just do (map #(partial f %) arguments)

This would allow to generate the input for chime-at more functionally.

(def args [["first"] ["second"]])
(defn f [args] ...) 
(chime-at intervals f args)

or just

(def args [["first"] ["second"]])
(defn f [args] ...) 
(def fns (map #(partial f %) args))
(chime-at intervals fns)

Breaks on newer core.async version

When my project depends on the latest version of core.async (currently 0.2.385) calling chime-at will result in the following exception:

Exception in thread "async-dispatch-4" java.lang.IllegalArgumentException: No implementation of method: :take! of protocol: #'clojure.core.async.impl.protocols/ReadPort found for class: chime$chime_ch$reify__19318
    at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:568)
    at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:560)
    at clojure.core.async.impl.protocols$eval59$fn__60$G__50__67.invoke(protocols.clj:15)
    at clojure.core.async.impl.ioc_macros$take_BANG_.invokeStatic(ioc_macros.clj:1022)
    at clojure.core.async.impl.ioc_macros$take_BANG_.invoke(ioc_macros.clj:1021)
    at chime$chime_at$fn__19347$state_machine__5082__auto____19348$fn__19350.invoke(chime.clj:66)
    at chime$chime_at$fn__19347$state_machine__5082__auto____19348.invoke(chime.clj:65)
    at clojure.core.async.impl.ioc_macros$run_state_machine.invokeStatic(ioc_macros.clj:1012)
    at clojure.core.async.impl.ioc_macros$run_state_machine.invoke(ioc_macros.clj:1011)
    at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invokeStatic(ioc_macros.clj:1016)
    at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invoke(ioc_macros.clj:1014)
    at chime$chime_at$fn__19347.invoke(chime.clj:65)
    at clojure.lang.AFn.run(AFn.java:22)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

Quick workaround: depend on 0.2.374

Expose :thread-factory option

Basically, along with :error-handler and :on-finished, one could/should(?) be able to provide an (optional) :thread-factory. We're talking for a few characters change here... Any thoughts?

On-finished weird behaviours

I can reproduce a behaviour that I think is weird with something like

(chime/chime-at
  [10 20 30]
  (fn [time] (println time))
  {:on-finished (fn [] (println "one") (Thread/sleep 100) (println "two"))})

And what happens is that the one gets printed but two never does.

Error running scheduled fn java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null

3월 07, 2021 2:34:40 오전 chime.core invoke
경고: Error running scheduled fn
java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at java.base/java.io.StringReader.(StringReader.java:50)
at clojure.data.json$read_str.invokeStatic(json.clj:282)
at clojure.data.json$read_str.doInvoke(json.clj:278)
at clojure.lang.RestFn.invoke(RestFn.java:439)
at tray.pnix.route$jspSQ_loop.invokeStatic(route.clj:151)
at tray.pnix.route$jspSQ_loop.invoke(route.clj:150)
at tray.pnix.tray$START$fn__32985.invoke(tray.clj:52)
at chime.core$chime_at$schedule_loop__27086$task__27090$fn__27091.invoke(core.clj:91)
at chime.core$chime_at$schedule_loop__27086$task__27090.invoke(core.clj:90)
at clojure.lang.AFn.run(AFn.java:22)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at java.base/java.lang.Thread.run(Thread.java:832)
}

New feature: request upcoming chimes

It would be useful to be able to ask chime for upcoming scheduled jobs. While my code could cache this information itself, I figure chime is already storing this information somewhere, so it would be better to just ask the source, rather than duplicating it in my own code.

Support for times already past in `chime-at`

#7 doesn't seem to be resolved.

Sometimes the following code produces an error.

(let [start-time (t/now)]
  (chime/chime-at [(-> 1 t/millis t/from-now)] println {:start-time start-time}))
=>
#object[chime$chime_at$cancel_BANG___15477
        0x40b6d48d
        "chime$chime_at$cancel_BANG___15477@40b6d48d"]
Exception in thread "async-dispatch-2" java.lang.IllegalArgumentException: No implementation of method: :before? of protocol: #'clj-time.core/DateTimeProtocol found for class: nil
	at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:568)
	at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:560)
	at clj_time.core$eval6044$fn__6089$G__6021__6096.invoke(core.clj:102)
	at chime$ms_between.invokeStatic(chime.clj:8)
	at chime$ms_between.invoke(chime.clj:7)
	at chime$chime_ch$fn__15317$state_machine__12479__auto____15332$fn__15334.invoke(chime.clj:38)
	at chime$chime_ch$fn__15317$state_machine__12479__auto____15332.invoke(chime.clj:38)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invokeStatic(ioc_macros.clj:973)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invoke(ioc_macros.clj:972)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invokeStatic(ioc_macros.clj:977)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invoke(ioc_macros.clj:975)
	at chime$chime_ch$fn__15317.invoke(chime.clj:38)
	at clojure.lang.AFn.run(AFn.java:22)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

Don't understand how to run chime each day at a specific hour.

Hi,
I swear I had this working at some point but it no longer does. I have this code that I use to schedule functions to run once a day at some given hour.

(defn schedule [function hour]
  (info (format "setting schedule for %s at %s"  (second (clojure.string/split (str function) #"\$|@")) hour))
  (chime/chime-at
   (chime/periodic-seq
    (-> (LocalTime/of hour 0 0)
        (.adjustInto (ZonedDateTime/now (ZoneId/of "Europe/Stockholm")))
        .toInstant)
    (Period/ofDays 1))
   function))
;; example
(schedule #(println "hello") 10)

But it doesn't run the functions.

I tried to schedule something each second, as a test. But that returned :not-delivered

 (chime/chime-at (-> (chime/periodic-seq (Instant/now) (Duration/ofSeconds 1))
    rest) #(println "hello"))
;; => #<core$chime_at$reify__16349@f0b1e5a: :not-delivered>

What am I misunderstanding? 😅

Exception thrown in README examples (when using java-time)

Hi all,

When I run the README examples of chime-at with java (8) time in the repl

(require '[chime :refer [chime-at]])  ;; [jarohen/chime "0.2.2"]
(import '[java.time Instant])

(chime-at [(.plusSeconds (Instant/now) 2)
           (.plusSeconds (Instant/now) 4)]
  (fn [time] (println "Chiming at" time)))

I get the following exception:

{:what :uncaught-exception, :exception #error {
 :cause No implementation of method: :to-date-time of protocol: #'clj-time.coerce/ICoerce found for class: java.time.Instant
 :via
 [{:type java.lang.IllegalArgumentException
   :message No implementation of method: :to-date-time of protocol: #'clj-time.coerce/ICoerce found for class: java.time.Instant
   :at [clojure.core$_cache_protocol_fn invokeStatic core_deftype.clj 583]}]
 :trace
 [[clojure.core$_cache_protocol_fn invokeStatic core_deftype.clj 583]
  [clojure.core$_cache_protocol_fn invoke core_deftype.clj 575]
  [clj_time.coerce$eval30076$fn__30077$G__30067__30082 invoke coerce.clj 18]
  [clojure.core$map$fn__5866 invoke core.clj 2753]
  [clojure.lang.LazySeq sval LazySeq.java 42]
  [clojure.lang.LazySeq seq LazySeq.java 51]
  [clojure.lang.RT seq RT.java 535]
  [clojure.core$seq__5402 invokeStatic core.clj 137]
  [clojure.core$drop_while$step__5940 invoke core.clj 2972]
  [clojure.core$drop_while$fn__5943 invoke core.clj 2977]
  [clojure.lang.LazySeq sval LazySeq.java 42]
  [clojure.lang.LazySeq seq LazySeq.java 51]
  [clojure.lang.RT seq RT.java 535]
  [clojure.core$seq__5402 invokeStatic core.clj 137]
  [clojure.core$seq__5402 invoke core.clj 137]
  [chime$chime_ch$fn__30271$state_machine__25597__auto____30286$fn__30288 invoke chime.clj 38]
  [chime$chime_ch$fn__30271$state_machine__25597__auto____30286 invoke chime.clj 38]
  [clojure.core.async.impl.ioc_macros$run_state_machine invokeStatic ioc_macros.clj 973]
  [clojure.core.async.impl.ioc_macros$run_state_machine invoke ioc_macros.clj 972]
  [clojure.core.async.impl.ioc_macros$run_state_machine_wrapped invokeStatic ioc_macros.clj 977]
  [clojure.core.async.impl.ioc_macros$run_state_machine_wrapped invoke ioc_macros.clj 975]
  [chime$chime_ch$fn__30271 invoke chime.clj 38]
  [clojure.lang.AFn run AFn.java 22]
  [java.util.concurrent.ThreadPoolExecutor runWorker ThreadPoolExecutor.java 1149]
  [java.util.concurrent.ThreadPoolExecutor$Worker run ThreadPoolExecutor.java 624]
  [java.lang.Thread run Thread.java 748]]}, :where Uncaught exception onasync-dispatch-4} 

Should I be using clj-time instead? Do the README examples need to be updated?

Thanks

:error-handler does not seem to be called

Hello,

Given this very simple test case:

(defn test-exception-with-handler []
  (chime-at (rest
    (periodic-seq (t/now)
                  (-> 5 t/seconds))
            (fn [time]
              (throw (Exception. "uh oh")))
            {:error-hander (fn [e]
                             (log/error e "something happened"))}))

It appears that the error handler is not called. Here's the printed output:

Exception in thread "async-thread-macro-11" java.lang.Error: java.lang.Exception: uh oh
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1148)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.Exception: uh oh

Am I missing something?
Thanks in advance!

Long running tasks

Suppose we have such code:

(chime/chime-at (chime/periodic-seq (Instant/now) (Duration/ofSeconds 20))
                            (fn [_]
                              (when (has-new-task)
                                 (long-running-task))))

We need to check if there is a new task every 20 secs, and if so, we need run long-time calculation (which can last for an hour). Then after long running task is completed, there will be a lot of (has-new-task) calls, which would be better to avoid.

Is it possible to add a parameter to avoid such behaviour and instead run only 1 function call after long-running-task is completed? Or is it better to use another mechanism for such situation?

Feature request: Allow adding new times to a chimes-ch channel

I'd like to create a channel then put! new times into it, like so:

(def chimes (chimes-ch []))

(a/<!! (go-loop []
           (when-let [msg (<! chimes)]
             (prn "Chiming at:" msg)
             (recur))))

(put! [(.plusSeconds (Instant/now) 3) (.plusSeconds (Instant/now) 5)])
;; currently, I get:
;; Execution error (IllegalArgumentException) at clojure.core.async.impl.protocols/eval2088$fn$G (protocols.clj:18).
;; No implementation of method: :put! of protocol: #'clojure.core.async.impl.protocols/WritePort found for class: chime.core_async$chime_ch$reify__37328

But perhaps there is a way to do this already? (I tried passing a channel to the ch param of chimes-ch and putting times into it, but that didn't work either).

Multiple invocations of `on-finished`

I'm not sure what the correct behaviour should be here, but might be worth thinking about a little.

Given the following code:

(let [now (Instant/now)
        chiming (chime/chime-at [(.plusSeconds now 1)
                                 (.plusSeconds now 3)]
                                (fn [time]
                                  (prn :chime (str time)))
                                {:on-finished #(prn :done)})]
    (Thread/sleep 4000)
    (.close chiming))

... we can see that on-finished is called twice:

:chime "2020-09-07T13:52:33.193745Z"
:chime "2020-09-07T13:52:35.193745Z"
:done
:done

This was surprising to me - perhaps on-finished should have an invariant that it is only ever called once?

Another option is that perhaps the value which on-finished evaluates to should be delivered to the invocation of close, meaning the fn can be evaluated once, but value delivered to multiple places...? Seems like this could be useful.

Drop times that have already passed

In one of the examples in the README, it says: "Any times that have already passed will be dropped, as before" and this is the behavior that I would like in my application. However it seems that this is not the case: if I pass a time in the past to chime-at, the function is immediately executed with that time. For example:

(chime/chime-at [(.minus (Instant/now) 1 (ChronoUnit/HOURS))] println)

This prints immediately with the time an hour in the past.

My solution for now is to manually drop input times in the past, but I'm interested to hear whether I'm misunderstanding the example text, or something else is going on.

Use of deprecated clj-time function

I'm testing the basic periodic scheduler, and getting the following printed to screen:

Chiming at #<DateTime 2013-12-06T16:15:03.302Z>
DEPRECATION WARNING: DEPRECATED: use in-millis

It appears to relate to https://github.com/clj-time/clj-time#bugs-and-enhancements

Note: version 0.6.0 introduces a number of API changes to improve consistency. The API now uses second, seconds and millis where it previously had sec, secs and msecs.

multiple chime-at calls

Is it safe to call chime-at multiple times with different schedules?
Will this run in different thread pools?

java.lang.InterruptedException race in chiming functions.

There seems to be a race between closing a chime and the chiming function invocation that can lead to a java.lang.InterruptedException being thrown when functions are awaiting something coming from, for example, a future, or have consumed a function that does this somewhere along the call stack.

It'd be nice if either the chiming function was never called, or any inflight functions were allowed to complete before closing.

Example:

(let [now (Instant/now)
      chiming (chime/chime-at (chime/periodic-seq now (Duration/ofSeconds 1))
                              (fn [time]
                                (log/info :future-task @(future (count {})))
                                (log/info :chime (str time)))
                              {:error-handler (fn [e]
                                                (log/error :chime-failed e)
                                                true)
                               :on-finished #(log/info :done)})]
  (Thread/sleep 1000)
  (.close chiming))

outputs:

2020-09-07 15:51:25.548 INFO  :future-task 0
2020-09-07 15:51:25.549 INFO  :chime 2020-09-07T14:51:25.547924Z
2020-09-07 15:51:26.550 INFO  :done
2020-09-07 15:51:26.550 ERROR :chime-failed #error {
 :cause nil
 :via
 [{:type java.lang.InterruptedException
   :message nil
   :at [java.util.concurrent.FutureTask awaitDone FutureTask.java 418]}]
 :trace
 [[java.util.concurrent.FutureTask awaitDone FutureTask.java 418]
  [java.util.concurrent.FutureTask get FutureTask.java 190]
  [clojure.core$deref_future invokeStatic core.clj 2300]
  [clojure.core$future_call$reify__8454 deref core.clj 6974]
  [clojure.core$deref invokeStatic core.clj 2320]
  [clojure.core$deref invoke core.clj 2306]
  ...
  [chime.core$chime_at$schedule_loop__13568$task__13572$fn__13573 invoke core.clj 91]
  [chime.core$chime_at$schedule_loop__13568$task__13572 invoke core.clj 90]
  [clojure.lang.AFn run AFn.java 22]
  [java.util.concurrent.Executors$RunnableAdapter call Executors.java 515]
  [java.util.concurrent.FutureTask run FutureTask.java 264]
  [java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask run ScheduledThreadPoolExecutor.java 304]
  [java.util.concurrent.ThreadPoolExecutor runWorker ThreadPoolExecutor.java 1128]
  [java.util.concurrent.ThreadPoolExecutor$Worker run ThreadPoolExecutor.java 628]
  [java.lang.Thread run Thread.java 834]]}

Note, might have to run this a couple of times to see the exception, as it doesn't always happen.

Core.async integration doc-string

Originally posted by @jimpil in #37 (comment)

Re: the core-async integration:

At first glance, I don't see how the stuff we're discussing breaks chime-ch. However, I did notice something else. The doc-string states:

ch - (optional) Channel to chime on - defaults to a new unbuffered channel
Closing this channel stops the schedule.

I see no evidence of this statement. I suspect you meant to say closing the returned channel stops the schedule, but then that should be a few lines up. If the intention of the doc-string is correct, then I think the chiming-fn should be something like the following:

(fn [time]
  (when-not (a/>!! ch time) 
    (a/close ch)))

With the risk of rushing to conclusions, i will say that this looks like another remnant of a time when chime would actually consider the return of the chime-fn. However, at the moment it is not (true is returned after it). I apologise in advance if that's far-reaching...

How to evaluate a function correctly every minute using chime-at?

Here are my tests:

(require '[overtone.at-at :refer :all]
         '[chime :refer [chime-at]]
         '[clj-time.periodic :refer [periodic-seq]]
         '[clj-time.core :as t])

;; 1. Use of future

(defonce data1 (atom {:num 1}))

(defonce updater
  (future
    (while true
      (swap! data1 update-in [:num] inc)
      (Thread/sleep 60000))))


;; 2. Using at-at

(defonce data2 (atom {:num 1}))

(def my-pool (mk-pool))

(every 60000 #(swap! data2 update-in [:num] inc) my-pool)


;; 3. Using chime

(defonce data3 (atom {:num 1}))

(chime-at (periodic-seq (t/now) (-> 60 t/seconds))
          (fn [] (swap! data3 update-in [:num] inc))
          {:error-handler (fn [e] (str e))})

After 5 minutes:

@data1
;;=> {:num 5}
@data2
;;=> {:num 8}
@data3
;;=> {:num 1}

Why is chime not counting at all?

Thank you!

Is chime-at supposed to be blocking or asynchronous?

In the README for 0.3.2, it says:

chime-at returns an AutoCloseable that can be closed to cancel the schedule.

To me, this implies that chime-at runs asynchronously, and that we can cancel its execution using the returned AutoCloseable. But when I run the example in the REPL, it blocks until it runs all the times in the sequence, then it returns the object (or never if it's infinite).

user=> (let [now (Instant/now)]
  #_=>   (chime/chime-at [(.plusSeconds now 2)
  #_=>                    (.plusSeconds now 4)]
  #_=>                   (fn [time]
  #_=>                     (println "Chiming at" time))))
Chiming at #object[java.time.Instant 0x1f5ffdd2 2020-08-25T00:14:32.904Z]
Chiming at #object[java.time.Instant 0x11981049 2020-08-25T00:14:34.904Z]
#object[chime.core$chime_at$reify__4951 0x77b586bf {:status :ready, :val nil}]
user=> 

Or perhaps I'm misunderstanding the purpose of the AutoCloseable. Does it serve a purpose after all the times have elapsed?

periodic-seq NullPointerException

Thanks for creating chime! It's been incredibly helpful. I ran into an error running "0.3.2" and "0.3.3-SNAPSHOT". Here is the calling code:

(defn -main
  [& args]
  (send "testing db...") ;; send text
  (chime/chime-at (chime/periodic-seq 
                   (-> (LocalTime/of 8 0 0)
                       (.adjustInto (ZonedDateTime/now (ZoneId/of "America/Chicago")))
                       .toInstant)
                   (Period/ofDays 1))
                  (send-rand) ;; send random text
                  {:on-finished (send "done chiming.")}))

Two things are interesting, I get the error below, but also, before this exception is thrown, it calls my chime fn (send-rand) and then calls :on finished (send "done chiming"). It should definitely not call (send-rand) as the periodic-seqis for another time.

:not-delivered>Jan 02, 2021 8:46:29 AM chime.core invoke WARNING: Error running scheduled fn java.lang.NullPointerException at chime.core$chime_at$schedule_loop__1079$task__1083$fn__1084.invoke(core.clj:91) at chime.core$chime_at$schedule_loop__1079$task__1083.invoke(core.clj:90) at clojure.lang.AFn.run(AFn.java:22) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

Again, thanks for making this! I know it can be frustrating to just have people paste in stack traces and want help. I am hoping other people have, or will, see the same issue and this will help them.

IllegalArgumentException when supplying an empty list for a schedule

Sometimes, I would like to specify an empty schedule, but when I supply [] or nil as the schedule arguments to chime-ch or chime-at, both will attempt to call clj-time.core/before? on nil:

user> (chime/chime-ch nil)
Exception in thread "async-dispatch-1" 
java.lang.IllegalArgumentException: No implementation of method: :before? of protocol: #'clj-time.core/DateTimeProtocol found for class: nil
	at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:568)
	at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:560)
	at clj_time.core$eval11045$fn__11090$G__11022__11097.invoke(core.clj:102)
	at chime$ms_between.invokeStatic(chime.clj:8)
	at chime$ms_between.invoke(chime.clj:7)
	at chime$chime_ch$fn__19258$state_machine__17295__auto____19259$fn__19261.invoke(chime.clj:38)
	at chime$chime_ch$fn__19258$state_machine__17295__auto____19259.invoke(chime.clj:38)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invokeStatic(ioc_macros.clj:1011)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invoke(ioc_macros.clj:1010)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invokeStatic(ioc_macros.clj:1015)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invoke(ioc_macros.clj:1013)
	at chime$chime_ch$fn__19258.invoke(chime.clj:38)
	at clojure.lang.AFn.run(AFn.java:22)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
#object[chime$chime_ch$reify__19297 0x6f223434 "chime$chime_ch$reify__19297@6f223434"]
user> (chime/chime-at nil (fn [_] (println "called")))
Exception in thread "async-dispatch-3" 
java.lang.IllegalArgumentException: No implementation of method: :before? of protocol: #'clj-time.core/DateTimeProtocol found for class: nil
	at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:568)
	at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:560)
	at clj_time.core$eval11045$fn__11090$G__11022__11097.invoke(core.clj:102)
	at chime$ms_between.invokeStatic(chime.clj:8)
	at chime$ms_between.invoke(chime.clj:7)
	at chime$chime_ch$fn__19258$state_machine__17295__auto____19259$fn__19261.invoke(chime.clj:38)
	at chime$chime_ch$fn__19258$state_machine__17295__auto____19259.invoke(chime.clj:38)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invokeStatic(ioc_macros.clj:1011)
	at clojure.core.async.impl.ioc_macros$run_state_machine.invoke(ioc_macros.clj:1010)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invokeStatic(ioc_macros.clj:1015)
	at clojure.core.async.impl.ioc_macros$run_state_machine_wrapped.invoke(ioc_macros.clj:1013)
	at chime$chime_ch$fn__19258.invoke(chime.clj:38)
	at clojure.lang.AFn.run(AFn.java:22)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
#function[chime/chime-at/cancel!--19353]

Both calls will return the expected channel/cancel function, but the exception is still worrisome and is causing my tests to fail.

I tried running the likely culprit:

    (go-loop [now (t/now)
              [next-time & more-times] (->> (times-fn)
                                            (map tc/to-date-time)
                                            (drop-while #(t/before? % now)))]
      ...)

But I could not reproduce the problem in my REPL.

By the way, thanks for a great project! I really like the abstraction provided by chime-ch.

Support for times already past in `chime-at`

I'd like to expire independent items (the expiry date is determined ahead of time) from a database, on application startup I schedule all outstanding expiries. In some cases this can mean the expiration date has already passed, which causes this issue:

=> (chime-at [(-> -2 secs from-now)] (fn [t] (println "HEY")))
#<IllegalArgumentException java.lang.IllegalArgumentException: No implementation of method: :before? of protocol: #'clj-time.core/DateTimeProtocol found for class: nil>

Without having to first filter expiries that have already passed (and manually expire them) it would be convenient if passed schedules could be immediately called.

(I'm not sure if there is perhaps a better way of achieving this or if I missed something in the documentation.)

Error in the doc-string?

In the doc-string of chime-ch there's a usage example:

Usage: 

(let [chimes (chime-ch [(.plusSeconds (Instant/now) -2) ; has already passed, will be ignored.
                        (.plusSeconds (Instant/now) 2)
                        (.plusSeconds (Instant/now) 2)])]
  (a/<!! (go-loop []
           (when-let [msg (<! chimes)]
             (prn "Chiming at:" msg)
             (recur)))))

The comment states that the first time will be ignored because it has passed, but running this code will print all 3 times.

Non determenistic behavior

I have noticed that the schedular is not determenistic, for example using the code shown like so from the repl:

(def s (start 3000))
(s)

And then redo this ~10 times in the repl the following behavior will be observed:
Somethimes "Trigger 0" will be shown right away and sometimes nothing will be shown.

The expected behavior i would like on every restart is that NO value should be shown until after 3000sec after the most current restart.

(ns hello-world.c
  (:require
   [clojure.core.async :as a :refer (<! go-loop)]
   [chime :refer (chime-ch)]
   [clj-time.core :as t]
   [clj-time.periodic :refer (periodic-seq)]
   ))

(defn start
  [keep-alive-period]
  (let [timer-ch (chime-ch (periodic-seq (t/now)
                                         (-> keep-alive-period t/seconds)))]
    (go-loop [i 0]
      (when-let [trigger-time (<! timer-ch)]
        (println "Trigger: " i)
        (recur (inc i))))
    (fn stop-fn [] (a/close! timer-ch))))

Thread names don't include numbers

Despite this code, I'm not seeing chime's threads include a number in their name, making debugging slightly more difficult.

Here's a log excerpt produced by my code, showing the problematic thread names:

2020-11-13T00:00:00.001627+00:00 app[bot.1]: INFO  [chime-] futbot.core - Youtube channel -unknown (UCmzFaEBQlLmMTWS0IQ90tgA)- job started...
2020-11-13T00:00:00.001960+00:00 app[bot.1]: INFO  [chime-] futbot.core - Daily schedule job started...
2020-11-13T00:00:00.108573+00:00 app[bot.1]: ERROR [chime-] futbot.core - Unexpected exception in Youtube channel -unknown (UCmzFaEBQlLmMTWS0IQ90tgA)- job
2020-11-13T00:00:00.108583+00:00 app[bot.1]: clojure.lang.ExceptionInfo: Google API call (https://www.googleapis.com/youtube/v3/search?part=snippet&order=date&type=video&maxResults=50&channelId=UCmzFaEBQlLmMTWS0IQ90tgA&publishedAfter=2020-11-12T00:00:00.002427Z&key=REDACTED) failed
2020-11-13T00:00:00.108755+00:00 app[bot.1]: INFO  [chime-] futbot.core - Youtube channel -unknown (UCmzFaEBQlLmMTWS0IQ90tgA)- job finished
2020-11-13T00:00:00.920143+00:00 app[bot.1]: INFO  [chime-] futbot.jobs - No matches remaining today - not scheduling any reminders.
2020-11-13T00:00:00.920324+00:00 app[bot.1]: INFO  [chime-] futbot.core - Daily schedule job finished

This excerpt shows two different jobs ("Youtube channel -unknown (UCmzFaEBQlLmMTWS0IQ90tgA)- job" and "Daily schedule job") that just happen to be scheduled to run at the same time (midnight UTC), resulting in their log output being interleaved. This makes it somewhat more difficult to properly correlate individual log entries with each job.

Oh and for reference, here's the log output pattern I'm using (my app uses Logback classic) - note in particular that it does not truncate the thread name component, meaning if the thread names had included a number, it would be visible:

      <pattern>%date %-5level [%thread] %logger{30} - %msg%n</pattern>

Dependency to clj-time unnecessary

Hi there,

First of all many thanks for this great library 👍 - much appreciated! Secondly, would you consider getting rid of the clj-time dependency, or at least making it optional (using a require expression as opposed to a :require clause)? From what I can see only joda_time.clj depends on it, and frankly that protocol extension can live in the README, right?

Thanks again :)

parameterize the executor

Hey, thanks for this library, exactly what I needed,

Instead of defaulting to Executors/newSingleThreadScheduledExecutor, can we get this parameterized, e.g. as a kwargs to chime-at. The reason for this is i'm trying to write unit tests with simulated time that aren't multi-threaded and mocked out the java.util.concurrent.ScheduledExecutorService interface.

Thanks!

chime-at calls callback after schedule is cancelled if a previous callback overruns

This may be by design, but I thought it was confusing and wanted to bring it to your attention just in case.

(defn do-stuff [now]
  (println "starting" now)
  (Thread/sleep 7000) ;; some overrunning task
  (println "done" now))

(def cancel-stuff! (chime-at (rest (periodic-seq (now) (seconds 5))) do-stuff))

Timeline:

 0 sec -- schedule starts
 5 sec -- do-stuff call 1 starts
10 sec -- time for do-stuff call 2 put into chime-ch channel
12 sec -- do-stuff call 1 finishes, do-stuff call 2 starts
15 sec -- time for do-stuff call 3 put into chime-ch channel
16 sec -- cancel-stuff! called
17 sec -- do-stuff call 2 finishes, do-stuff call 3 starts
24 sec -- do-stuff call 3 finishes

Is this a bug, or the expected behavior? I can see an argument for the latter, since cancel-stuff! is called after the next time in the schedule (and thus after it's added to chime-at's internal chime-ch channel). This is somewhat non-intuitive, however; generally I'd expect a job that hasn't started yet to stay that way if the schedule is cancelled.

I'm aware the README says that with chime-at cancelling overrunning jobs is the caller's responsibility, but this is slightly different situation. If this is by design, you may want to update the documentation to clarify this.

Thank you!

Extension idea - sequence of time + function + some controls

Hi

I've just checked chime and it does things slightly different than what I thought.

If chime-ch takes a seq of map, which may be look like:
(chime-ch [{:at <time1> :fn my-fn1} {:at <time2> :fn my-fn2} ...])
then fn can be executed when time comes.

Also it may allow extra key to control fn execution when the time's already passed:
{:at <time1> :fn fn1 :run-fn-for-passed-time? true}

Dynamic bindings not conveyed

One of the most annoying things about working with Java Executors is that binding-conveyance is on you. Observe this:

(def ^:dynamic *foo* :nothing)
=> #'chime.core/*foo*
(binding [*foo* :something] 
  (chime-at [(.plusSeconds (Instant/now) 1)] (fn [_] (println *foo*))))
=> #object[chime.core$chime_at$reify__1917 0x6303b429 {:status :pending, :val nil}]
:nothing ;; after 1 second

This comes down to going beyond the language (i.e. as long as you stay within Clojure futures,agents,refs you don't need to
worry about it). Thankfully the fix is super simple - use bound-fn:

(.schedule pool ^Runnable (bound-fn [] (task)) ...) ;; wrap task with `bound-fn`

Hope that helps...

(binding [*foo* :something] 
  (chime-at [(.plusSeconds (Instant/now) 1)] (bound-fn [_] (println *foo*))))
=> #object[chime.core$chime_at$reify__1917 0x1df32999 {:status :pending, :val nil}]
:something

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.