harmon25

harmon25

Alternate :multipart plug parser - S3 storage instead of tmp/

Hi folks,

I was able to solve the problem of proxying + encrypting uploads to S3 via NodeJS in a memory efficient stream. This is easier in NodeJS as an express controller can receive the file contents as a stream and that stream can be transformed and piped up to S3 with predictable memory consumption regardless of file size. See busboy

I want the same behavior in Elixir - but this requires a custom multipart parser!

I have created a POC that instead of writing the uploaded byte chunks via Plug.Upload to tmp/ the file is uploaded to S3.
I started from the builtin Plug.Parsers.MULTIPART, and have modified to achieve the above. It is structurally very similar to the normal multipart parser.

If the file is <5 MB it is persisted with a simple s3.put_object, when >5MB the file is persisted via an s3 multipart upload.

One change I made to more easily handle the multipart s3 upload, is altering the read_length option from 1_000_000 bytes (~1MB) to 5_242_880 bytes (5MB). Are there potential negative side effects from doing this? (aside from obvious 5x memory consumption)

This was done because s3 multipart uploads must be 5MB chunks, so this allows chunks based on read_length and no extra chunking logic…

Anyway, here are some questions I have for the community:

Could an Elixir Stream be used here similar to how it is done in NodeJS?

  • I.E Plug.Upload actually returns a list of streams to files that are lazily parsed from the body
  • I was not sure how to create a stream from the uploaded chunks and not introduce some memory leak as I cannot control the upload pace
    • I need to buffer those bytes somewhere, right?

Is this a good use case for Broadway/GenStage?

  • Thinking something like this (p: producer, c: consumer):
  • Multipart Parser ( p ) → Hash?(transform1)(p/c) → Encrypt(transform2) (p/c) → S3Upload ( c )

Should more flexibility be introduced to the builtin parser to enable altering its behavior to allow this type of functionality.

Or

Should this be its own hex package, used as an alternative to the built-in multipart parser.

Thanks for taking to time to read this, I am very open to suggestions and ideas!

If anyone would like to help make this into a published hex package, lets colab!

Most Liked

shanesveller

shanesveller

This is perhaps not what you want to hear, but if the ultimate destination is S3, it’s generally considered a better practice to have the client send the content straight to S3. This is done using signed URLs, which is loosely supported but under-documented in ExAws, removing your server compute from the equation altogether. This approach does require that you’re willing to defer any post-processing that you would ordinarily do in-line during the upload, and to put in the work to make that behavior work asynchronously after the fact.

It’s not universally appropriate for all use cases and if uploads aren’t a core focus of the work in question it may not be worth it, since it can be significantly more engineering time than to just do it naively with the application server, especially if it’s the first time a dev/team is implementing this technique.

harmon25

harmon25

I have kind of given up on the stream idea, and instead just a more configurable multipart parser that accepts a module + behavior and some config to allow customizing what happens to the file as the body is being parsed.

I implemented both a Temp and S3 adapter.
Temp is nearly identical to the behavior of the built-in Plug.Upload.
And S3 implements some of the first POC to allow chunk uploading to S3 rather than writing to temp.

I designed it so adapters could implement some custom logic like encryption before writing the file..

It might not be as elegant as using streams would be, but seems to work pretty well!

harmon25

harmon25

Yea, I am trying to represent the file parsing as a stream but due to some contention on the conn, or probably that its being accessed in multiple places - this does not seem to work and the stream just hangs. Here is some code:

 Stream.resource(
  fn ->
    IO.inspect("Start Streaming")
    conn
  end,
  fn conn ->
    Plug.Conn.read_part_body(conn, opts)
    |> IO.inspect(label: "read this part!")
    |> case do
      {:ok, tail, conn} -> {tail, conn}
      {:more, tail, conn} -> {tail, conn}
      {:done, conn} -> {:halt, conn}
    end
  end,
  fn _conn ->
    IO.inspect("streaming completed!")
  end
)

The previous attempt going to object storage directly from the parser and blocking the conn worked OK - and behaves more like Plug.Upload, but as you mention N streams to N files inside a Plug.Upload like struct is pretty tricky, and is what I am going for, drawing parallels to nodejs body parsers like busboy

Going lower level may be the only option, but its still just a multipart form we are parsing here, and this would hopefully still work as a simple plug parser…

After looking more at Plug.Conn docs this stands out and is likely the issue i am having:

Because the request body can be of any size, reading the body will only work once, as Plug will not cache the result of these operations. If you need to access the body multiple times, it is your responsibility to store it. Finally keep in mind some plugs like Plug.Parsers may read the body, so the body may be unavailable after being accessed by such plugs.

Where Next?

Popular in Discussions Top

matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New
andre1sk
A big advantage to Elixir is all the distributed goodness but for many applications running on multiple nodes having integrated Etcd, Zoo...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New
AstonJ
If a newbie asked you about Phoenix Contexts, how would you explain the basics to them? Feel free to be as concise or in-depth as you li...
New
IVR
Hi all, I’ve seen a number of related threads in the past, but I’d still be very curious to hear an up-to-date opinion on this topic. I...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
jer
I’ve been using umbrellas for a while, and generally started off (on greenfield projects at least) by isolating subapps based on clearly ...
New
100phlecs
Are there any downsides, like perf issues, to putting all functional components in CoreComponents as long as you prefix it with the conte...
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36128 110
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
AstonJ
We’ve put together this wiki for Phoenix LiveView - please feel free to add any info you feel is worth including. What is Phoenix LiveV...
New
Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 126479 1222
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement