├── .gitignore ├── Makefile ├── README.md ├── include └── erlfu.hrl ├── package.exs ├── rebar.config └── src ├── erlfu.app.src ├── erlfu.erl ├── future.erl └── future_tests.erl /.gitignore: -------------------------------------------------------------------------------- 1 | /deps/ 2 | /.eunit/ 3 | /ebin/ 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR=$(shell which rebar || echo ./rebar) 2 | 3 | all: $(REBAR) 4 | $(REBAR) get-deps compile 5 | 6 | tests: $(REBAR) 7 | $(REBAR) eunit skip_deps=true 8 | 9 | sh: all 10 | erl -name f$$$$ -pa .eunit/ -pa ebin/ -pa deps/*/ebin -eval 'erlfu:start(), shell_default:m(future_tests), shell_default:m(future).' 11 | 12 | test: tests 13 | 14 | clean: 15 | $(REBAR) clean skip_deps=true 16 | 17 | # Detect or download rebar 18 | 19 | REBAR_URL=http://cloud.github.com/downloads/basho/rebar/rebar 20 | ./rebar: 21 | erl -noshell -s inets -s ssl \ 22 | -eval 'httpc:request(get, {"$(REBAR_URL)", []}, [], [{stream, "./rebar"}])' \ 23 | -s init stop 24 | chmod +x ./rebar 25 | 26 | distclean: 27 | rm -f ./rebar 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlfu # 2 | 3 | Futures implemented in Erlang. It's an implementation using processes 4 | to represent a future. These futures can be used to: 5 | 1. Store a value later on 6 | 2. Compute a value using a fun 7 | 3. Chain/wrap futures to archive feature composition 8 | 9 | Futures are actually [garbage collected processes](http://github.com/gleber/gcproc) 10 | which are based on Tony Rogvall's [resource](http://github.com/tonyrog/resource) project. 11 | 12 | Notes on limitations: 13 | 14 | 1. requires SMP support 15 | 2. garbage collection works only locally (i.e. future which is not 16 | referenced on the node where it was created will be garbage collected) 17 | 18 | ## Goals ## 19 | 20 | Implement futures/promises framework which allows to chain futures to 21 | implement **reusable mechanisms** like timeouts, authentication, sharding, 22 | etc. 23 | 24 | Inspired by http://monkey.org/~marius/talks/twittersystems/ 25 | 26 | TODO: write more about futures, terminology and possible uses 27 | 28 | ## Roadmap ## 29 | 30 | - make set/2 and exec/2 transparent in regard to wrappers ?? 31 | - add wrappers that pass params to other futures for sharding and authentication 32 | - add complex composition to 33 | -- wait for specific, but gather other values 34 | -- retry next future if first is slow (i.e. redundant fetching data from 35 | replicated database if one of shards is slow) 36 | 37 | ## Compositions ## 38 | 39 | Currently the code supports few basic compositions: 40 | 41 | 1. combine 42 | 2. select 43 | 3. timeout 44 | 4. retry 45 | 5. safe 46 | 6. catcher 47 | 48 | ## Examples ## 49 | 50 | Simple example with delayed setting of value: 51 | ```erlang 52 | 1> F = future:new(fun() -> timer:sleep(10000), 10 end). 53 | {future,<0.36.0>,#Ref<0.0.0.1736>,undefined} 54 | 2> F:get(). %% it hangs for 10 seconds 55 | 10 56 | ``` 57 | 58 | Exceptions are propagated with stacktrace preserved: 59 | ```erlang 60 | 4> F = future:new(fun() -> a = b end). 61 | {future,<0.41.0>,#Ref<0.0.0.21416>,undefined} 62 | 5> F:get(). 63 | ** exception error: no match of right hand side value b 64 | in function erl_eval:expr/3 65 | in call from future:get/1 (src/future.erl, line 94) 66 | 6> 67 | ``` 68 | 69 | Values can be bound to future after it is created: 70 | ```erlang 71 | 7> F = future:new(). 72 | {future,<0.47.0>,#Ref<0.0.0.27235>,undefined} 73 | 8> spawn(fun() -> timer:sleep(10000), F:set(42) end). 74 | <0.49.0> 75 | 9> F:get(). 76 | 42 77 | ``` 78 | 79 | Futures can be cloned to rerun them if need be: 80 | ```erlang 81 | 65> F = future:new(fun() -> timer:sleep(5000), io:format("Run!~n"), crypto:rand_uniform(0, 100) end). 82 | {future,{gcproc,<0.1546.0>,{resource,160354072,<<>>}}, 83 | #Ref<0.0.0.2024>,undefined} 84 | 66> F:get(). 85 | Run! 86 | 16 87 | 67> F2 = F:clone(). 88 | {future,{gcproc,<0.1550.0>,{resource,160354272,<<>>}}, 89 | #Ref<0.0.0.2033>,undefined} 90 | 68> F2:get(). 91 | Run! 92 | 75 93 | ``` 94 | Deeply wrapped futures can be cloned as well, see `future_tests:clone_side_effect_fun_test/0`. 95 | Combined futures can be cloned as well, see `future_tests:clone_combined_test/0`. 96 | 97 | Multiple futures' values can be collected. If one future fails 98 | everything will fail: 99 | ```erlang 100 | 5> F1 = future:new(fun() -> timer:sleep(3000), 10 end). 101 | {future,<0.50.0>,#Ref<0.0.0.76697>,undefined} 102 | 6> F2 = future:new(fun() -> timer:sleep(3000), 5 end). 103 | {future,<0.53.0>,#Ref<0.0.0.76941>,undefined} 104 | 7> lists:sum(future:collect([F1, F2])). 105 | 15 106 | ``` 107 | 108 | One can map over futures, which allows to run multiple concurrent 109 | computations in parallel: 110 | ```erlang 111 | 1> F1 = future:new(fun() -> timer:sleep(3000), 10 end). 112 | {future,{gcproc,<0.45.0>,{resource,35806032,<<>>}}, 113 | #Ref<0.0.0.86>,undefined} 114 | 2> F2 = future:new(fun() -> timer:sleep(3000), 5 end). 115 | {future,{gcproc,<0.49.0>,{resource,35805216,<<>>}}, 116 | #Ref<0.0.0.92>,undefined} 117 | 3> F3 = future:map(fun(X) -> X:get() * 2 end, [F1, F2]). 118 | {future,{gcproc,<0.52.0>,{resource,35798200,<<>>}}, 119 | #Ref<0.0.0.97>,undefined} 120 | 4> F3:get(). 121 | [20,10] 122 | ``` 123 | 124 | Futures can be used to capture process termination reason: 125 | ```erlang 126 | 1> Pid = spawn(fun() -> timer:sleep(10000), erlang:exit(shutdown) end). 127 | <0.70.0> 128 | 2> F = future:wait_for(Pid). 129 | {future,{gcproc,<0.72.0>,{resource,47420068511440,<<>>}}, 130 | #Ref<0.0.0.276>,undefined} 131 | 3> F:get(). 132 | shutdown 133 | ``` 134 | 135 | A fun can be executed when future is bound: 136 | ```erlang 137 | 1> Self = self(). 138 | <0.38.0> 139 | 2> F = future:new(fun() -> timer:sleep(1000), 42 end). 140 | {future,{gcproc,<0.46.0>,{resource,17793680,<<>>}}, 141 | #Ref<0.0.0.104>,undefined} 142 | 3> F:on_done(fun(Result) -> Self ! Result end). 143 | {future,{gcproc,<0.51.0>,{resource,47235452025976,<<>>}}, 144 | #Ref<0.0.0.111>,undefined} 145 | 4> flush(). 146 | Shell got {ok,42} 147 | ok 148 | ``` 149 | `future:on_success/2` and `future:on_failure/2` can be used to execute 150 | a fun when future bounds to a value or to an exception respectively. 151 | 152 | ### Compositions ### 153 | 154 | #### Timeout #### 155 | 156 | Timeout future wrapper can be used to limit time of execution of a future: 157 | ```erlang 158 | 8> F = future:timeout(future:new(fun() -> timer:sleep(1000), io:format("Done!") end), 500). 159 | {future,<0.51.0>,#Ref<0.0.0.18500>,undefined} 160 | 9> F:get(). 161 | ** exception throw: timeout 162 | in function future:'-timeout/2-fun-0-'/2 (src/future.erl, line 270) 163 | in call from future:'-wrap/2-fun-0-'/2 (src/future.erl, line 220) 164 | in call from future:'-do_exec/3-fun-0-'/3 (src/future.erl, line 42) 165 | in call from future:handle/1 (src/future.erl, line 188) 166 | ``` 167 | but if timeout time is larger than 1 second it will normally perform 168 | expected computation: 169 | ```erlang 170 | 13> F = future:timeout(future:new(fun() -> timer:sleep(1000), io:format("Done!~n"), done_returned end), 5000), F:get(). 171 | Done! 172 | done_returned 173 | ``` 174 | 175 | #### Retry #### 176 | A wrapper which implements retrying in non-invasive way. It can be 177 | used to limit number of retries of establishing connection to external 178 | possibly-faulty resource. Example: 179 | 180 | ```erlang 181 | 10> F = future:new(fun() -> {ok, S} = gen_tcp:connect("faulty-host.com", 80, []), S end). 182 | {future,<0.69.0>,#Ref<0.0.0.125>,undefined} 183 | 11> F2 = future:retry(F). 184 | {future,<0.72.0>,#Ref<0.0.0.132>,undefined} 185 | 12> F2:get(). 186 | #Port<0.937> 187 | ``` 188 | or 189 | ```erlang 190 | 1> F = future:new(fun() -> {ok, S} = gen_tcp:connect("non-existing-host.com", 23, []), S end). 191 | {future,<0.34.0>,#Ref<0.0.0.67>,undefined} 192 | 2> F2 = future:retry(F). 193 | {future,<0.39.0>,#Ref<0.0.0.76>,undefined} 194 | 3> F2:get(). 195 | ** exception error: {retry_limit_reached,3,{error,{badmatch,{error,nxdomain}}}} 196 | in function erl_eval:expr/3 197 | in call from future:reraise/1 (src/future.erl, line 232) 198 | in call from future:'-wrap/2-fun-0-'/2 (src/future.erl, line 263) 199 | in call from future:'-do_exec/3-fun-0-'/3 (src/future.erl, line 43) 200 | in call from future:reraise/1 (src/future.erl, line 232) 201 | ``` 202 | 203 | #### Safe #### 204 | Safe wrapper wraps future execution and catches errors and exits: 205 | ```erlang 206 | 18> F = future:safe(future:new(fun() -> error(error_in_computation) end)), F:get(). 207 | {error,error_in_computation} 208 | ``` 209 | but it will pass through throws, since they are a code flow control 210 | tools. 211 | 212 | #### Catcher #### 213 | Catcher wrapper is a stronger variant of Safe wrapper, which 214 | intercept all exceptions, including errors, exits and throws: 215 | ```erlang 216 | 21> F = future:catcher(future:new(fun() -> throw(premature_end) end)), F:get(). 217 | {error,throw,premature_end} 218 | ``` 219 | -------------------------------------------------------------------------------- /include/erlfu.hrl: -------------------------------------------------------------------------------- 1 | -define(is_future(F), is_record(F, future)). 2 | -define(is_futurable(F), (?is_future(F) orelse is_function(F, 0))). 3 | 4 | -define(is_realized(F), (?is_future(F) 5 | andalso (F#future.proc == undefined) 6 | andalso (F#future.ref == undefined))). 7 | -opaque future() :: tuple(). 8 | %% -type future() :: #future{}. 9 | -type future_res() :: {'value', term()} | 10 | {'error', {atom(), term(), list(term())}}. 11 | -opaque future_attachment() :: {reference(), reference()}. 12 | 13 | 14 | -record(future, {proc :: gcproc:gcproc(), 15 | ref :: reference(), 16 | result :: 'undefined' | future_res()}). 17 | -------------------------------------------------------------------------------- /package.exs: -------------------------------------------------------------------------------- 1 | Expm.Package.new(name: "erlfu", description: "Futures/promises for Erlang implemented as garbage collected processes", 2 | version: "0.9.0", keywords: ["future", "promise", "language extension", "gc"], 3 | maintainers: [[name: "Gleb Peregud", 4 | email: "gleber.p@gmail.com"]], 5 | repositories: [[github: "gleber/erlfu"]]) 6 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | 3 | {deps, [ 4 | {gcproc, ".*", {git, "https://github.com/gleber/gcproc.git", "HEAD"}}, 5 | {erlando, ".*", {git, "https://github.com/rabbitmq/erlando.git", "HEAD"}}, 6 | {proper, ".*", {git, "https://github.com/manopapad/proper.git", "HEAD"}} 7 | ]}. 8 | -------------------------------------------------------------------------------- /src/erlfu.app.src: -------------------------------------------------------------------------------- 1 | {application, erlfu, [ 2 | {description, "erlfu - erlang implementation of futures"}, 3 | {vsn, "0.9.0"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | gcproc 10 | ]}, 11 | {env, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/erlfu.erl: -------------------------------------------------------------------------------- 1 | -module(erlfu). 2 | 3 | -export([start/0]). 4 | 5 | ensure_started(App) -> 6 | case application:start(App) of 7 | ok -> ok; 8 | {error, {already_started, App}} -> 9 | ok 10 | end. 11 | 12 | start() -> 13 | ensure_started(gcproc), 14 | ensure_started(erlfu). 15 | -------------------------------------------------------------------------------- /src/future.erl: -------------------------------------------------------------------------------- 1 | -module(future). 2 | 3 | -export([%% creating 4 | new/0, %% creates unbounded future 5 | new/1, %% creates a future with a fun to compute 6 | new_static/1, %% creates a future with a static value 7 | new_static_error/1, new_static_error/2, new_static_error/3, %% creates a future with a static error 8 | 9 | wait_for/1, %% bounds to process termination reason 10 | on_done/2, %% executes fun() upon execution of future() (or when it's bound) 11 | on_success/2, %% executes fun() upon successful execution of future() 12 | on_failure/2, %% executes fun() upon failed execution (i.e. execution 13 | %% failed with error or exit) of future() 14 | 15 | %% manipulating; all these will fail if future is already bound 16 | set/2, %% sets value of a future to a term 17 | set_error/2, %% sets value of a future to an error 18 | set_error/3, %% as above 19 | set_error/4, %% as above 20 | execute/2, %% tells future to execute a fun and set it's own value, 21 | clone/1, %% clones a future (clone properly clones deeply wrapped futures) 22 | 23 | %% getting 24 | ready/1, %% returns true if future is bounded 25 | get/1, %% waits for future to compute and returns the value 26 | call/1, %% the same as get/1 27 | as_fun/1, %% returns fun, which returns the same value as get/1 28 | 29 | %% finishing 30 | realize/1, %% waits for future to compute, stops the future process and returns bounded local future 31 | done/1, %% waits for future to compute and stops the future process 32 | cancel/1 %% cancels the future and stops the process 33 | ]). 34 | 35 | %% collections 36 | -export([select/1, %% finds first available future among provided futures 37 | select_value/1, %% value of the above 38 | 39 | select_full/1, %% returns first available future among provided futures, 40 | %% list of already failed futures and list 41 | %% of not-yet-responded futures 42 | select_full_value/1, %% value of the above 43 | 44 | combine/1, %% combines multiple futures into a future which returns a list of values 45 | combine_value/1, %% value of the above 46 | 47 | map/2, %% maps multiple futures with a fun and returns combined future 48 | wrap/1, wrap/2, %% returns a future, which wraps argument future with a fun 49 | chain/1, chain/2 %% returns a future, which realizes argument future and wraps it with a fun 50 | ]). 51 | 52 | %% wrappers 53 | -export([timeout/1, %% limits future time-to-bound to 5000ms 54 | timeout/2, %% as above, with configurable timeout 55 | safe/1, %% catches errors and exceptions 56 | catcher/1, %% catches errors, exceptions and throws 57 | retry/1, retry/2 %% retries on errors (3 times by default) 58 | ]). 59 | 60 | %% low level functions - useful for implementing compositions 61 | -export([ref/1, %% returns future reference 62 | proc/1, %% returns future's process as a gcproc 63 | attach/1, %% attaches process to a future, which results in a message 64 | %% {future, reference(), future_res()} sent to self 65 | %% and returns future_attachment() 66 | 67 | detach/2, %% detach/2 will consume the message from the future based on 68 | %% future_attachment(), and returns future_res() 69 | 70 | handle/1 %% handles future_res(), returns appropriate value or raises appropriate exception 71 | ]). 72 | 73 | -include_lib("erlfu/include/erlfu.hrl"). 74 | 75 | -type future_opt() :: {'wraps', future()}. 76 | 77 | -record(state, {ref :: reference(), 78 | waiting = [] :: list(pid()), 79 | executable :: 'undefined' | pid(), 80 | result :: 'undefined' | future_res(), 81 | worker :: 'undefined' | pid(), 82 | opts = [] :: list(future_opt())}). 83 | 84 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 85 | %% 86 | %% API: basics 87 | %% 88 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 89 | 90 | new() -> 91 | Ref = make_ref(), 92 | Proc = gcproc:spawn(fun() -> 93 | loop(#state{ref = Ref}) 94 | end), 95 | #future{proc = Proc, ref = Ref}. 96 | 97 | new(Future) -> 98 | new0(Future, []). 99 | 100 | new0(Future, []) when ?is_future(Future) -> 101 | Future; 102 | 103 | new0(Fun, Opts) when is_function(Fun, 0) -> 104 | Ref = make_ref(), 105 | Proc = gcproc:spawn(fun() -> 106 | W = do_exec(Ref, Fun, []), 107 | loop(#state{ref = Ref, 108 | worker = W, 109 | executable = Fun, 110 | opts = Opts}) 111 | end), 112 | #future{proc = Proc, ref = Ref}; 113 | 114 | new0(Fun, Opts) when is_function(Fun, 1) -> 115 | Ref = make_ref(), 116 | Wraps = proplists:get_value(wraps, Opts), 117 | true = (Wraps /= undefined), 118 | Proc = gcproc:spawn(fun() -> 119 | W = do_exec(Ref, Fun, [Wraps]), 120 | loop(#state{ref = Ref, 121 | worker = W, 122 | executable = Fun, 123 | opts = Opts}) 124 | end), 125 | #future{proc = Proc, ref = Ref}. 126 | 127 | new_static(Term) -> 128 | Ref = make_ref(), 129 | Proc = gcproc:spawn(fun() -> 130 | loop(#state{ref = Ref, 131 | result = {value, Term}}) 132 | end), 133 | #future{proc = Proc, ref = Ref}. 134 | 135 | new_static_error(Error) -> 136 | new_static_error(error, Error). 137 | 138 | new_static_error(Class, Error) -> 139 | new_static_error(Class, Error, []). 140 | 141 | new_static_error(Class, Error, Stack) -> 142 | Ref = make_ref(), 143 | Proc = gcproc:spawn(fun() -> 144 | loop(#state{ref = Ref, 145 | result = {error, {Class, Error, Stack}}}) 146 | end), 147 | #future{proc = Proc, ref = Ref}. 148 | 149 | execute(Fun, #future{ref = Ref} = Self) when is_function(Fun, 0) -> 150 | do_call(Self, {execute, Ref, self(), Fun}, executing), 151 | Self. 152 | 153 | set_error(Error, #future{} = Self) -> 154 | Self:set_error(error, Error). 155 | set_error(Class, Error, #future{} = Self) -> 156 | Self:set_error(Class, Error, []). 157 | 158 | set_error(Class, Error, Stacktrace, #future{ref = Ref} = Self) -> 159 | Err = {Class, Error, Stacktrace}, 160 | do_call(Self, {set, Ref, self(), {error, Err}}, set), 161 | Self#future{result = {error, Err}}. 162 | 163 | set(Value, #future{ref = Ref} = Self) -> 164 | do_call(Self, {set, Ref, self(), {value, Value}}, set), 165 | Self#future{result = {value, Value}}. 166 | 167 | done(#future{proc = Proc, ref = Ref} = _Self) -> 168 | %% Let future process know it should terminate gracefully as soon as it is done 169 | Proc:send({done, Ref}), 170 | ok. 171 | 172 | realize(#future{} = Self) when ?is_realized(Self) -> 173 | Self; %% realizing already realized future yields no changes 174 | 175 | realize(#future{} = Self) -> 176 | Res = do_get(Self), 177 | Self:done(), 178 | Self#future{proc = undefined, ref = undefined, result = Res}. 179 | 180 | call(Self) -> 181 | Self:get(). 182 | 183 | get(#future{} = Self) -> 184 | handle(do_get(Self)). 185 | 186 | as_fun(Self) -> 187 | fun() -> Self:get() end. 188 | 189 | ready(#future{result = {_Type, _Value}}) -> 190 | true; 191 | ready(#future{ref = Ref, result = undefined} = Self) -> 192 | case do_call(Self, {ready, Ref, self()}, future_ready) of 193 | {future_ready, Ref, Ready} -> 194 | Ready 195 | end. 196 | 197 | cancel(#future{proc = Proc, ref = Ref} = F) -> 198 | Mon = Proc:monitor(), 199 | Proc:send({cancel, Ref}), 200 | receive 201 | {'DOWN', Mon, process, _Pid, normal} -> ok 202 | end, 203 | Proc:demonitor(Mon), 204 | F#future{proc = undefined, ref = Ref}. 205 | 206 | clone(#future{ref = Ref} = Self) -> 207 | Info = do_call(Self, {get_info, Ref, self()}, future_info), 208 | case Info of 209 | %% future_info, Ref, Result, Executable, Wraps 210 | {future_info, Ref, undefined, undefined, undefined} -> 211 | new(); 212 | {future_info, Ref, {value, Result}, undefined, undefined} -> 213 | new_static(Result); 214 | {future_info, Ref, {error, {Class, Error, Stack}}, undefined, undefined} -> 215 | new_static_error(Class, Error, Stack); 216 | {future_info, Ref, _, Fun, undefined} when is_function(Fun) -> 217 | new(Fun); 218 | {future_info, Ref, _, Fun, Wraps} 219 | when is_function(Fun), ?is_future(Wraps) -> 220 | Wraps2 = Wraps:clone(), 221 | wrap(Fun, Wraps2); 222 | {future_info, Ref, _, Fun, ListOfWraps} 223 | when is_function(Fun), is_list(ListOfWraps) -> 224 | ListOfWraps2 = [ X:clone() || X <- ListOfWraps ], 225 | wrap(Fun, ListOfWraps2) 226 | end. 227 | 228 | wait_for(Proc) when element(1, Proc) == gcproc -> 229 | new(fun() -> 230 | Mon = Proc:monitor(), 231 | receive 232 | {'DOWN', Mon, process, _Pid, Reason} -> 233 | Reason 234 | end 235 | end); 236 | wait_for(Pid) when is_pid(Pid) -> 237 | new(fun() -> 238 | Mon = erlang:monitor(process, Pid), 239 | receive 240 | {'DOWN', Mon, process, Pid, Reason} -> 241 | Reason 242 | end 243 | end). 244 | 245 | on_done(F, Future) when is_function(F, 1), ?is_future(Future) -> 246 | wrap(fun(X) -> 247 | {ok, F(X:get())} 248 | end, catcher(Future)). 249 | 250 | on_success(F, Future) when is_function(F, 1), ?is_future(Future) -> 251 | wrap(fun(X) -> 252 | case X:get() of 253 | {ok, Value} -> 254 | {ok, F(Value)}; 255 | _ -> {error, failed} 256 | end 257 | end, catcher(Future)). 258 | 259 | on_failure(F, Future) when is_function(F, 1), ?is_future(Future) -> 260 | wrap(fun(X) -> 261 | case X:get() of 262 | {ok, _Value} -> 263 | {error, succeeded}; 264 | {error, Error} -> 265 | {ok, F(Error)} 266 | end 267 | end, catcher(Future)). 268 | 269 | wrap([Initial0|List]) when ?is_futurable(Initial0) -> 270 | Initial = new(Initial0), 271 | lists:foldl(fun wrap/2, Initial, List). 272 | 273 | wrap(Wrapper, Futures0) when is_function(Wrapper), 274 | is_list(Futures0) -> 275 | {wrapper_arity_1, true, _} = {wrapper_arity_1, is_function(Wrapper, 1), erlang:fun_info(Wrapper)}, 276 | Futures = [ new(X) || X <- Futures0 ], 277 | new0(fun(X) -> 278 | Wrapper(X) %%X:done() 279 | end, 280 | [{wraps, Futures}]); 281 | wrap(Wrapper, Future0) when is_function(Wrapper), 282 | ?is_futurable(Future0) -> 283 | {wrapper_arity_1, true, _} = {wrapper_arity_1, is_function(Wrapper, 1), erlang:fun_info(Wrapper)}, 284 | Future = new(Future0), 285 | new0(fun(X) -> 286 | Wrapper(X) %%X:done() 287 | end, 288 | [{wraps, Future}]). 289 | 290 | chain([C|List]) when is_list(List) -> 291 | Initial = new(C), 292 | lists:foldl(fun(S, Res) -> 293 | chain(Res, S) 294 | end, Initial, List). 295 | 296 | chain(C1, C2) when ?is_futurable(C1), is_function(C2, 1) -> 297 | chain0(C1, C2, []). 298 | 299 | map(Fun, Futures) -> 300 | new0(fun(X) -> 301 | Fs = [ wrap(Fun, F) || F <- X ], 302 | do_collect(Fs) 303 | end, 304 | [{wraps, Futures}]). 305 | 306 | combine(Futures) -> 307 | new0(fun(X) -> 308 | do_collect(X) 309 | end, 310 | [{wraps, Futures}]). 311 | 312 | combine_value(Futures) -> 313 | F = combine(Futures), 314 | F:get(). 315 | 316 | select_full(Futures) -> 317 | new0(fun() -> 318 | do_select_any(Futures) 319 | end, 320 | [{wraps, Futures}]). 321 | 322 | select_full_value(Futures) -> 323 | F = select_full(Futures), 324 | F:get(). 325 | 326 | select(Futures) -> 327 | new0(fun() -> 328 | {F, _, _} = do_select_any(Futures), 329 | F 330 | end, 331 | [{wraps, Futures}]). 332 | 333 | select_value(Futures) -> 334 | F = select(Futures), 335 | F:get(). 336 | 337 | proc(Self) -> 338 | Self#future.proc. 339 | 340 | ref(Self) -> 341 | Self#future.ref. 342 | 343 | %% ============================================================================= 344 | %% 345 | %% API: Standard wrappers 346 | %% 347 | %% ============================================================================= 348 | 349 | %% Future to add: 350 | %% 1. DONE retries 351 | %% 2. stats 352 | %% 3. auth 353 | %% 4. logging 354 | 355 | retry(F) -> 356 | retry(F, 3). 357 | retry(F, Count) -> 358 | wrap(fun(X) -> 359 | retry_wrapper(X, 0, Count) 360 | end, F). 361 | 362 | timeout(F) -> 363 | timeout(F, 5000). 364 | timeout(F, Timeout) -> 365 | wrap(fun(#future{proc = Proc} = X) -> 366 | {Ref, Mon} = attach(X), 367 | %% unwrapped do_detach is done below 368 | receive 369 | {future, Ref, Res} -> 370 | case Mon of 371 | undefined -> ok; 372 | _ -> Proc:demonitor(Mon) 373 | end, 374 | %% X:done(), 375 | handle(Res); 376 | {'DOWN', Mon, process, _Pid, Reason} -> 377 | Proc:demonitor(Mon), 378 | reraise_down_reason(Reason) 379 | after Timeout -> 380 | Proc:demonitor(Mon), 381 | X:cancel(), 382 | throw(timeout) 383 | end 384 | end, F). 385 | 386 | safe(F) -> 387 | wrap(fun(X) -> 388 | Res = do_get(X), 389 | %% X:done(), 390 | case Res of 391 | {value, R} -> 392 | {ok, R}; 393 | {error, {error, Error, _}} -> 394 | {error, Error}; 395 | {error, {exit, Error, _}} -> 396 | {error, Error}; 397 | {error, {throw, Error, _}} -> 398 | throw(Error) 399 | end 400 | end, F). 401 | 402 | catcher(F) -> 403 | wrap(fun(X) -> 404 | Res = do_get(X), 405 | %% X:done(), 406 | case Res of 407 | {value, R} -> 408 | {ok, R}; 409 | {error, {Class, Error, _}} -> 410 | {error, Class, Error} 411 | end 412 | end, F). 413 | 414 | 415 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 416 | %% 417 | %% Internal: loops and functions 418 | %% 419 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 420 | 421 | loop(State) -> 422 | %% erlang:process_flag(trap_exit, true), 423 | loop0(State). 424 | 425 | %% loop0(#state{result = {lazy, Fun}} = State) -> 426 | %% %% implement: 427 | %% %% - cancel 428 | %% %% - get 429 | %% %% - ready 430 | %% %% drop: 431 | %% %% - execute 432 | %% %% - set 433 | %% ok; 434 | 435 | %% loop of an unbounded future 436 | loop0(#state{ref = Ref, 437 | waiting = Waiting, 438 | result = undefined, 439 | worker = Worker, 440 | executable = Exec, 441 | opts = Opts} = State) -> 442 | {WorkerPid, WorkerMon} = case Worker of 443 | {_,_} -> Worker; 444 | undefined -> {undefined, undefined} 445 | end, 446 | receive 447 | {get_info, Ref, Requester} -> 448 | Requester ! {future_info, Ref, 449 | undefined, Exec, 450 | proplists:get_value(wraps, Opts)}, 451 | loop0(State); 452 | 453 | {cancel, Ref} -> 454 | case Worker of 455 | undefined -> ok; 456 | {Pid, MonRef} -> 457 | exit(Pid, kill), 458 | receive 459 | {'DOWN', MonRef, process, Pid, _} -> ok 460 | end 461 | end, 462 | ok; 463 | 464 | {execute, Ref, Caller, Fun} -> 465 | case proplists:get_value(wraps, Opts) of 466 | undefined -> ok; 467 | _ -> error({notimplemented, "Set is not transparent for wrapping futures"}) 468 | end, 469 | case Worker of 470 | undefined -> 471 | Caller ! {executing, Ref}, 472 | NewWorker = do_exec(Ref, Fun, []), 473 | loop0(State#state{worker = NewWorker, executable = Fun}); 474 | _ -> 475 | Caller ! {'EXIT', executing, Ref, badfuture}, 476 | loop0(State) 477 | end; 478 | 479 | {computed, Ref, Result} -> 480 | notify(Ref, Waiting, Result), 481 | receive 482 | {'DOWN', WorkerMon, process, WorkerPid, _} -> ok 483 | end, 484 | loop0(State#state{waiting = [], result = Result, worker = undefined}); 485 | 486 | {set, Ref, Caller, Result} -> 487 | case proplists:get_value(wraps, Opts) of 488 | undefined -> ok; 489 | _ -> error({notimplemented, "Set is not transparent for wrapping futures"}) 490 | end, 491 | case Worker of 492 | undefined -> 493 | Caller ! {set}, 494 | notify(Ref, Waiting, Result), 495 | loop0(State#state{waiting = [], result = Result}); 496 | _ -> 497 | Caller ! {'EXIT', set, Ref, badfuture}, 498 | loop0(State) 499 | end; 500 | 501 | {ready, Ref, Requester} -> 502 | Requester ! {future_ready, Ref, false}, 503 | loop0(State); 504 | 505 | {get, Ref, Requester} -> 506 | loop0(State#state{waiting = [Requester | Waiting]}); 507 | 508 | {'DOWN', WorkerMon, process, WorkerPid, Reason} -> 509 | reraise_down_reason(Reason) 510 | end; 511 | 512 | %% loop of a bounded future 513 | loop0(#state{ref = Ref, 514 | waiting = [], 515 | result = {Type, _Value} = Result, 516 | worker = undefined, 517 | executable = Exec, 518 | opts = Opts} = State) when Type /= lazy -> 519 | receive 520 | {get_info, Ref, Requester} -> 521 | Requester ! {future_info, Ref, Result, Exec, proplists:get_value(wraps, Opts)}, 522 | loop0(State); 523 | {cancel, Ref} -> ok; 524 | {done, Ref} -> ok; %% upon receiving done bounded future terminates 525 | {execute, Ref, Caller, _Fun} -> 526 | Caller ! {'EXIT', executing, Ref, badfuture}, 527 | loop0(State); %% futures can be bound only once 528 | {computed, Ref, _} -> exit(bug); %% futures can be bound only once 529 | {set, Ref, Caller, Result} -> 530 | Caller ! {set}, 531 | loop0(State); 532 | {set, Ref, Caller, _V} -> 533 | Caller ! {'EXIT', executing, Ref, badfuture}, 534 | loop0(State); %% futures can be bound only once 535 | {ready, Ref, Requester} -> 536 | Requester ! {future_ready, Ref, true}, 537 | loop0(State); 538 | {get, Ref, Requester} -> 539 | notify(Ref, Requester, Result), 540 | loop0(State) 541 | end. 542 | 543 | 544 | notify(Ref, Pid, Result) when is_pid(Pid) -> 545 | notify(Ref, [Pid], Result); 546 | notify(_, [], _) -> 547 | ok; 548 | notify(Ref, [P|T], Result) -> 549 | P ! {future, Ref, Result}, 550 | notify(Ref, T, Result). 551 | 552 | do_exec(Ref, Fun, Args) -> 553 | Pid = self(), 554 | spawn_monitor( 555 | fun() -> 556 | try 557 | Res = apply(Fun, Args), 558 | Pid ! {computed, Ref, {value, Res}} 559 | catch 560 | Class:Error -> 561 | Pid ! {computed, Ref, {error, {Class, Error, erlang:get_stacktrace()}}} 562 | end 563 | end). 564 | 565 | do_call(#future{proc = Proc, ref = Ref}, Msg, RespTag) -> 566 | Mon = Proc:monitor(), 567 | Proc:send(Msg), 568 | receive 569 | Resp when is_tuple(Resp), 570 | element(1, Resp) == 'EXIT'; 571 | element(2, Resp) == RespTag; 572 | element(3, Resp) == Ref -> 573 | Proc:demonitor(Mon), 574 | erlang:error(element(4, Resp)); 575 | Resp when is_tuple(Resp), 576 | element(1, Resp) == RespTag; 577 | element(2, Resp) == Ref -> 578 | Proc:demonitor(Mon), 579 | Resp; 580 | {'DOWN', Mon, process, _Pid, Reason} -> 581 | Proc:demonitor(Mon), 582 | reraise_down_reason(Reason) 583 | end. 584 | 585 | 586 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 587 | %% 588 | %% API helpers 589 | %% 590 | %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 591 | 592 | 593 | do_collect(Futures) -> 594 | L = [ {F, attach(F)} || F <- Futures ], 595 | Res = [ detach(Attach, F) || {F, Attach} <- L ], 596 | %% [ F:done() || F <- Futures ], 597 | [ handle(R) || R <- Res ]. 598 | 599 | do_select_any(Futures) -> 600 | L = [ {F, Ref, Mon} || F <- Futures, 601 | {Ref, Mon} <- [attach(F)]], 602 | collector_any(L, []). 603 | 604 | collector_any([], _) -> 605 | erlang:error(all_futures_failed); 606 | collector_any(L, Failed) -> 607 | receive 608 | {future, Ref, Res} -> 609 | case Res of 610 | {value, _} -> 611 | {value, {F, _, _}, L2} = lists:keytake(Ref, 2, L), 612 | {F, Failed, L2}; 613 | {error, _} -> 614 | {value, {F, _, _}, L2} = lists:keytake(Ref, 2, L), 615 | collector_any(L2, [F|Failed]) 616 | end; 617 | {'DOWN', Mon, process, _Pid, _Reason} -> 618 | {value, {F, _, _}, L2} = lists:keytake(Mon, 3, L), 619 | collector_any(L2, [F|Failed]) 620 | end. 621 | 622 | do_get(#future{result = Res} = Self) when ?is_realized(Self) -> 623 | Res; 624 | do_get(#future{} = Self) -> 625 | Att = attach(Self), 626 | detach(Att, Self). 627 | 628 | -spec attach(future()) -> future_attachment(). 629 | attach(#future{proc = Proc, ref = Ref, result = undefined} = _Self) -> 630 | Mon = Proc:monitor(), 631 | Proc:send({get, Ref, self()}), 632 | {Ref, Mon}; 633 | attach(#future{ref = Ref, result = {_Type, _Value} = Result} = _Self) -> 634 | self() ! {future, Ref, Result}, 635 | {Ref, undefined}. 636 | 637 | -spec detach(future_attachment(), future()) -> future_res(). 638 | detach({Ref, Mon}, #future{ref = Ref, proc = Proc} = _Self) -> 639 | receive 640 | {future, Ref, Res} -> 641 | case Mon of 642 | undefined -> ok; 643 | _ -> Proc:demonitor(Mon) 644 | end, 645 | Res; 646 | {'DOWN', Mon, process, _Pid, Reason} -> 647 | Proc:demonitor(Mon), 648 | reraise_down_reason(Reason) 649 | end. 650 | 651 | handle({value, Value}) -> 652 | Value; 653 | handle({error, {_Class, _Error, _ErrorStacktrace} = E}) -> 654 | reraise(E). 655 | 656 | reraise({Class, Error, ErrorStacktrace}) -> 657 | {'EXIT', {get_stacktrace, CurrentStacktrace}} = (catch erlang:error(get_stacktrace)), 658 | erlang:raise(Class, Error, ErrorStacktrace ++ CurrentStacktrace). 659 | 660 | reraise_down_reason(Reason) -> 661 | case Reason of 662 | {{nocatch, Error}, Stack} -> 663 | reraise({throw, Error, Stack}); 664 | {Error,Stack} -> 665 | reraise({error, Error, Stack}); 666 | Error -> 667 | reraise({exit, Error, []}) 668 | end. 669 | 670 | chain0(C1, C2, Opts) when ?is_futurable(C1), is_function(C2, 1) -> 671 | F1 = new(C1), 672 | new0(fun() -> 673 | C2(F1:realize()) 674 | end, 675 | [{wraps, F1}] ++ Opts). 676 | 677 | retry_wrapper(_X, {C,E,S}, Max, Max) -> 678 | reraise({error, {retry_limit_reached, Max, {C,E}}, S}); 679 | retry_wrapper(X, _E, C, Max) -> 680 | retry_wrapper(X, C, Max). 681 | 682 | retry_wrapper(X, Count, Max) -> 683 | Res = do_get(X), %% handle dead future 684 | case Res of 685 | {value, R} -> 686 | %% X:done(), 687 | R; 688 | {error, {throw, Error, _}} -> %% throw is a flow control tool, not an retry-able error 689 | throw(Error); 690 | {error, {_, _, _} = E} -> 691 | Clone = X:clone(), 692 | %% X:done(), 693 | retry_wrapper(Clone, E, Count + 1, Max) 694 | end. 695 | -------------------------------------------------------------------------------- /src/future_tests.erl: -------------------------------------------------------------------------------- 1 | -module(future_tests). 2 | 3 | -include_lib("proper/include/proper.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -include_lib("erlfu/include/erlfu.hrl"). 6 | 7 | 8 | -define(QC(Arg), proper:quickcheck(Arg, [])). 9 | 10 | 11 | prop_basic() -> 12 | ?FORALL(X, term(), 13 | begin 14 | F = future:new(), 15 | F:set(X), 16 | Val = F:get(), 17 | F:done(), 18 | X == Val 19 | end). 20 | 21 | basic_test() -> 22 | {timeout, 100, fun() -> 23 | true = ?QC(future:prop_basic()) 24 | end}. 25 | 26 | get_test() -> 27 | erlfu:start(), 28 | F = future:new(), 29 | F:set(42), 30 | Val = F:get(), 31 | F:done(), 32 | ?assertEqual(42, Val). 33 | 34 | double_set_good_test() -> 35 | F = future:new(), 36 | F:set(42), 37 | F:set(42), 38 | Val = F:get(), 39 | F:done(), 40 | ?assertEqual(42, Val). 41 | 42 | double_set_bad_test() -> 43 | F = future:new(), 44 | F:set(42), 45 | ?assertException(error, badfuture, F:set(wontwork)). 46 | 47 | %% set_not_transparent_test() -> 48 | %% F = future:new(fun() -> 1 end), 49 | %% F2 = future:safe(F), 50 | %% F2Proc = F2:proc(), 51 | %% F2Proc:unlink(), 52 | %% F2Monitor = future:wait_for(F2Proc), 53 | %% timer:sleep(10), 54 | %% F2:set(2), 55 | %% ?assertMatch({notimplemented, _, _}, F2Monitor:get()), 56 | %% ?assertException(exit, noproc, F2:get()). 57 | 58 | realize_test() -> 59 | F = future:new(), 60 | F:set(42), 61 | F2 = F:realize(), 62 | ?assertEqual(42, F2:get()). 63 | 64 | worker_killed_test() -> 65 | Fun = fun() -> 66 | exit(self(), kill) 67 | end, 68 | F = future:new(Fun), 69 | ?assertException(exit, killed, F:get()). 70 | 71 | future_killed_test() -> 72 | Fun = fun() -> 73 | timer:sleep(100000) 74 | end, 75 | F = future:new(Fun), 76 | Proc = element(2, F), 77 | Pid = Proc:pid(), 78 | exit(Pid, kill), 79 | ?assertException(exit, killed, F:get()). 80 | 81 | clone_val_test() -> 82 | F = future:new(), 83 | F:set(42), 84 | F2 = F:clone(), 85 | F:done(), 86 | F3 = F2:realize(), 87 | ?assertEqual(42, F3:get()). 88 | 89 | clone_fun_test() -> 90 | F = future:new(fun() -> 91 | 43 92 | end), 93 | F2 = F:clone(), 94 | F:done(), 95 | F3 = F2:realize(), 96 | ?assertEqual(43, F3:get()). 97 | 98 | clone_deep_fun_test() -> 99 | F1 = future:new(fun() -> 100 | 1 101 | end), 102 | ?assertEqual(1, F1:get()), 103 | F1C = F1:clone(), 104 | ?assertEqual(1, F1C:get()), 105 | F2 = future:wrap(fun(X) -> 106 | X:get() + 2 107 | end, F1), 108 | ?assertEqual(3, F2:get()), 109 | F2C = F2:clone(), 110 | ?assertEqual(3, F2C:get()), 111 | F3 = future:wrap(fun(Y) -> 112 | Y:get() + 3 113 | end, F2), 114 | ?assertEqual(6, F3:get()), 115 | F3C = F3:clone(), 116 | ?assertEqual(6, F3C:get()), 117 | F4 = future:wrap(fun(Z) -> 118 | Z:get() + 4 119 | end, F3), 120 | ?assertEqual(10, F4:get()), 121 | F4C = F4:clone(), 122 | ?assertEqual(10, F4C:get()), 123 | ok. 124 | 125 | clone_clones_fun_test() -> 126 | F1 = future:new(fun() -> 127 | 1 128 | end), 129 | F1C = F1:clone(), 130 | F2 = future:wrap(fun(X) -> 131 | X:get() + 2 132 | end, F1C), 133 | F2C = F2:clone(), 134 | F3 = future:wrap(fun(Y) -> 135 | Y:get() + 3 136 | end, F2C), 137 | F3C = F3:clone(), 138 | ?assertEqual(6, F3C:get()). 139 | 140 | clone_combined_test() -> 141 | {messages, []} = process_info(self(), messages), 142 | Self = self(), 143 | F1 = future:new(fun() -> 144 | Self ! 1 145 | end), 146 | F2 = future:new(fun() -> 147 | Self ! 2 148 | end), 149 | F3 = future:combine([F1, F2]), 150 | ?assertEqual([1, 2], F3:get()), 151 | timer:sleep(100), 152 | %% we should have exactly two messages in the queue 153 | ?assertEqual({messages, [1,2]}, process_info(self(), messages)), 154 | F3C = F3:clone(), 155 | ?assertEqual([1, 2], F3C:get()), 156 | timer:sleep(100), 157 | %% we should have exactly six (three old plus three new) messages in the queue 158 | ?assertEqual({messages, [1,2,1,2]}, process_info(self(), messages)), 159 | flush(), 160 | ok. 161 | 162 | clone_side_effect_fun_test() -> 163 | Self = self(), 164 | F1 = future:new(fun() -> 165 | Self ! 1 166 | end), 167 | F2 = future:wrap(fun(X) -> 168 | X:get() + (Self ! 2) 169 | end, F1), 170 | F3 = future:wrap(fun(Y) -> 171 | Y:get() + (Self ! 3) 172 | end, F2), 173 | ?assertEqual(6, F3:get()), 174 | timer:sleep(100), 175 | %% we should have exactly three messages in the queue 176 | ?assertEqual({messages, [1,2,3]}, process_info(self(), messages)), 177 | F3C = F3:clone(), 178 | ?assertEqual(6, F3C:get()), 179 | timer:sleep(100), 180 | %% we should have exactly six (three old + three new) messages in the queue 181 | ?assertEqual({messages, [1,2,3,1,2,3]}, process_info(self(), messages)), 182 | flush(), 183 | ok. 184 | 185 | clone2_fun_test() -> 186 | Self = self(), 187 | F = future:new(fun() -> 188 | Self ! 1, 189 | 43 190 | end), 191 | F:get(), 192 | receive 193 | 1 -> ok 194 | after 200 -> 195 | error(timeout) 196 | end, 197 | F2 = F:clone(), 198 | F3 = F2:realize(), 199 | receive 200 | 1 -> ok 201 | after 200 -> 202 | error(timeout) 203 | end, 204 | ?assertEqual(43, F3:get()), 205 | flush(). 206 | 207 | clone_retry_test() -> 208 | Self = self(), 209 | F = future:new(fun() -> 210 | Self ! 2, 211 | error(test) 212 | end), 213 | F2 = future:retry(F, 3), 214 | F3 = F2:realize(), 215 | receive 2 -> ok after 200 -> error(timeout) end, 216 | receive 2 -> ok after 200 -> error(timeout) end, 217 | receive 2 -> ok after 200 -> error(timeout) end, 218 | ?assertException(error, {retry_limit_reached, 3, _}, F3:get()), 219 | flush(). 220 | 221 | cancel_test() -> 222 | Self = self(), 223 | F = future:new(fun() -> 224 | timer:sleep(100), 225 | exit(Self, kill), 226 | 42 227 | end), 228 | F:cancel(), 229 | timer:sleep(300), 230 | true. 231 | 232 | safe_ok_test() -> 233 | F = future:new(fun() -> 1 end), 234 | F2 = future:safe(F), 235 | F3 = F2:realize(), 236 | ?assertEqual({ok, 1}, F3:get()). 237 | 238 | safe_err_test() -> 239 | F = future:new(fun() -> error(1) end), 240 | F2 = future:safe(F), 241 | F3 = F2:realize(), 242 | ?assertEqual({error, 1}, F3:get()). 243 | 244 | timeout_test() -> 245 | F = future:new(fun() -> 246 | timer:sleep(1000), 247 | done 248 | end), 249 | F2 = future:timeout(F, 100), 250 | F3 = F2:realize(), 251 | ?assertException(throw, timeout, F3:get()). 252 | 253 | safe_timeout_test() -> 254 | F = future:new(fun() -> 255 | timer:sleep(1000), 256 | done 257 | end), 258 | F2 = future:safe(future:timeout(F, 100)), 259 | F3 = F2:realize(), 260 | ?assertException(throw, timeout, F3:get()). 261 | 262 | c4tcher_timeout_test() -> %% seems like 'catch' in the function name screwes up emacs erlang-mode indentation 263 | F = future:new(fun() -> 264 | timer:sleep(1000), 265 | done 266 | end), 267 | F2 = future:catcher(future:timeout(F, 100)), 268 | F3 = F2:realize(), 269 | ?assertEqual({error, throw, timeout}, F3:get()). 270 | 271 | retry_success_test() -> 272 | T = ets:new(retry_test, [public]), 273 | ets:insert(T, {counter, 4}), 274 | F = future:new(fun() -> 275 | Val = ets:update_counter(T, counter, -1), 276 | 0 = Val, 277 | Val 278 | end), 279 | F2 = future:retry(F, 5), 280 | F3 = F2:realize(), 281 | ?assertEqual(0, F3:get()). 282 | 283 | retry_fail_test() -> 284 | T = ets:new(retry_test, [public]), 285 | ets:insert(T, {counter, 4}), 286 | F = future:new(fun() -> 287 | Val = ets:update_counter(T, counter, -1), 288 | 0 = Val, 289 | Val 290 | end), 291 | F2 = future:retry(F, 3), 292 | F3 = F2:realize(), 293 | ?assertException(error, {retry_limit_reached, 3, _}, F3:get()). 294 | 295 | combine_test() -> 296 | T = ets:new(collect_test, [public]), 297 | ets:insert(T, {counter, 0}), 298 | Fun = fun() -> 299 | ets:update_counter(T, counter, 1) 300 | end, 301 | F = future:combine([future:new(Fun), 302 | future:new(Fun), 303 | future:new(Fun)]), 304 | Res = F:get(), 305 | ?assertEqual([1,2,3], lists:sort(Res)). 306 | 307 | select_static_errors_test() -> 308 | F = future:select_full([F1 = future:new(), 309 | F2 = future:new()]), 310 | F1:set_error(badarg), 311 | F2:set_error(badarg), 312 | ?assertException(error, 313 | all_futures_failed, 314 | F:get()). 315 | 316 | select_static_both_test() -> 317 | F = future:select_full([F1 = future:new(), 318 | F2 = future:new()]), 319 | F1:set(1), 320 | F2:set_error(badarg), 321 | SR = F:get(), 322 | ?assertMatch({_, [_], []}, SR). 323 | 324 | select_static_slow_test() -> 325 | F = future:select_full([ F1 = future:new(), 326 | _F2 = future:new()]), 327 | F1:set(1), 328 | SR = F:get(), 329 | ?assertMatch({_, [], [_]}, SR). 330 | 331 | select_mixed_test() -> 332 | T = ets:new(select_test, [public]), 333 | ets:insert(T, {counter, 0}), 334 | Fun = fun() -> 335 | I = ets:update_counter(T, counter, 1), 336 | timer:sleep(I * 100), 337 | I 338 | end, 339 | SR = future:select_full([future:new_static_error(badarg), 340 | future:new(Fun), 341 | future:new(Fun), 342 | future:new(Fun)]), 343 | ?assertMatch({_, [_], [_, _]}, SR:get()), 344 | {F, Errors, _Slow} = SR:get(), 345 | ?assertEqual(1, F:get()), 346 | [E] = Errors, 347 | ?assertException(error, badarg, E:get()). 348 | 349 | select_all_fail_test() -> 350 | Fun = fun() -> 351 | error(lets_fail) 352 | end, 353 | F = future:select_full([future:new(Fun), 354 | future:new(Fun), 355 | future:new(Fun)]), 356 | ?assertException(error, 357 | all_futures_failed, 358 | F:get()). 359 | 360 | select_all_down_test() -> 361 | Fun = fun() -> 362 | exit(self(), kill) 363 | end, 364 | All = [future:new(Fun), 365 | future:new(Fun), 366 | future:new(Fun)], 367 | F = future:select(All), 368 | ?assertException(error, 369 | all_futures_failed, 370 | F:get()). 371 | 372 | wait_for_test() -> 373 | Pid = spawn(fun() -> timer:sleep(100) end), 374 | F = future:wait_for(Pid), 375 | ?assertMatch(normal, F:get()). 376 | 377 | wait_for_fail_test() -> 378 | Pid = spawn(fun() -> timer:sleep(100), erlang:exit(badarg) end), 379 | F = future:wait_for(Pid), 380 | ?assertMatch(badarg, F:get()). 381 | 382 | on_success_test() -> 383 | Self = self(), 384 | F = future:new_static(1), 385 | F:on_success(fun(X) -> 386 | Self ! {done, X} 387 | end), 388 | receive 389 | {done, X} -> 390 | ?assertEqual(1, X) 391 | after 392 | 200 -> 393 | error(timeout) 394 | end. 395 | 396 | on_done_value_test() -> 397 | Self = self(), 398 | F = future:new_static(777), 399 | F:on_done(fun(X) -> 400 | Self ! {done, X} 401 | end), 402 | receive 403 | {done, X} -> 404 | ?assertEqual({ok, 777}, X) 405 | after 406 | 200 -> 407 | error(timeout) 408 | end. 409 | 410 | on_done_error_test() -> 411 | Self = self(), 412 | F = future:new_static_error(throw, interrupt), 413 | F:on_done(fun(X) -> 414 | Self ! {done, X} 415 | end), 416 | receive 417 | {done, X} -> 418 | ?assertEqual({error, throw, interrupt}, X) 419 | after 420 | 200 -> 421 | error(timeout) 422 | end. 423 | 424 | on_success_wait_for_test() -> 425 | Self = self(), 426 | Pid = spawn(fun() -> timer:sleep(100) end), 427 | F = future:wait_for(Pid), 428 | F:on_success(fun(X) -> 429 | Self ! {dead, X} 430 | end), 431 | receive 432 | {dead, X} -> 433 | ?assertEqual(normal, X) 434 | after 435 | 300 -> 436 | error(timeout) 437 | end. 438 | 439 | flush() -> 440 | receive 441 | _ -> flush() 442 | after 0 -> 443 | ok 444 | end. 445 | --------------------------------------------------------------------------------