1
votes

I've seen several examples of Elixir GenServer but they are mostly dealing with Array of values (e.g. shopping cart) or counter increments. Therefore they demonstrate how to deal with simple data types.

I was wondering how can I pass state in a Phoenix application when I'm updating certain Model records.

The example I can provide is this:

  • step1: I receive AWS SNS notification (containing data what new s3 object was added) => just store the message to model Notification
  • step2: I parse the message inside Notification to read s3 object filename. Then store this to new Document model
  • step3: I fetch the metadata of the s3 object (e.g. original_name) and store it

Coming from Ruby on Rails I would do it as this:

  • Controller creates Notification then schedule background job (Sidekiq) for 2nd step
  • Background job creates Document and schedules another job to pull metadata PullDocumentMetadata.perform_later("Document", document.id)

example:

 class NotificationController
   def create
     # ...
     notification = Notification.create(body: message_body)
     ProcessNotification.perform_later("Notification", notification.id)
     # ...
   end
 end

 class ProcessNotification
   # ...
   def process(resource_class, resource_id)
     notification = resource_class.constantize.find(resource_id)
     document = Document.new(filename: parse_filename(notification.body))
     document.save
     PullMetadata.perform_later("Document", document.id)
   end
   # ...
 end

 class PullMetadata
   # ...
   def process(resource_class, resource_id)
     document = resource_class.constantize.find(resource_id)
     document.original_name = fetch_original_name(document.filename)
     document.save
   end
   # ...
 end

Now I was trying to replicate something similar with Phoenix using Genserver (step by step call)

The first step (create Notification is done by Phoenix controller and I want to isolate other two steps to 2 genserver calls:

defmodule NotificationController do
   # ...
   def create(conn, params) do
     notification = # ... store body to %{}Notification
     # ...
     pid = GenServer.start_link(ProcessNotification, {Notification, notification.id})
     GenServer.cast(pid, :process_to_document)
   end
end

defmodule ProcessNotification do
   def handle_cast(:process_to_document, {Notification, notification_id}) do
     notification = Repo.get(Notification, notification_id)
     filename = not_important_how_i_parse_body(notification)

     doc = %{}Document |> Document.changeset(%{filename: filename}) |> Repo.insert!

     {:noreply, {Document, document.id}}
   end

   def handle_cast(:pull_metadata, {Document, document_id}) do
     document = Repo.get(Document, document_id)
     original_name = not_important_how_i_pull_the_metadata(document)

     doc = %{}Document |> Document.changeset(%{original_name: original_name}) |> Repo.update!

     {:noreply, {Document, document.id}}
   end     
end

Now here are my questions:

  • I'm changing the state of Genserver (initially it was {Notification, id}, then it's {Document, id}. It feels to me like the Genserver is expecting maybe the same type all the time? So maybe should I always return the `{Notification, id} and pull Document from an association? Or is this ok as it is?
  • How well would Genserver hold the state of a Struct if I would init GenServer with `pid = GenServer.start_link(ProcessNotification, notification) ...so would it Marshal the object, or is this antimatter ?
  • How can I actually cast from a cast, so from process_to_document I would cast pull_metadata. Or should I schedule these in the controller like this:

example:

defmodule NotificationController do
   # ...
   def create(conn, params) do
     notification = # ... store body to %{}Notification
     # ...
     pid = GenServer.start_link(ProcessNotification, {Notification, notification.id})
     GenServer.cast(pid, :process_to_document)
     GenServer.cast(pid, :pull_metadata)

   end
end

I'm pretty sure what I'm doing is wrong, so I appreciate any idea how this should be better.

1

1 Answers

2
votes

Cast form a cast is simple.

GenServer.cast self(), {:another_event, some_data}

However, I'm not even sure why you would need to do that since you seem to be starting the gen server from your controller. I don't think that is the correct approach. All you seem to need here is to spawn a process to do all the work.

Here you could use Task module

Task.start fn -> 
  # do my heady lifting here
end

If you want to handle error handling and retries, then you could use a supervisor to start the task.

If your concerned about the number of processes you will create, then you would look at worker pools.

BTW, GenServer state is pretty simple. The initial state is what you return from init/1. Next state is what you return from the handle_cast/2 or handle_call/3

defmodule MyGenServer do
  use GenServer

  def start_link(args) do
    init_args = # do something with args
    GenServer.start_link(__MODULE__, init_args)
  end

  def init(init_args) do
    initial_state = # perhaps manipulate init_args
    {:ok, initial_state}
  end

  def handle_cast(event, current_state) do
    new_state = # manipulate current_state
    GenServer.cast self(), {:another_event, some_data}
    {:noreply, new_state}
  end

  def handle_call(event, sender, current_date) do
    new_state = # manipulate current_state
    {:reply, return_value, new_state}
  end
end

Just remember that each handle_xxx needs to run to completion before another handle_xxx can be called. So, you can't call GenServer.call to the same GenServer since that will deadlock the process.

But GenServer.cast is async so its not a problem.

The other option is to do a send self(), {:event, data} from inside the GenServer handlers. That will run the handle_info/2 handler on the GenServer.