ryan-senn

ryan-senn

Help with picking the right tools to perform a lot of concurrent http requests

Hello,

I’m building a somewhat unique tool and I’m looking for guidance on how to best architect it.
Basically I need to crawl 100 web pages, collect all links (a tags) and follow them to find out the final destination of the link (follow redirects, some of them use short URLs etc.) to check for the destination domain. Each page can have ~200 links, so I need to potentially follow 20k links.
I am using Elixir 1.10.2 and Phoenix 1.5.1.

Here is what I had in mind, please let me know if there is a better/more efficient way to achieve this.

  1. Use a LiveView module as a GenServer, this way I can show progress live in the browser
  2. Use Task.async to fetch the 100 pages concurrently
  3. Catch the responses with a handle_info function. Use Floki to parse and find all a tags (links). Use Enum.each on the links and make the http request in another Task.async for each of them. I would use Tesla with Hackney, as Tesla has a middle ware to follow redirects. I would do a head request, as I don’t need the body.
  4. Catch the responses with another handle_info function and check for the domain. If it matches the domain, update the socket to show the link, otherwise just return the unmodified socket

Here is what I had in mind code-wise (not implemented, just laying out the basic idea).

defmodule Example.PageLive do
  use Example, :live_view

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def handle_event("submit", _, socket) do
    # the initial 100 pages
    pages = ["http://example.com"]

    # Task.async each of them
    Enum.each(pages, fn page ->
      Task.async(fn ->
        # load the links
        links =
          page
          |> Tesla.get()
          |> Floki.parse_and_find_links()

        # send the response to handle_info
        {:page_loaded, links}
      end)
    end)

    # could show status
    {:noreply, socket}
  end

  def handle_info({_pid, {:page_loaded, links}}, socket) do
    # Task.async each of them
    Enum.each(links, fn link ->
      Task.async(fn ->
        # find destination with Tesla.head and the follow_redirects middleware
        destination = Tesla.head(link)

        # send the response to handle_info
        {:link_loaded, destination}
      end)
    end)

    # could show status
    {:noreply, socket}
  end

  def handle_info({_pid, {:link_loaded, destination}}, socket) do
    if URI.parse(destination).host == "example.com" do
      # do stuff if destination is the domain we're looking for
      {:noreply, socket}
    else
      # ignore otherwise
      {:noreply, socket}
    end
  end
end

Is this a sensible approach? Do you think that Tesla and Hackney are good choices? I don’t have much experience with GenServers and stuff like that. What sort of server would I need to be able to run all of this specs wise? Also I read a little bit about hackney pools, do I need to do anything? Also will using a LiveView for my GenServer cause any performance issues?

And last question: Is Elixir a good choice for doing something like this?

Thanks heaps in advance!

Most Liked

LostKobrakai

LostKobrakai

Your general approach seems fine. I’d switch out Task.async with something better at dealing with potential of overwhelming the system/network/the found endpoints… like GenStage. Also I’d be cautious of starting an exponentially growing numbers of Tasks concurrently without limit. The BEAM is great at handling the processes, but timeouts might spiral out of control. I’d rather look at one (or a set of) queues to schedule work onto.

If you find performance problems then I’d start looking into alternative http clients.

quatermain

quatermain

This is maybe ready-made solution for you Crawly or you can inspire from their solution.

Where Next?

Popular in Questions Top

chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
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
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
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
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

Other popular topics Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43657 311
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
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
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
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New

We're in Beta

About us Mission Statement