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 objectfilename
. Then store this to newDocument
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 castpull_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.