fireproofsocks
Risks of serializing structs and captured function
Sorry for the long post, but in the process of building out a job queue using Oban, the desire to have messages include captured functions has come up. Certain variables are known at the time when the job message is created, so it is possible to do something like the following:
job_args = %{
captured_fn: fn -> MyModule.something(input, opts) end
|> :erlang.binary_to_term()
|> Base.encode64()
}
Serialization using :erlang.binary_to_term/2 and Base.encode64 is required so that Elixir functions/structs/atoms/etc may be safely inserted into the database as an Oban job because the args are JSON encoded.
I understand that if we were to require other languages to create jobs records in the database, they would have a hard time creating such a message (anyone feel like reverse-engineering :erlang.binary_to_term/2 in, say, Python?). Likewise, if any other system had to read job data out of the database when it was encoded in this way, it would be a similar pain.
However, the alternative to send args that identify the module, function, and function arguments isn’t much better:
job_args = %{
module: MyModule,
fun: :something,
fun_args: [input, opts]
}
The module name and the function atom convert to strings and can be converted back with a little tweaking, but the opts… those could be nearly anything… so … what to do?
The ability of having a worker that can run any function you throw at it is pretty tempting, and I think that flexibility may outweigh the cons of requiring Elixir to be on both ends of the pipeline.
If this wide-open flexibility of having carte-blanche captured functions is really an anti-pattern, then the only other way I can think to structure the worker is to have it have it operate on messages like this:
job_args = %{
type: "something",
input: input,
opts: opts
}
and then in the worker it could do something like:
case type do
"something" -> MyModule.something(input, opts)
end
i.e. the worker would need to know in advance what possibilities it should expect. In practice, there might only be a couple dozen.
However, even this approach would still fail the JSON encoding if the input were a struct or when the options were a keyword list.
I’m hoping someone can shed some light on this problem – maybe I’m not thinking about this the correct way. I understand that serializing certain things (pids or refs) is asking for trouble, but structs, modules, atoms, and functions seem pretty safe.
Thank you in advance for your thoughts!
Most Liked
josevalim
Not necessarily. My point is that a unique name is generated for each anonymous function and the process is opaque. Renaming a private function to give it a clearer name, changing lines, etc could all affect the name of the anonymous function. You have no control over it and you should assume that any change will give you something new.
Here is an example:
iex(18)> defmodule Foo do
...(18)> def fun, do: fn a, b -> a + b end
...(18)> end
iex(19)> fun = Foo.fun()
#Function<0.51000596/2 in Foo.fun/0>
iex(20)> defmodule Foo do
...(20)> def fun, do: fn a,
...(20)> b -> a +
...(20)> b
...(20)> end
...(20)> end
warning: redefining module Foo (current version defined in memory)
iex:20
iex(21)> Foo.fun() == fun
false
Same code, different line breaks, different functions.
josevalim
Just to make it clear beyond doubt, if you serialize an anonymous function and then you do a new deploy of the system were said anonymous function is slightly changed (for example, it is one line down), then you will no longer be able to execute it. So this approach is absolutely a no-go, even if using only Elixir. I would say it is in general an anti-pattern, encoding the message type is much better. ![]()
ityonemo
Just note when you unpickle a lambda, it generally speaking won’t work unless the unpickler has the module that the lambda came from.
It’s basically an mfa itself. So unpickling a lambda into another language is gonna be a doozy.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex









