[erlang-questions] Futures/promises and ability to "call" abstract modules

128 views
Skip to first unread message

Gleb Peregud

unread,
Nov 19, 2012, 5:32:58 AM11/19/12
to Erlang
Hello

Last evening I was trying to implement futures/promise mechanism in Erlang (mostly for fun, since I am still unsure if it is useful). I got inspired with the presentation [1], which mentioned using futures as a foundation of building services, where things like timeouts, tracing, authentication, etc. is built by composing futures (see slide 41).

Do you think that such composition of futures could be useful as a tool to improve code reuse of communication patterns in Erlang (as described in the presentation)?

I've implemented futures using processes and message passing and stumbled upon two issues:
1) garbage collection of futures
2) slightly too much code when using them

Example of the first problem is here:
1> F = future:new(fun() -> timer:sleep(10000), 10 end).
{future,<0.36.0>,#Ref<0.0.0.1736>,undefined}
2> F:get(). %% it hangs for 10 seconds
10
Since future F is represented as a process <0.36.0> it will stay running forever till it's timed out (which is not a good solution, since someone may still have a reference to this future) or F:done() manually called.

My idea is to insert into 'future' tuple a NIF-generated resource, which will have a destructor attached (called upon garbage collection of the resource) which will call F:done(). Will it work?

The second issue is illustrated here:
7> F = future:new().                                      
{future,<0.47.0>,#Ref<0.0.0.27235>,undefined}
8> spawn(fun() -> timer:sleep(10000), F:set(42) end).
<0.49.0>
9> F:get().
42
In ideal world it should be enough to just write "F" (without :get()) to fetch future's value, but it seems too far fetched for Erlang. Slightly better solution would be to allow calling future with "F()".

This can be done by extending concept of "abstract modules" with "default call". Currently abstract modules allow the following:

{future, Pid, Ref, undefined}:get() which is translated to future:get({future, Pid, Ref, undefined})

With a simple change in beam_emu.c in call_fun function (which would replace obsolete fun tuples) we can allow for the following:

{future, Pid, Ref, undefined}() which COULD be translated to future:call({future, Pid, Ref, undefined})

hence allowing to use just "F()" to read a value of the future. This will also extend "metaprogramming" capabilities of Erlang for some other quirky use, which may or may not be a Good Thing(tm).

Thoughts?

Cheers,
Gleb Peregud

1: http://monkey.org/~marius/talks/twittersystems/

Gleb Peregud

unread,
Nov 19, 2012, 5:40:26 AM11/19/12
to Erlang
Forgot to include a link to my implementation:

https://github.com/gleber/erlfu

Vlad Dumitrescu

unread,
Nov 19, 2012, 5:41:55 AM11/19/12
to Gleb Peregud, Erlang
Hi Gleb,

just a quick observation about garbage collecting futures: would the NIF-generated resource keep track of usage across processes? I fI send a future as a message, it may be referenced by multiple processes which have their own heap and garbage collection...

regards,
Vlad


On Mon, Nov 19, 2012 at 11:32 AM, Gleb Peregud <gleb...@gmail.com> wrote:
_______________________________________________
erlang-questions mailing list
erlang-q...@erlang.org
http://erlang.org/mailman/listinfo/erlang-questions


Gleb Peregud

unread,
Nov 19, 2012, 5:44:17 AM11/19/12
to Vlad Dumitrescu, Erlang
I assumed that NIF-generated resources are shared between processes (the same way as large binaries are), and I haven't done any tests on this. Are you sure it is garbate collected multiple times (once per referencing process)?

Vlad Dumitrescu

unread,
Nov 19, 2012, 5:46:09 AM11/19/12
to Gleb Peregud, Erlang
I have no idea, that's why I asked :-)
/Vlad

Gleb Peregud

unread,
Nov 19, 2012, 5:48:12 AM11/19/12
to Vlad Dumitrescu, Erlang
Sverker Eriksson wrote the following in [1]:

> But even if the resource terms look alike, they are unique and there is
> no bug leaking NIF resources (that I know of). A resource is released
> (and destructor called) when the last reference is garbage collected.
> The shell can fool you however, as it keeps a command history that can
> retain terms even though you think the variables are forgotten. Test NIF
> resource cleanup by running a test module and call
> erlang:garbage_collect to force destructors to be called.

This seems to mean that they are "shared" and garbage collected just once.

1: http://erlang.org/pipermail/erlang-questions/2011-January/055524.html

Gleb Peregud

unread,
Nov 19, 2012, 8:18:11 AM11/19/12
to Vlad Dumitrescu, Erlang
With the these changes [1] the following code works as intended:

7> F = future:new(fun() -> timer:sleep(5000), 42 end).
{future,<0.44.0>,#Ref<0.0.0.71>,undefined}
8> F().
42

Aside from a problem where it improperly reports arity in case of
undefined function:

9> F(1,2,3).
** exception error: undefined function future:call/3

1: https://github.com/gleber/otp/compare/call-abstract-module

Patrik Nyblom

unread,
Nov 19, 2012, 8:38:53 AM11/19/12
to erlang-q...@erlang.org
On 11/19/2012 11:48 AM, Gleb Peregud wrote:
> Sverker Eriksson wrote the following in [1]:
>
>> But even if the resource terms look alike, they are unique and there is
>> no bug leaking NIF resources (that I know of). A resource is released
>> (and destructor called) when the last reference is garbage collected.
>> The shell can fool you however, as it keeps a command history that can
>> retain terms even though you think the variables are forgotten. Test NIF
>> resource cleanup by running a test module and call
>> erlang:garbage_collect to force destructors to be called.
> This seems to mean that they are "shared" and garbage collected just once.
That is correct. The callback however, is not really able to send
messages, you would have to relay information to a thread which in turn
would have to send a message to the future to make it exit. It seems to
be technically possible at least, given an SMP VM. You would more or
less implement garbage collection of processes, which would be kind of
cool, I think :)

Of course distribution would generate a slightly bigger challenge...

Cheers,
/Patrik

Robert Virding

unread,
Nov 19, 2012, 9:01:32 AM11/19/12
to Gleb Peregud, Erlang
Wouldn't it be easier if future:new just returned a fun? Then you could do F() without any changes?

Robert

Gleb Peregud

unread,
Nov 19, 2012, 9:05:30 AM11/19/12
to Robert Virding, Erlang
Then I wouldn't be able to set future's value after it being defined.
Like in this example:

7> F = future:new().
{future,<0.47.0>,#Ref<0.0.0.27235>,undefined}
8> spawn(fun() -> timer:sleep(10000), F:set(42) end).
<0.49.0>
9> F:get().
42

Without this ability such futures are useless with request-reply pattern:
1) Client sends request
2) Server creates a future and sends it back to client
3) Client does it's work with the value of the future (without
bothering the fact that value may become available in uncertain
future)
4) Server finishes computations and sets future's value

Gleb Peregud

unread,
Nov 19, 2012, 2:06:26 PM11/19/12
to Patrik Nyblom, Erlang
Thanks for info :) Distributed version, if implemented nicely, would
probably need changes in ERTS (e.g. ability to send resources between
nodes and call some callbacks upon receive); otherwise users of
library would need to manually call something like F:register() upon
every receive...

Actually garbage collection of processes may be useful as a standalone
library - I had few situations when I wanted it to exist (but always
managed to do the job without it).

David Mercer

unread,
Nov 19, 2012, 3:58:37 PM11/19/12
to Gleb Peregud, Patrik Nyblom, Erlang
At a high level, how would you GC the processes? Just kill processes stuck at a receive (with no after) for which no other process on any connected node have a reference to the process? Or is there a more elegant/better way to do it?

Cheers,

DBM

> -----Original Message-----
> From: erlang-quest...@erlang.org [mailto:erlang-questions-
> bou...@erlang.org] On Behalf Of Gleb Peregud
> Sent: Monday, November 19, 2012 13:06
> To: Patrik Nyblom
> Cc: Erlang
> Subject: Re: [erlang-questions] Futures/promises and ability to "call"
> abstract modules
>

Gleb Peregud

unread,
Nov 19, 2012, 4:06:45 PM11/19/12
to David Mercer, Erlang
In this particular case of futures implementation, using NIF-based
resources, it would be enough to send a {done, Ref} to a process and
it'll handle it and cease any further looping (hence terminating
itself), which seems elegant enough. Killing is also an option, but
brutality is not necessary in my case :)

Of course distributed case, as mentioned, is a whole new story.

Garrett Smith

unread,
Nov 19, 2012, 4:40:52 PM11/19/12
to Gleb Peregud, Erlang
On Mon, Nov 19, 2012 at 4:32 AM, Gleb Peregud <gleb...@gmail.com> wrote:
> Hello
>
> Last evening I was trying to implement futures/promise mechanism in Erlang
> (mostly for fun, since I am still unsure if it is useful). I got inspired
> with the presentation [1], which mentioned using futures as a foundation of
> building services, where things like timeouts, tracing, authentication, etc.
> is built by composing futures (see slide 41).
>
> Do you think that such composition of futures could be useful as a tool to
> improve code reuse of communication patterns in Erlang (as described in the
> presentation)?

From the presentation, the only motivation for "futures" that I can see is this:

http://monkey.org/~marius/talks/twittersystems/#26

Here's what's on that slide, entitled "Futures":

* A placeholder for for a result that is, usually, being computed concurrently
-- long computations
-- network call
-- reading from disk
* Computations can fail:
-- connection failure
-- timeout
-- div by zero
* Futures are how we represent concurrent execution.

I don't see anywhere a problem here, unless you're unhappy with
threads and want to use some "async" library that requires the use of
callbacks, or this futures thing.

Of course that's why you'd use Erlang.

As it turns out, all of this is handled rather elegantly by Erlang's
concurrency model, which uses processes and message passing.

It might be an interesting exercise to figure out how GC works in
under a particular use case, but from the list above, it's unclear
what problems you're trying to solve in Erlang.

Garrett

Steve Davis

unread,
Nov 19, 2012, 7:44:05 PM11/19/12
to erlang-pr...@googlegroups.com, Erlang
On Monday, November 19, 2012 8:01:44 AM UTC-6, Robert Virding wrote:
Wouldn't it be easier if future:new just returned a fun? Then you could do F() without any changes?

Robert


+1 
Isn't this the solution? If not, I'm finding it hard to see *why* not!

Fred Hebert

unread,
Nov 19, 2012, 7:58:09 PM11/19/12
to Garrett Smith, Erlang
On 11/19, Garrett Smith wrote:
>
> From the presentation, the only motivation for "futures" that I can see is this:
>
> http://monkey.org/~marius/talks/twittersystems/#26
>
> Here's what's on that slide, entitled "Futures":
>
> * A placeholder for for a result that is, usually, being computed concurrently
> -- long computations
> -- network call
> -- reading from disk
> * Computations can fail:
> -- connection failure
> -- timeout
> -- div by zero
> * Futures are how we represent concurrent execution.
>

Based on these requirements, it sounds like a sequence of calls to the
`rpc` module would solve the problem:

5> MaxTime = rpc:async_call(node(), timer, sleep, [30000]).
<0.48.0>
6> lists:sort([a,c,b]).
[a,b,c]
7> rpc:yield(MaxTime).
... [long wait] ...
ok

or:

8> Key2 = rpc:async_call(node(), timer, sleep, [30000]).
<0.52.0>
9> rpc:nb_yield(Key2).
timeout
12> rpc:nb_yield(Key2, 1000).
timeout
13> rpc:nb_yield(Key2, 100000).
... [long wait] ...
{value,ok}

The only part missing is the caching of results for something more or
less read-only. If anything, wrapping around `rpc` calls doesn't sound
all bad for an implementation, especially given it supports multinode
calls and whatnot already.

Regards,
Fred.

Dmitry Belyaev

unread,
Nov 19, 2012, 10:48:23 PM11/19/12
to Gleb Peregud, Erlang
Erlang R15B01 (erts-5.9.1) [source] [64-bit] [smp:2:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.1 (abort with ^G)
1> F = (fun() -> FM = fun(M, V) -> receive {value, Value} -> M(M, Value); {get, P} -> P ! V, M(M, V) end end, P = spawn(fun() -> FM(FM, undefined) end), fun() -> P ! {get, self()}, receive V -> V end end end)().
#Fun<erl_eval.20.82930912>
2> F().
undefined
3> element(2,lists:keyfind('P', 1, lists:flatten(element(2,erlang:fun_info(F, env))))) ! {value, 10}.
{value,10}
4> F().
10
5>

--
Dmitry Belyaev

David Mercer

unread,
Nov 20, 2012, 9:34:51 AM11/20/12
to Dmitry Belyaev, Gleb Peregud, Erlang
How does that spawned process get garbage collected?

Cheers,

DBM


> -----Original Message-----
> From: erlang-quest...@erlang.org [mailto:erlang-questions-

Gleb Peregud

unread,
Nov 20, 2012, 10:20:18 AM11/20/12
to David Mercer, Erlang
This is yet to be implemented. The idea is to rely on already existing
implementation of NIF resources - this implies that a user of the
library would have to always store Pid of garbage collectable process
(let's call it gcproc) along with that resource in his heap, for
example as {'gcproc', pid(), resource()} and use special API to do
sending, killing, linking, monitoring, etc.

Since the resource associated with the process is always stored in
heap of the process, the resource will not get garbage collected until
no heap references it. This way we tie together process and resource
lifetime. And since NIF resources can have a destructor in C which is
called upon resource is garbage collected, we can make use of it to
inform gcproc to terminate.

As Patrik pointed out it is not as trivial as it sounds, but still possible.

Dmitry Belyaev

unread,
Nov 20, 2012, 10:32:16 AM11/20/12
to David Mercer, Erlang
This is not real implementation. I just showed that it is possible to use fun environment outside of it. That way no parameterized modules and new implicit call to them are required.
Of course garbage collection is the problem. Right now I don't have any good enough proposal to solve it.

--
Dmitry Belyaev

Joseph Wayne Norton

unread,
Nov 20, 2012, 10:32:41 AM11/20/12
to Gleb Peregud, Erlang
FYI.

https://github.com/tonyrog/resource

Please see this repository if you are not already aware of it.

regards,

Joe N.

On Nov 21, 2012, at 12:20 AM, Gleb Peregud <gleb...@gmail.com> wrote:

> Since the resource associated with the process is always stored in
> heap of the process, the resource will not get garbage collected until
> no heap references it. This way we tie together process and resource
> lifetime. And since NIF resources can have a destructor in C which is
> called upon resource is garbage collected, we can make use of it to
> inform gcproc to terminate.

Gleb Peregud

unread,
Nov 20, 2012, 10:35:36 AM11/20/12
to Joseph Wayne Norton, Erlang
That's exactly what I was thinking about! Thanks a bunch!

Gleb Peregud

unread,
Nov 20, 2012, 10:39:13 AM11/20/12
to Dmitry Belyaev, Erlang
Dmitry, this is an interesting idea, but feels a bit hacky :)
Unfortunately I don't think that (with this approach)
resource:notify_when_destroyed/2 will help to implement automatic
garbage collection.

In contrast with abstract modules, where it should be possible to
implement. Of course without new implicit call user would still have
to use "F:get()" to get future value.

Tony Rogvall

unread,
Nov 20, 2012, 11:16:18 AM11/20/12
to Gleb Peregud, Erlang
On 20 nov 2012, at 16:20, Gleb Peregud <gleb...@gmail.com> wrote:

This is yet to be implemented. The idea is to rely on already existing
implementation of NIF resources - this implies that a user of the
library would have to always store Pid of garbage collectable process
(let's call it gcproc) along with that resource in his heap, for
example as {'gcproc', pid(), resource()} and use special API to do
sending, killing, linking, monitoring, etc.

Since the resource associated with the process is always stored in
heap of the process, the resource will not get garbage collected until
no heap references it. This way we tie together process and resource
lifetime. And since NIF resources can have a destructor in C which is
called upon resource is garbage collected, we can make use of it to
inform gcproc to terminate.

As Patrik pointed out it is not as trivial as it sounds, but still possible.

Maybe git://github.com/tonyrog/resource.git could come in handy.

/Tony
"Installing applications can lead to corruption over time. Applications gradually write over each other's libraries, partial upgrades occur, user and system errors happen, and minute changes may be unnoticeable and difficult to fix"



Gleb Peregud

unread,
Nov 20, 2012, 11:55:41 AM11/20/12
to Tony Rogvall, Erlang
Yes, it is very handy :) Thanks!

Actually I've used it to create basic implementation of GC-able processes here:

https://github.com/gleber/gcproc

Gleb Peregud

unread,
Nov 20, 2012, 1:59:38 PM11/20/12
to Garrett Smith, Erlang
Garret, my initial motivation of futures can be actually found at
slide 41-44. It is the ability to compose/stack some generic behaviors
like retries, sharding, timeouts upon some basic operations without
knowing details of those operations.

I've implemented "retry", "timeout" and "safe" wrappers in erlfu to
see if it is feasible concept. Let's assume you have a DB server which
has a very unstable connection, but you want to make sure to get some
results from that server.

Here's example basic code for fetching results from that server:

fetch(Key, Host, Port) ->
{ok, S} = get_tcp:connect(Host, Port, [line]),
gen_tcp:send(S, ["GET ", Key, "\n\n"]),
{ok, Result} = gen_tcp:recv(S, 0),
list_to_integer(Result).

And you have some sort of server who manages connection to that server:

handle_call({fetch, Key}, #state{host = Host, port = Port} = State) ->
Result = fetch(Key, Host, Port),
{reply, Result, State}.

but since remote server is very unstable there's high probabilty that
gen_server:calls will timeout and the gen_server will block for a long
time. So you rewrite it like this (I know I could use noreply, spawn,
gen_server:reply combo for this, but bear with me):

handle_call({fetch, Key}, #state{host = Host, port = Port} = State) ->
F = future:new(fun() -> fetch(Key, Host, Port) end),
{reply, F, State}.

and make client run F:get() when it needs actual data.

This makes gen_server independent of the unstable remote DB server,
which makes things a bit better. But it's not enough, since connection
to DB server is very unstable, hence you want to add timeouts and
retries, and you can do it easily by rewriting that like this:

handle_call({fetch, Key}, #state{host = Host, port = Port} = State) ->
F = future:new(fun() -> fetch(Key, Host, Port) end),
F2 = future:timeout(F, 5000),
F3 = future:retry(F2, 5),
{reply, F, State}.

It's a two-lines change but it "automagically" makes these requests do
5 retries with 5s timeout each.

This example is a bit artificial, but gives some idea about possible
use of those "futures". Other uses which seems possible are generic
sharding behavior, stats gathering, tracing and logging. Which
essentially gives ability to easily reuse some communication-related
patters with very little code repetition.

P.S. http://github.com/gleber/erlfu now uses gcproc and resource
projects to make futures garbage collected (with a limitation that it
works only inside one node).

Gleb Peregud

unread,
Nov 21, 2012, 4:43:05 PM11/21/12
to Garrett Smith, Erlang
Now that example actually work. Previously retries were not working
due to future cloning worked only one level deep (it was a shallow).
I've improved it to do deep copying and now, along with automatic
garbage collection, it can be used in practice.

It was a great exercise :) Let me know if anyone *actually* use it ;)
Reply all
Reply to author
Forward
0 new messages