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?

LostKobrakai

LostKobrakai

By your descriptions I feel like you’re trying to solve everything at once. It might be worth trying to think about things more separately. E.g. spliting your business domain up into “querying external sources”, “processing data (of those sources)” and “detecting and handling problems”. E.g. the first part when using GenStage should only concern the producer. No consumer needs to be aware where data is coming from or how it was batched on retrieval. The second and third part would probably be more on the consumers side, where you need to decide how to handle the one failing item, which was produced, but also what should happen to other (following) items in case of errors.

Also I don’t really expect starting GenStages on demand (per source/trigger/collection) to be a problem.

dimitarvp

dimitarvp

If a part of remote collection retrieval fails, do you want to fail the entire collection gathering effort, or is it okay to partially process whatever has been accumulated so far?

Where Next?

Popular in Questions Top

Tee
can someone please explain to me how Enum.reduce works with maps
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
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
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
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
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
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
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New

We're in Beta

About us Mission Statement