fireproofsocks

fireproofsocks

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!

First 8 of 8 Posts Switch mode

ityonemo

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.

dimitarvp

dimitarvp

IMO your last option makes the most sense. All your code will have to know all possible ways to process data coming from the job queue anyway so what’s the problem with matching 10-20 hardcoded strings against their corresponding module/function calls? Don’t get too academic. :slight_smile:

As for encoding / decoding data structures beyond what JSON can safely offer, have you entertained the idea of using FlatBuffers? The serialization there is version-aware and you can deprecate fields. FlatBuffers is one of the very few serialization formats I’ve known that can help you evolve your data structures mostly painlessly.

Using JSON is fine but there’s no point desperately holding on to it when it doesn’t seem that it can get the job done.

And finally, if you’re convinced you’re only going to have Elixir code process all the data and you are reasonably sure you won’t be changing those data structures often and you are sure you won’t be posting new versions while the job queue still has 50_000 messages with the old data to process… then indeed using :erlang.term_to_binary and :erlang.binary_to_term seems to be a no-brainer.

hassan

hassan

Nope, not a problem. Google ‘external term format implementations’ for multiple specific (Python, Rust) and general references.

HTH!

fireproofsocks

fireproofsocks OP

Wow. I had no idea this was a thing!

I had not heard of these either. Sounds like a useful tool!

Thanks for providing a sanity check! I think we are weighing the proper pros and cons here. It’s always possible to include a version number in the message somehow, so I think we’d have options available if our “interfaces” (i.e. behaviour implementations) changed between versions, and the use-cases right now are such that deleting old messages and re-running jobs is not a deal-breaker.

josevalim

josevalim

Creator of Elixir

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. :slight_smile:

fireproofsocks

fireproofsocks OP

Could you say more about this? Is the capture pointing to a line number somehow?

josevalim

josevalim

Creator of Elixir

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.

fireproofsocks

fireproofsocks OP

Thank you for the detailed explanation!

And thank you for creating Elixir – thanks for making developing fun again.

— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement