david_ex

david_ex

Software design: processing remote collection of items via paginated API, while properly handling errors

What I’m struggling with

I want to process a collection of items that are remote. These items aren’t all available in a single request: to get all items, multiple requests will have to be made (e.g. via a paginated api). Further, there are several of these collections I’d like to retrieve and process, and they have varying sizes. The collections of items would be retrieved from various sources, such as paginated APIs or scraping. Although each collection of items would be small enough to fit in memory at any one time, I’d ideally like to avoid this in order to prevent issues down the road.

Conceptually, I’d like to stream the collection of items and process them one at a time (where by “processing” I currently mean persisting them to the DB, although some response may be deserialized beforehand).

Approaches I’ve considered

Stream.resource/3

It would be really nice to stream the collections via Stream.resource/3 (example here), but I don’t see how to nicely handle the non-happy path:

  • network errors: having retries is obviously trivial, but in case the server is unresponsive (e.g. circuit breaker is open), the error needs to be reported to the caller. I considered implementing this via throw/raise, but that means that every time I want to iterate over a collection, the code must be surrounded by try/catch/rescue, which doesn’t seem very ergonomic.

  • politeness: whether scraping or interacting with an API, I can’t just slam the remote server with requests as fast as I can process the data. Delays between requests must be inserted, etc. Here also, while it would be possible to do that within Stream.resource/3 I’m afraid it would make the code a little messy and less maintainable.

GenServer, preload all items

I could implement a GenServer that would fetch all items (handling network issues, politeness, etc.) and either return an error (e.g. the remote server is unresponsive) or the full/partial collection of items for processing.

Although the collection of items will fit in memory, I can’t guarantee that as I have no control over the number of items in a collection. Therefore, I’d rather be able to process the batches of items as they arrive to avoid having to worry about memory constraints, especially if running several collection processing processes in parallel.

GenServer, continuation-passing style

I don’t really have any experience with CPS (a better illustration in my opinion is in Fred’s Erlang book, search for " Continuation-Passing Style").

Basically, I could have a function in the GenServer that would return:

  • a batch of items and a “next” information (i.e. information on getting the next batch of items, which the GenServer would understand: it could be a url, limit/offset combo, etc.) in the happy path case. After processing the batch of items, the caller would again call the GenServer passing it the “next” value to continue where it left off. If no “next” information is returned, the caller would know it’s arrived at the end of the collection and there are no more items to fetch.
  • an error tuple if something happened (circuit breaker tripped, etc.)

I would probably implement this in an async fashion where the batch request returns immediately with a reference, and the actual items in the batch are sent later to the caller along with the reference. I.e. the call would get and process the items within a receive block.

It feels like this may be the best alternative, but historically all of my dumb ideas started out looking like good ones ¯\_(ツ)_/¯ so I’d rather have opinions from more experience OTP wranglers before setting down the wrong path.

Opinions?

I’m certain this has all been done before in OTP systems, but I couldn’t find any info on the various approaches, pros/cons etc. If anybody can point me in the direction of blog posts, source code, theory, etc. that would be great.

And of course, please criticize the above approaches and suggest any others you think may be more suitable.

Most Liked

gregvaughn

gregvaughn

Great timing! Just today I deployed an upgrade to our system like this to better report errors. I’ve got a module named Unpager that does a Stream.unfold around our home-grown api client abstraction. The Unpager grabs the first page of results and creates a Task to pull down the subsequent page. After exhausting the first page, it awaits the Task and starts one for the next subsequent page. Our api client code uses outgoing throttling to ensure we meet the third party’s requirements – it’s not a responsibility of the Unpager.

Originally it logged when some page request had an error and ended the stream cleanly. Unfortunately the logged info lacked context and we tended to have failures we didn’t notice. Today’s change made it return one more item, the error tuple, when an error occurs. This allows the client call sites to decide whether to pattern match on the error tuple or not for maximum flexibility.

mbuhot

mbuhot

Seems like it might be a good fit for GenStage.

A Producer stage can fetch from the external APIs, buffering results in a DB table.

Consumers can then send demand for items, which will be served from the DB until the buffer is drained and more items are fetched from the API.

david_ex

david_ex

I’ve toyed with GenStage (more specifically, Flow) and while it’s a great piece of software (which I plan to use in another part of my project), I think it’s not a great fit here because:

  • the collections will typically have fewer than 2000 items, so they should be fetched relatively quickly. This means that the GenStage needs to request more work from some manager process (assuming I use 1 GenStage per source), or I have to handle stopping the GenStage when the collection has been retrieved, and starting a new producer stage (and changing the consumer’s subscription to use the new producer).

  • I haven’t understood how to handle/surface errors in the pipepline without bringing the whole thing down. Granted, this is probably more of an issue of my knowledge/skills, but when experimenting with Flow I was under the impressing that returning an error from the producer would bring the whole pipleine down without letting the items in transit get drained through the pipeline.

The idea of having a consumer to process all collection items that subscribes to multiple producer stages is really nice. But given the nature of the problem (many smallish, paginated collections), I’m somewhat concerned about the overhead of managing the pipeline (i.e. “updating” the pipeline as collections are done being retrieved).

As far as I can tell, GenStage/Flow/Broadway would fit better when you have a large collection of items to process (e.g. clean all records in a DB) where you set up the pipeline and when it terminates successfully the work is done.

Am I misguided? Are there example out there on how to manage GenStage/Flow/Broadway when wanting to process many collections that are on the small side?

Where Next?

Popular in Questions 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
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
dotdotdotPaul
Okay, I’m having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I’m sure I’...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
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
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

We're in Beta

About us Mission Statement