├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── sample ├── multi_files.cr └── multi_http.cr ├── shard.yml ├── spec ├── await_async_spec.cr └── spec_helper.cr └── src ├── await_async.cr └── await_async ├── helper.cr └── mini_future.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /shard.lock 9 | 10 | # Test files 11 | /generated/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | 3 | install: 4 | - shards install 5 | 6 | script: 7 | - crystal spec 8 | - crystal tool format --check 9 | - bin/ameba 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yacine Petitprez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Await / Async 2 | 3 | [![Build Status](https://travis-ci.org/anykeyh/await_async.svg?branch=master)](https://travis-ci.org/anykeyh/await_async) 4 | 5 | Add `await` and `async` keywords to Crystal. 6 | 7 | ## Installation 8 | 9 | In your `shards.yml`: 10 | 11 | ```yaml 12 | dependencies: 13 | await_async: 14 | github: anykeyh/await_async 15 | branch: master 16 | ``` 17 | 18 | Then: 19 | 20 | ```crystal 21 | require "await_async" 22 | 23 | future = async fetch_something 24 | 25 | do_some_computation_now 26 | 27 | await future 28 | ``` 29 | 30 | ## Usage 31 | 32 | - Call `async` on any method or block to create a `MiniFuture` 33 | - Call `await` on any `MiniFuture` to wait for/get the result 34 | - Conveniently, you can call `await` on future's array. 35 | 36 | Can improve drastically application which relay on blocking IO like web API 37 | or file writing. 38 | 39 | ### await(timeout, future) 40 | 41 | ```crystal 42 | future = async check_website 43 | 44 | begin 45 | await 5.seconds, future 46 | rescue MiniFuture::TimeoutException 47 | # rescue from timeout 48 | end 49 | ``` 50 | 51 | ### `async!` / `async` 52 | 53 | By default, `async!` call the newly created fiber just after creation. 54 | 55 | - You can use instead `async` so the fiber won't start now: 56 | 57 | ```crystal 58 | future = async! { 1 + 2 } 59 | # At this moment the result is already computed 60 | # future.finished? == true 61 | await future # => 3 62 | 63 | # vs 64 | 65 | future = async { 1 + 2 } 66 | # Here the result is not computed 67 | # future.finished? == false 68 | await future # Compute now 69 | ``` 70 | 71 | Usually, use `async` if your block is computation intensive and current thread 72 | has IO blocking operation. Use `async!` in other cases. 73 | 74 | In case of errors, the exception will be raise at `await` moment, in the await 75 | thread. 76 | 77 | ## `MiniFuture` 78 | 79 | A minimalist version of future. Has `finished?` and `running?` methods. 80 | 81 | I don't use Crystal's `Concurrent::Future` class because `:nodoc:`. 82 | 83 | ## Why? 84 | 85 | Because crystal is great for building CLI tools. And CLI deals a lot with 86 | files and sockets. And IO performed in main thread are slow. 87 | 88 | Usage of `Channel` is recommended for complex software, as it offers more patterns. 89 | 90 | `await/async` is useful to build fast and deliver fast. 91 | 92 | ## I don't want await/async to be exported in the global scope 93 | 94 | 1. require `await_async/helper` instead of `await_async` 95 | 2. In the class/module you want to use the methods, add `include AwaitAsync::Helper`. 96 | You can also simply call `await/async` directly from `AwaitAsync::Helper` 97 | 98 | ## Example 99 | 100 | ```crystal 101 | def fetch_websites_async 102 | %w[ 103 | www.github.com 104 | www.yahoo.com 105 | www.facebook.com 106 | www.twitter.com 107 | crystal-lang.org 108 | ].map do |url| 109 | async! do 110 | HTTP::Client.get "https://#{url}" 111 | end 112 | end 113 | end 114 | 115 | # Process the websites concurrently. Start querying another website when the 116 | # first one is waiting for response 117 | await(5.seconds, fetch_websites_async).each do |response| 118 | # ... 119 | end 120 | ``` 121 | 122 | ## License 123 | 124 | MIT 125 | -------------------------------------------------------------------------------- /sample/multi_files.cr: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "file_utils" 3 | require "../src/await_async" 4 | 5 | FileUtils.rm_rf "generated" 6 | FileUtils.mkdir "generated" 7 | 8 | def multiple_small_write 9 | 2048.times do |i| 10 | File.write("generated/#{i}", i.to_s) 11 | end 12 | end 13 | 14 | def multiple_small_write_async 15 | 2048.times.map do |i| 16 | async! File.write("generated/#{i}", i.to_s) 17 | end 18 | end 19 | 20 | puts "With synchronous call:" 21 | puts Benchmark.measure { multiple_small_write } 22 | 23 | FileUtils.rm_rf "generated/*" 24 | 25 | puts "With asynchronous call:" 26 | puts Benchmark.measure { await multiple_small_write_async } 27 | -------------------------------------------------------------------------------- /sample/multi_http.cr: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "http/client" 3 | require "../src/await_async" 4 | 5 | WEBSITES = %w[ 6 | www.github.com 7 | www.yahoo.com 8 | www.facebook.com 9 | www.twitter.com 10 | crystal-lang.org 11 | ] 12 | 13 | def fetch_websites 14 | WEBSITES.map do |url| 15 | HTTP::Client.get "https://#{url}" 16 | end 17 | end 18 | 19 | def fetch_websites_async 20 | WEBSITES.map do |url| 21 | async! do 22 | HTTP::Client.get "https://#{url}" 23 | end 24 | end 25 | end 26 | 27 | puts "With synchronous call:" 28 | puts Benchmark.measure { fetch_websites } 29 | 30 | puts "With asynchronous call:" 31 | puts Benchmark.measure { await fetch_websites_async } 32 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: await_async 2 | version: 0.1.0 3 | 4 | crystal: 0.36.1 5 | 6 | authors: 7 | - Yacine Petitprez 8 | 9 | description: | 10 | Provide await and async keyword in crystal language 11 | 12 | development_dependencies: 13 | ameba: 14 | github: crystal-ameba/ameba 15 | version: ~> 0.13.4 16 | 17 | license: MIT 18 | -------------------------------------------------------------------------------- /spec/await_async_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | def long_running_method 4 | sleep 0.1 5 | "FOO" 6 | end 7 | 8 | describe AwaitAsync::Helper do 9 | it "works with method" do 10 | x = async! long_running_method 11 | (await x).should eq "FOO" 12 | end 13 | 14 | it "works with block" do 15 | x = async! { "FOO" } 16 | (await x).should eq "FOO" 17 | end 18 | 19 | it "works with array" do 20 | arr = 3.times.map { |x| async! { 1 + x } } 21 | await(arr).to_a.should eq [1, 2, 3] 22 | end 23 | 24 | it "can check finished? method" do 25 | x = async! { 26 | sleep 0.1 27 | "FOO" 28 | } 29 | x.finished?.should be_false 30 | x.running?.should be_true 31 | 32 | (await x).should eq "FOO" 33 | x.finished?.should be_true 34 | x.running?.should be_false 35 | 36 | x = async! { 37 | sleep 0.1 38 | :BAR 39 | } 40 | sleep 0.15 # < Should give time to run the fiber. 41 | 42 | (await x).should eq :BAR 43 | x.finished?.should be_true 44 | x.running?.should be_false 45 | end 46 | 47 | it "offers low priority async" do 48 | async! { 1 + 1 }.finished?.should be_true 49 | async { 1 + 1 }.finished?.should be_false 50 | end 51 | 52 | it "can call multiple time await" do 53 | f = async! { 1 + 1 } 54 | await f 55 | await f 56 | end 57 | 58 | it "can await with timeout" do 59 | x = async! { sleep 0.5 } 60 | 61 | expect_raises MiniFuture::TimeoutException do 62 | await 0.2.seconds, x 63 | end 64 | 65 | x = async! { sleep 0.1 } 66 | await 0.2.seconds, x # < Should not raise exception 67 | end 68 | 69 | it "can await with timeout (array)" do 70 | x = 5.times.map { async! { sleep 0.5 } }.to_a 71 | 72 | expect_raises MiniFuture::TimeoutException do 73 | await 0.2.seconds, x 74 | end 75 | 76 | x = 5.times.map { async! { sleep 0.1 } }.to_a 77 | await 0.2.seconds, x # < Should not raise exception 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/await_async" 3 | -------------------------------------------------------------------------------- /src/await_async.cr: -------------------------------------------------------------------------------- 1 | require "./await_async/helper" 2 | 3 | include AwaitAsync::Helper 4 | -------------------------------------------------------------------------------- /src/await_async/helper.cr: -------------------------------------------------------------------------------- 1 | require "./mini_future" 2 | 3 | module AwaitAsync::Helper 4 | extend self 5 | 6 | # Await a future to resolve. 7 | def await(future : MiniFuture) 8 | future.await 9 | end 10 | 11 | # Await a future to resolve, or throw `MiniFuture::TimeoutException` 12 | # otherwise. 13 | def await(timeout : Time::Span, future : MiniFuture) 14 | future.await(timeout) 15 | end 16 | 17 | # Iterate through all the future and await for them. 18 | def await(futures : Enumerable(MiniFuture(T))) forall T 19 | futures.map(&.await) 20 | end 21 | 22 | # Iterate through all the future and await for them. 23 | def await(timeout : Time::Span, futures : Enumerable(MiniFuture(T))) forall T 24 | futures.map(&.await(timeout)) 25 | end 26 | 27 | # Ask Crystal to run this method asynchronously in its own fiber. 28 | macro async!(method) 29 | MiniFuture(typeof({{method}})).new { {{method}} } 30 | end 31 | 32 | # Ask Crystal to run this method asynchronously in its own fiber. 33 | # 34 | # NOTE: The fiber won't be started after creation. 35 | macro async(method) 36 | MiniFuture(typeof({{method}})).new(immediate: false) { {{method}} } 37 | end 38 | 39 | macro async!(&block) 40 | %lmb = -> { {{block.body}} } 41 | MiniFuture(typeof(%lmb.call)).new { %lmb.call } 42 | end 43 | 44 | macro async(&block) 45 | %lmb = -> { {{block.body}} } 46 | MiniFuture(typeof(%lmb.call)).new(immediate: false) { %lmb.call } 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /src/await_async/mini_future.cr: -------------------------------------------------------------------------------- 1 | # Lightweight Future structure. 2 | class MiniFuture(T) 3 | class TimeoutException < Exception 4 | end 5 | 6 | @status = :running 7 | @channel = Channel(T).new(1) 8 | @error : Exception? 9 | @output : T? 10 | 11 | def initialize(immediate = true, &block : -> T) 12 | spawn do 13 | begin 14 | @channel.send block.call 15 | rescue ex 16 | @error = ex 17 | ensure 18 | @status = :terminated 19 | end 20 | end 21 | 22 | Fiber.yield if immediate 23 | end 24 | 25 | def running? 26 | @status == :running 27 | end 28 | 29 | def finished? 30 | !running? 31 | end 32 | 33 | def error? 34 | @error 35 | end 36 | 37 | def await(timeout : Time::Span? = nil) 38 | if @status != :flushed 39 | if timeout 40 | timeout_channel = Channel(Nil).new 41 | 42 | spawn do 43 | sleep timeout.not_nil! 44 | timeout_channel.send nil unless @status == :flushed 45 | end 46 | 47 | select 48 | when timeout_channel.receive 49 | raise TimeoutException.new 50 | when @output = @channel.receive 51 | @status = :flushed 52 | end 53 | else 54 | @status = :flushed 55 | @output = @channel.receive 56 | end 57 | end 58 | 59 | if ex = @error 60 | raise ex 61 | end 62 | 63 | {% if T.nilable? %} 64 | @output 65 | {% else %} 66 | @output.not_nil! 67 | {% end %} 68 | end 69 | end 70 | --------------------------------------------------------------------------------