36
I Use This!
Inactive

News

Analyzed about 10 hours ago. based on code collected about 16 hours ago.
Posted about 8 years ago
I was asked a great question on Slack the other day - I wish I could remember the person's name (sorry!) but I can't find it ... anyway they asked me (paraphrased): I see you're creating a new process for each session by generating a unique key. ... [More] Given that process names need to be atoms, isn't this a problem? Won't you fill up the atom table and cause your VM to crash? Yep. This needs to change. There are a finite number of atoms that you can use in your Elixir code, which stems from how Erlang handles these things. An atom is like a symbol in Ruby or Smalltalk: its name is its value. They are used as labels and, as such, aren't garbage collected in the same way as other code in your system. It's a weird Achilles heel, but it exists nonetheless: the Erlang VM can only support 1,048,576 atoms. This number can change if you need it to, but it goes against some general guidance which I knew about but thought I could avoid (more below): do not arbitrarily generate atoms. Erlang Limits In addition to the limit on the atom table, there is also a limit on the number of current alive (running) processes: 32,768. You can (just like atoms) change this number if you want to. For now I'll just change the way I start up my Shopping process to avoid the atom problem. I still need the key, but I'll remove the setting of the name in GenServer.start_link/3: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args) #remove the name: key bit end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] #... end The atom problem has now been dealt with - and it was no small problem. Atoms don't go away once they've been added to the VM so after a period of time (after a million or so people drop by) - my VM would have simply crashed. This could have happened in a month (I should be so lucky!) or after six years. Or it might never have happened. My little session isn't the only thing creating atoms. Libraries and frameworks create them too. An interesting problem to have and one I should have avoided to start with... but that's how we learn things isn't it! Ahh failure... But why did I do this in the first place? A Session Per User My idea was that each customer would have some kind of cookie - a way of tracking them as they came to the store to shop for things - sort of like a "Shopping Cart Key" if you will. I thought having parity between the cookie and the session process as well as the session in the database would make good sense. I still like the idea - but now I'm seeing that I need to pay more attention to how the session process will end and I need to do that now. I would be very lucky to have 100 concurrent sessions running at any given time - meaning 100 active shoppers. But that raises some questions: what makes them inactive? can I reactivate the session once it becomes inactive? what do I do with the old, inactivated sessions after a long period of time? A lot of this is business logic which I should probably figure out right now - so I will - and I'll tackle it next time. [Less]
Posted about 8 years ago
I was asked a great question on Slack the other day - I wish I could remember the person’s name (sorry!) but I can’t find it … anyway they asked me (paraphrased): I see you’re creating a new process for each session by generating a unique key. ... [More] Given that process names need to be atoms, isn’t this a problem? Won’t you fill up the atom table and cause your VM to crash? Yep. This needs to change. There are a finite number of atoms that you can use in your Elixir code, which stems from how Erlang handles these things. An atom is like a symbol in Ruby or Smalltalk: its name is its value. They are used as labels and, as such, aren’t garbage collected in the same way as other code in your system. It’s a weird Achilles heel, but it exists nonetheless: the Erlang VM can only support 1,048,576 atoms. This number can change if you need it to, but it goes against some general guidance which I knew about but thought I could avoid (more below): do not arbitrarily generate atoms. Erlang Limits In addition to the limit on the atom table, there is also a limit on the number of current alive (running) processes: 32,768. You can (just like atoms) change this number if you want to. For now I’ll just change the way I start up my Shopping process to avoid the atom problem. I still need the key, but I’ll remove the setting of the name in GenServer.start_link/3: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args) #remove the name: key bit end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] #... end The atom problem has now been dealt with - and it was no small problem. Atoms don’t go away once they’ve been added to the VM so after a period of time (after a million or so people drop by) - my VM would have simply crashed. This could have happened in a month (I should be so lucky!) or after six years. Or it might never have happened. My little session isn’t the only thing creating atoms. Libraries and frameworks create them too. An interesting problem to have and one I should have avoided to start with… but that’s how we learn things isn’t it! Ahh failure… But why did I do this in the first place? A Session Per User My idea was that each customer would have some kind of cookie - a way of tracking them as they came to the store to shop for things - sort of like a “Shopping Cart Key” if you will. I thought having parity between the cookie and the session process as well as the session in the database would make good sense. I still like the idea - but now I’m seeing that I need to pay more attention to how the session process will end and I need to do that now. I would be very lucky to have 100 concurrent sessions running at any given time - meaning 100 active shoppers. But that raises some questions: what makes them inactive? can I reactivate the session once it becomes inactive? what do I do with the old, inactivated sessions after a long period of time? A lot of this is business logic which I should probably figure out right now - so I will - and I’ll tackle it next time. Want to learn Elixir? Learn how to build fast, fault-tolerant applications with Elixir. This is not a traditional, boring tutorial. You'll get an ebook (epub or mobi) as well as 3 hours worth of tightly-edited, lovingly produced Elixir content. You'll learn Elixir while doing Elixir, helping me out at my new fictional job as development lead at Red:4 Aerospace. [Less]
Posted about 8 years ago
I was asked a great question on Slack the other day - I wish I could remember the person's name (sorry!) but I can't find it ... anyway they asked me (paraphrased): I see you're creating a new process for each session by generating a unique key. ... [More] Given that process names need to be atoms, isn't this a problem? Won't you fill up the atom table and cause your VM to crash? Yep. This needs to change. There are a finite number of atoms that you can use in your Elixir code, which stems from how Erlang handles these things. An atom is like a symbol in Ruby or Smalltalk: its name is its value. They are used as labels and, as such, aren't garbage collected in the same way as other code in your system. It's a weird Achilles heel, but it exists nonetheless: the Erlang VM can only support 1,048,576 atoms. This number can change if you need it to, but it goes against some general guidance which I knew about but thought I could avoid (more below): do not arbitrarily generate atoms. Erlang Limits In addition to the limit on the atom table, there is also a limit on the number of current alive (running) processes: 32,768. You can (just like atoms) change this number if you want to. For now I'll just change the way I start up my Shopping process to avoid the atom problem. I still need the key, but I'll remove the setting of the name in GenServer.start_link/3: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args) #remove the name: key bit end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] #... end The atom problem has now been dealt with - and it was no small problem. Atoms don't go away once they've been added to the VM so after a period of time (after a million or so people drop by) - my VM would have simply crashed. This could have happened in a month (I should be so lucky!) or after six years. Or it might never have happened. My little session isn't the only thing creating atoms. Libraries and frameworks create them too. An interesting problem to have and one I should have avoided to start with... but that's how we learn things isn't it! Ahh failure... But why did I do this in the first place? A Session Per User My idea was that each customer would have some kind of cookie - a way of tracking them as they came to the store to shop for things - sort of like a "Shopping Cart Key" if you will. I thought having parity between the cookie and the session process as well as the session in the database would make good sense. I still like the idea - but now I'm seeing that I need to pay more attention to how the session process will end and I need to do that now. I would be very lucky to have 100 concurrent sessions running at any given time - meaning 100 active shoppers. But that raises some questions: what makes them inactive? can I reactivate the session once it becomes inactive? what do I do with the old, inactivated sessions after a long period of time? A lot of this is business logic which I should probably figure out right now - so I will - and I'll tackle it next time. [Less]
Posted about 8 years ago
I was asked a great question on Slack the other day - I wish I could remember the person's name (sorry!) but I can't find it ... anyway they asked me (paraphrased): I see you're creating a new process for each session by generating a unique key. ... [More] Given that process names need to be atoms, isn't this a problem? Won't you fill up the atom table and cause your VM to crash? Yep. This needs to change. There are a finite number of atoms that you can use in your Elixir code, which stems from how Erlang handles these things. An atom is like a symbol in Ruby or Smalltalk: its name is its value. They are used as labels and, as such, aren't garbage collected in the same way as other code in your system. It's a weird Achilles heel, but it exists nonetheless: the Erlang VM can only support 1,048,576 atoms. This number can change if you need it to, but it goes against some general guidance which I knew about but thought I could avoid (more below): do not arbitrarily generate atoms. Erlang Limits In addition to the limit on the atom table, there is also a limit on the number of current alive (running) processes: 32,768. You can (just like atoms) change this number if you want to. For now I'll just change the way I start up my Shopping process to avoid the atom problem. I still need the key, but I'll remove the setting of the name in GenServer.start_link/3: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args) #remove the name: key bit end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] #... end The atom problem has now been dealt with - and it was no small problem. Atoms don't go away once they've been added to the VM so after a period of time (after a million or so people drop by) - my VM would have simply crashed. This could have happened in a month (I should be so lucky!) or after six years. Or it might never have happened. My little session isn't the only thing creating atoms. Libraries and frameworks create them too. An interesting problem to have and one I should have avoided to start with... but that's how we learn things isn't it! Ahh failure... But why did I do this in the first place? A Session Per User My idea was that each customer would have some kind of cookie - a way of tracking them as they came to the store to shop for things - sort of like a "Shopping Cart Key" if you will. I thought having parity between the cookie and the session process as well as the session in the database would make good sense. I still like the idea - but now I'm seeing that I need to pay more attention to how the session process will end and I need to do that now. I would be very lucky to have 100 concurrent sessions running at any given time - meaning 100 active shoppers. But that raises some questions: what makes them inactive? can I reactivate the session once it becomes inactive? what do I do with the old, inactivated sessions after a long period of time? A lot of this is business logic which I should probably figure out right now - so I will - and I'll tackle it next time. Want to learn Elixir? Learn how to build fast, fault-tolerant applications with Elixir. This is not a traditional, boring tutorial. You'll get an ebook (epub or mobi) as well as 3 hours worth of tightly-edited, lovingly produced Elixir content. You'll learn Elixir while doing Elixir, helping me out at my new fictional job as development lead at Red:4 Aerospace. [Less]
Posted about 8 years ago by Rob Conery
I've run into my first problem, and it's a big one: using Atoms inappropriately.
Posted about 8 years ago
Let’s implement an intelligent shopping cart - something that tracks what the customer is doing, how they came to our store, etc. I tend to think of these things in terms of a “Session” - a shopping process where a customer selects things, puts them ... [More] back, and eventually (hopefully) buys something. If I do things correctly (to me, at least), I should end up with tight little functions an exactly 0 if statements. Task 1: Proper Initialization For review - we have a struct defined that we can use to hold items, logs, discounts and more. Each customer coming to our store will have a dedicated process which is supervised by the VM (/apps/shopping/lib/shopping/session.ex): defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] end Working with GenServers takes a little time to get used to. What we’re doing is actually wrapping GenServer functionality in a standardized way. With the code above we’ve implemented the first method: start_link. This wraps GenServer.start_link/3. Now we need to open up a public API which will accept calls like add_item, remove_item etc. One problem we have, however, is that functional programming doesn’t have the concept of “state”, necessarily. It’s all about immutability and not changing things - so how the hell are we going to track the items in our cart! That’s where GenServers come in. They don’t really have state - they have accumulators. The best way to explain this is to just show you what I mean. When you call start_link you pass in the initial “state” in the second position. This can be any data you want. In our case we’re matching on %{key: key} = args which means we expect the key named “key”, which represents a session key, to be passed in as our argument. The args variable can contain anything else, but at the very least it needs to have a key. GenServer will now set the args as our initial state. But we have a problem - these args can literally be anything and we don’t want that. We also don’t want to bleed out our struct to calling code (forcing the calling code to initialize only our struct) - let’ make this a little friendlier and a bit tighter using the init callback: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end def init(%{key: key} = args) do struct(%Redfour.Shopping.Session{}, args) end #... end In the code above init/1 will be called automatically by GenServer right after start_link/3 and the initial arguments will be passed to it. The result of init/1 will then be the thing stored in “state”. All we’re doing here is creating the Session struct and passing it back. Why did we do this? Because: We want to be sure we’re working with our struct as our state so we can add items, logs, etc We’re allowing a map to be passed in instead of the struct, making our API a little easier to use. We don’t have to do this - this is just my preference. Now, because I’m completely anal, let’s tighten this up: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args), do: GenServer.start_link(__MODULE__,args, name: key) def init(%{key: key} = args), do: struct(%Redfour.Shopping.Session{}, args) #... end You don’t have to do it like this - I just kind of dig one-liners as they’re a bit easier to read. Now let’s add something to the cart. Task 2: Adding, Removing, Updating Calling code shouldn’t need to know it’s working with a GenServer, it makes things a bit brittle and breaks an otherwise lovely encapsulation. So let’s consider our public API: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits # privates end This is just the start, and if you’re new to GenServers this will look strange. Basically we’re just wrapping the functionality of GenServer here in a nicer API. For the method calls themselves, we’re demanding a pid and then whatever arguments we need. I like to use Keyword Lists to identify arguments explicitly. This helps with matching. When appropriate I’ll use a struct (and it’s appropriate here) - I’ll get to that in a minute. The most important part is that we’re telling GenServer to issue a call/2 to a process identified by our pid. GenServer will do just that, passing the argument tuple along. But then what happens? This is where OO people might cringe. We’re going to “dual purpose” our module here to not only issue the call, but to also pick it up. Which makes sense because this module is also our process - so basically it’s calling itself. Why the ceremony? Simple answer: we need state: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits def handle_call({:select_item, item}, _sender, session) do end def handle_call({:remove_item, sku: sku}, _sender, session) do end def handle_call({:change_item, sku: sku}, _sender, session) do end # privates end With the addition of these three handle_call/3 methods, we can now execute calls to our process. Notice the three arguments - the first is the identifier and the data wrapped in a tuple (this is a standard Erlang/Elixir way of passing data around so you can match on it). The second identifies who’s calling, which we’re ignoring by preceding the variable with an _. Finally we have our session - which is our state. Let’s do the simplest implementation: def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} {:reply, session, session} end This code is extremely simple. We’re appending the passed in item to the items list on our struct (see above) and passing back a new session. Finally, we’re telling GenServer that things are lovely with {:reply, session, session}. Let’s talk about that response. If things go wrong we can pass back something else, like: :noreply, session - basically something happened, all went well, here’s the new state :stop, reason, [reply], state - something bad happened, stop the process with a reason These are very useful responses for certain scenarios. For instance if an item comes in with a negative price, it might be a good idea to kill the session and send the customer packing fearing some type of hack - you could use :stop for that if you like. For what we’re doing, we’re replying with the new session and then resetting the new state of our process using that same session. This is the basic structure of our process - now we just need to wire things up. Task 3: Data Access Our Shopping.SessionSupervisor will restart our session if it :exits unceremoniously. We could create a “stash worker” (see Programming Elixir from PragProg, page 214) and have our process seamlessly restarted, or we could be a little safer and use a persistent data store. Note: I originally stated that Supervisor’s will restart a service with the last known state. This is incorrect - thanks to Graham Kay for correcting the mistake! I’ll sidestep discussions on what’s the most appropriate, but let’s quickly have a look at our options: A standard database like Postgres. For this we could use Ecto or something a little lighter weight, such as Moebius. A document system, such as Erlang’s built-in Mnesia using the Amnesia library. This is a great option for getting things off the ground, but not so good moving forward. The simple reason is that querying is a bit painful but also - it’s not partition tolerant. Meaning if one node goes down, writes can’t happen. It is ACID compliant, however… We could also use RethinkDB using Peter Hamilton’s rethinkdb-elixir library. I’ve used it, I really like it! We’re not allowed to use MongoDB here at Red:4, but you’re welcome to have a look at that solution. No persistence at all. This is a bonkers approach. It means that if you restart your server, your customers lose their sessions. When you do the math on this it’s actually a very, very small inconvenience to a select few people. Erlang tends to run for long stretches and if you plan your restarts accordingly - well it might not be so bad. Also - losing cart data might be acceptable to you. For me, I’m choosing Postgres and Moebius. I like the ease of use (it’s why I wrote it) and if I didn’t choose it… well I’d probably be in trouble. If you want to play along, I’ll be using the dbworker branch which should be 2.0 in the future. The first thing I need to do is install Moebius in apps/shopping/mix.exs and set it as an application: def application do [applications: [:logger, :moebius], mod: {Redfour.Shopping, []}] end defp deps do [ {:moebius, github: "robconery/moebius", branch: "dbworker"}, ] end Next, I need to setup a database module that will become a worker which will be supervised. I’ll add /lib/shopping/db.ex: defmodule Redfour.Shopping.Db do use Moebius.Database import Moebius.DocumentQuery alias Redfour.Shopping.Db #session stuff def find_or_create_session(%{key: key, domain: domain} = args) do case db(:sessions) |> contains(key: key) |> Db.first do nil -> db(:sessions) |> Db.save(struct(%Redfour.Shopping.Session{}, args)) found -> found end end def save_session(session) do db(:sessions) |> Db.save(session) end end Moebius allows you to work with Postgres as a document store - which is a great way to get yourself off the ground. Here I simply need to use Moebius.Database which gives us some macros to play with, including run, first, save. Which is all I need for our Session. With the first method I’m finding or creating (using Postgres 9.5 I could use upsert but I’m using 9.4), with the second I’m just pushing the entire session into the database as a document (Moebius wraps this up for me). OK, so I have a module that “declares a database” if you will. Why did I do all of this? Because my new database module is a GenServer and I’ll want to be sure it’s supervised within the scope of my app. For that, let’s add some code to our start routine: defmodule Redfour.Shopping do use Application import Supervisor.Spec, warn: false #the entry point to start our app def start(_type, _args) do #supervision goes here start_session_supervisor start_database end def start_database do #start the supervised DB db_worker = worker(Redfour.Shopping.Db, [database: "redfour"]) Supervisor.start_link [db_worker], strategy: :one_for_one end #... end We’re loading up a worker, passing in a reference to our database. This is a significant change to what Moebius used to do (you would set the connection info in config) - by changing like this we can now have multiple database connections formalized and supervised. Yeeha. Now that I have this in place, let’s make our Session data aware: defmodule Redfour.Shopping.Session do use GenServer alias Redfour.Shopping.Db defstruct [ store_id: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [%{entry: "Session Created"}], discounts: [] ] def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__, args, name: key) end def init(%{key: key, domain: domain} = args) do session = Db.find_or_create_session(args) {:ok, session} end #GenServer callbacks def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} |> save_session {:reply, session, session} end #... # privates def save_session(%Redfour.Shopping.Session{} = session, log: log) do %{session | logs: List.insert_at(session.logs, -1, %{entry: log})} |> Db.save_session end end This is a good start, but it’s far from a final solution. Things are still a bit too lose. Summary We need to put some guards in place for when things don’t go well, and I also need to plug in better date management. Right now you can pass whatever you want to select_item which isn’t good… so we’ll fix that. Next time! Want to learn Elixir? Learn how to build fast, fault-tolerant applications with Elixir. This is not a traditional, boring tutorial. You'll get an ebook (epub or mobi) as well as 3 hours worth of tightly-edited, lovingly produced Elixir content. You'll learn Elixir while doing Elixir, helping me out at my new fictional job as development lead at Red:4 Aerospace. [Less]
Posted about 8 years ago
Let's implement an intelligent shopping cart - something that tracks what the customer is doing, how they came to our store, etc. I tend to think of these things in terms of a "Session" - a shopping process where a customer selects things, puts them ... [More] back, and eventually (hopefully) buys something. If I do things correctly (to me, at least), I should end up with tight little functions an exactly 0 if statements. Task 1: Proper Initialization For review - we have a struct defined that we can use to hold items, logs, discounts and more. Each customer coming to our store will have a dedicated process which is supervised by the VM (/apps/shopping/lib/shopping/session.ex): defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] end Working with GenServers takes a little time to get used to. What we're doing is actually wrapping GenServer functionality in a standardized way. With the code above we've implemented the first method: start_link. This wraps GenServer.start_link/3. Now we need to open up a public API which will accept calls like add_item, remove_item etc. One problem we have, however, is that functional programming doesn't have the concept of "state", necessarily. It's all about immutability and not changing things - so how the hell are we going to track the items in our cart! That's where GenServers come in. They don't really have state - they have accumulators. The best way to explain this is to just show you what I mean. When you call start_link you pass in the initial "state" in the second position. This can be any data you want. In our case we're matching on %{key: key} = args which means we expect the key named "key", which represents a session key, to be passed in as our argument. The args variable can contain anything else, but at the very least it needs to have a key. GenServer will now set the args as our initial state. But we have a problem - these args can literally be anything and we don't want that. We also don't want to bleed out our struct to calling code (forcing the calling code to initialize only our struct) - let' make this a little friendlier and a bit tighter using the init callback: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end def init(%{key: key} = args) do struct(%Redfour.Shopping.Session{}, args) end #... end In the code above init/1 will be called automatically by GenServer right after start_link/3 and the initial arguments will be passed to it. The result of init/1 will then be the thing stored in "state". All we're doing here is creating the Session struct and passing it back. Why did we do this? Because: We want to be sure we're working with our struct as our state so we can add items, logs, etc We're allowing a map to be passed in instead of the struct, making our API a little easier to use. We don't have to do this - this is just my preference. Now, because I'm completely anal, let's tighten this up: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args), do: GenServer.start_link(__MODULE__,args, name: key) def init(%{key: key} = args), do: struct(%Redfour.Shopping.Session{}, args) #... end You don't have to do it like this - I just kind of dig one-liners as they're a bit easier to read. Now let's add something to the cart. Task 2: Adding, Removing, Updating Calling code shouldn't need to know it's working with a GenServer, it makes things a bit brittle and breaks an otherwise lovely encapsulation. So let's consider our public API: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits # privates end This is just the start, and if you're new to GenServers this will look strange. Basically we're just wrapping the functionality of GenServer here in a nicer API. For the method calls themselves, we're demanding a pid and then whatever arguments we need. I like to use Keyword Lists to identify arguments explicitly. This helps with matching. When appropriate I'll use a struct (and it's appropriate here) - I'll get to that in a minute. The most important part is that we're telling GenServer to issue a call/2 to a process identified by our pid. GenServer will do just that, passing the argument tuple along. But then what happens? This is where OO people might cringe. We're going to "dual purpose" our module here to not only issue the call, but to also pick it up. Which makes sense because this module is also our process - so basically it's calling itself. Why the ceremony? Simple answer: we need state: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits def handle_call({:select_item, item}, _sender, session) do end def handle_call({:remove_item, sku: sku}, _sender, session) do end def handle_call({:change_item, sku: sku}, _sender, session) do end # privates end With the addition of these three handle_call/3 methods, we can now execute calls to our process. Notice the three arguments - the first is the identifier and the data wrapped in a tuple (this is a standard Erlang/Elixir way of passing data around so you can match on it). The second identifies who's calling, which we're ignoring by preceding the variable with an _. Finally we have our session - which is our state. Let's do the simplest implementation: def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} {:reply, session, session} end This code is extremely simple. We're appending the passed in item to the items list on our struct (see above) and passing back a new session. Finally, we're telling GenServer that things are lovely with {:reply, session, session}. Let's talk about that response. If things go wrong we can pass back something else, like: :noreply, session - basically something happened, all went well, here's the new state :stop, reason, [reply], state - something bad happened, stop the process with a reason These are very useful responses for certain scenarios. For instance if an item comes in with a negative price, it might be a good idea to kill the session and send the customer packing fearing some type of hack - you could use :stop for that if you like. For what we're doing, we're replying with the new session and then resetting the new state of our process using that same session. This is the basic structure of our process - now we just need to wire things up. Task 3: Data Access Our Shopping.SessionSupervisor will restart our session if it :exits unceremoniously. Now that we've seen how GenServer's maintain state, it will use that last-known state for a restart. We could lean on that, or we could be a little safer and use a persistent data store. I'll sidestep discussions on what's the most appropriate, but let's quickly have a look at our options: A standard database like Postgres. For this we could use Ecto or something a little lighter weight, such as Moebius. A document system, such as Erlang's built-in Mnesia using the Amnesia library. This is a great option for getting things off the ground, but not so good moving forward. The simple reason is that querying is a bit painful but also - it's not partition tolerant. Meaning if one node goes down, writes can't happen. It is ACID compliant, however... We could also use RethinkDB using Peter Hamilton's rethinkdb-elixir library. I've used it, I really like it! We're not allowed to use MongoDB here at Red:4, but you're welcome to have a look at that solution. No persistence at all. This is a bonkers approach. It means that if you restart your server, your customers lose their sessions. When you do the math on this it's actually a very, very small inconvenience to a select few people. Erlang tends to run for long stretches and if you plan your restarts accordingly - well it might not be so bad. Also - losing cart data might be acceptable to you. For me, I'm choosing Postgres and Moebius. I like the ease of use (it's why I wrote it) and if I didn't choose it... well I'd probably be in trouble. If you want to play along, I'll be using the dbworker branch which should be 2.0 in the future. The first thing I need to do is install Moebius in apps/shopping/mix.exs and set it as an application: def application do [applications: [:logger, :moebius], mod: {Redfour.Shopping, []}] end defp deps do [ {:moebius, github: "robconery/moebius", branch: "dbworker"}, ] end Next, I need to setup a database module that will become a worker which will be supervised. I'll add /lib/shopping/db.ex: defmodule Redfour.Shopping.Db do use Moebius.Database import Moebius.DocumentQuery alias Redfour.Shopping.Db #session stuff def find_or_create_session(%{key: key, domain: domain} = args) do case db(:sessions) |> contains(key: key) |> Db.first do nil -> db(:sessions) |> Db.save(struct(%Redfour.Shopping.Session{}, args)) found -> found end end def save_session(session) do db(:sessions) |> Db.save(session) end end Moebius allows you to work with Postgres as a document store - which is a great way to get yourself off the ground. Here I simply need to use Moebius.Database which gives us some macros to play with, including run, first, save. Which is all I need for our Session. With the first method I'm finding or creating (using Postgres 9.5 I could use upsert but I'm using 9.4), with the second I'm just pushing the entire session into the database as a document (Moebius wraps this up for me). OK, so I have a module that "declares a database" if you will. Why did I do all of this? Because my new database module is a GenServer and I'll want to be sure it's supervised within the scope of my app. For that, let's add some code to our start routine: defmodule Redfour.Shopping do use Application import Supervisor.Spec, warn: false #the entry point to start our app def start(_type, _args) do #supervision goes here start_session_supervisor start_database end def start_database do #start the supervised DB db_worker = worker(Redfour.Shopping.Db, [database: "redfour"]) Supervisor.start_link [db_worker], strategy: :one_for_one end #... end We're loading up a worker, passing in a reference to our database. This is a significant change to what Moebius used to do (you would set the connection info in config) - by changing like this we can now have multiple database connections formalized and supervised. Yeeha. Now that I have this in place, let's make our Session data aware: defmodule Redfour.Shopping.Session do use GenServer alias Redfour.Shopping.Db defstruct [ store_id: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [%{entry: "Session Created"}], discounts: [] ] def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__, args, name: key) end def init(%{key: key, domain: domain} = args) do session = Db.find_or_create_session(args) {:ok, session} end #GenServer callbacks def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} |> save_session {:reply, session, session} end #... # privates def save_session(%Redfour.Shopping.Session{} = session, log: log) do %{session | logs: List.insert_at(session.logs, -1, %{entry: log})} |> Db.save_session end end This is a good start, but it's far from a final solution. Things are still a bit too lose. Summary We need to put some guards in place for when things don't go well, and I also need to plug in better date management. Right now you can pass whatever you want to select_item which isn't good... so we'll fix that. Next time! [Less]
Posted about 8 years ago
Let's implement an intelligent shopping cart - something that tracks what the customer is doing, how they came to our store, etc. I tend to think of these things in terms of a "Session" - a shopping process where a customer selects things, puts them ... [More] back, and eventually (hopefully) buys something. If I do things correctly (to me, at least), I should end up with tight little functions an exactly 0 if statements. Task 1: Proper Initialization For review - we have a struct defined that we can use to hold items, logs, discounts and more. Each customer coming to our store will have a dedicated process which is supervised by the VM (/apps/shopping/lib/shopping/session.ex): defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] end Working with GenServers takes a little time to get used to. What we're doing is actually wrapping GenServer functionality in a standardized way. With the code above we've implemented the first method: start_link. This wraps GenServer.start_link/3. Now we need to open up a public API which will accept calls like add_item, remove_item etc. One problem we have, however, is that functional programming doesn't have the concept of "state", necessarily. It's all about immutability and not changing things - so how the hell are we going to track the items in our cart! That's where GenServers come in. They don't really have state - they have accumulators. The best way to explain this is to just show you what I mean. When you call start_link you pass in the initial "state" in the second position. This can be any data you want. In our case we're matching on %{key: key} = args which means we expect the key named "key", which represents a session key, to be passed in as our argument. The args variable can contain anything else, but at the very least it needs to have a key. GenServer will now set the args as our initial state. But we have a problem - these args can literally be anything and we don't want that. We also don't want to bleed out our struct to calling code (forcing the calling code to initialize only our struct) - let' make this a little friendlier and a bit tighter using the init callback: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end def init(%{key: key} = args) do struct(%Redfour.Shopping.Session{}, args) end #... end In the code above init/1 will be called automatically by GenServer right after start_link/3 and the initial arguments will be passed to it. The result of init/1 will then be the thing stored in "state". All we're doing here is creating the Session struct and passing it back. Why did we do this? Because: We want to be sure we're working with our struct as our state so we can add items, logs, etc We're allowing a map to be passed in instead of the struct, making our API a little easier to use. We don't have to do this - this is just my preference. Now, because I'm completely anal, let's tighten this up: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args), do: GenServer.start_link(__MODULE__,args, name: key) def init(%{key: key} = args), do: struct(%Redfour.Shopping.Session{}, args) #... end You don't have to do it like this - I just kind of dig one-liners as they're a bit easier to read. Now let's add something to the cart. Task 2: Adding, Removing, Updating Calling code shouldn't need to know it's working with a GenServer, it makes things a bit brittle and breaks an otherwise lovely encapsulation. So let's consider our public API: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits # privates end This is just the start, and if you're new to GenServers this will look strange. Basically we're just wrapping the functionality of GenServer here in a nicer API. For the method calls themselves, we're demanding a pid and then whatever arguments we need. I like to use Keyword Lists to identify arguments explicitly. This helps with matching. When appropriate I'll use a struct (and it's appropriate here) - I'll get to that in a minute. The most important part is that we're telling GenServer to issue a call/2 to a process identified by our pid. GenServer will do just that, passing the argument tuple along. But then what happens? This is where OO people might cringe. We're going to "dual purpose" our module here to not only issue the call, but to also pick it up. Which makes sense because this module is also our process - so basically it's calling itself. Why the ceremony? Simple answer: we need state: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits def handle_call({:select_item, item}, _sender, session) do end def handle_call({:remove_item, sku: sku}, _sender, session) do end def handle_call({:change_item, sku: sku}, _sender, session) do end # privates end With the addition of these three handle_call/3 methods, we can now execute calls to our process. Notice the three arguments - the first is the identifier and the data wrapped in a tuple (this is a standard Erlang/Elixir way of passing data around so you can match on it). The second identifies who's calling, which we're ignoring by preceding the variable with an _. Finally we have our session - which is our state. Let's do the simplest implementation: def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} {:reply, session, session} end This code is extremely simple. We're appending the passed in item to the items list on our struct (see above) and passing back a new session. Finally, we're telling GenServer that things are lovely with {:reply, session, session}. Let's talk about that response. If things go wrong we can pass back something else, like: :noreply, session - basically something happened, all went well, here's the new state :stop, reason, [reply], state - something bad happened, stop the process with a reason These are very useful responses for certain scenarios. For instance if an item comes in with a negative price, it might be a good idea to kill the session and send the customer packing fearing some type of hack - you could use :stop for that if you like. For what we're doing, we're replying with the new session and then resetting the new state of our process using that same session. This is the basic structure of our process - now we just need to wire things up. Task 3: Data Access Our Shopping.SessionSupervisor will restart our session if it :exits unceremoniously. We could create a "stash worker" (see Programming Elixir from PragProg, page 214) and have our process seamlessly restarted, or we could be a little safer and use a persistent data store. Note: I originally stated that Supervisor's will restart a service with the last known state. This is incorrect - thanks to Graham Kay for correcting the mistake! I'll sidestep discussions on what's the most appropriate, but let's quickly have a look at our options: A standard database like Postgres. For this we could use Ecto or something a little lighter weight, such as Moebius. A document system, such as Erlang's built-in Mnesia using the Amnesia library. This is a great option for getting things off the ground, but not so good moving forward. The simple reason is that querying is a bit painful but also - it's not partition tolerant. Meaning if one node goes down, writes can't happen. It is ACID compliant, however... We could also use RethinkDB using Peter Hamilton's rethinkdb-elixir library. I've used it, I really like it! We're not allowed to use MongoDB here at Red:4, but you're welcome to have a look at that solution. No persistence at all. This is a bonkers approach. It means that if you restart your server, your customers lose their sessions. When you do the math on this it's actually a very, very small inconvenience to a select few people. Erlang tends to run for long stretches and if you plan your restarts accordingly - well it might not be so bad. Also - losing cart data might be acceptable to you. For me, I'm choosing Postgres and Moebius. I like the ease of use (it's why I wrote it) and if I didn't choose it... well I'd probably be in trouble. If you want to play along, I'll be using the dbworker branch which should be 2.0 in the future. The first thing I need to do is install Moebius in apps/shopping/mix.exs and set it as an application: def application do [applications: [:logger, :moebius], mod: {Redfour.Shopping, []}] end defp deps do [ {:moebius, github: "robconery/moebius", branch: "dbworker"}, ] end Next, I need to setup a database module that will become a worker which will be supervised. I'll add /lib/shopping/db.ex: defmodule Redfour.Shopping.Db do use Moebius.Database import Moebius.DocumentQuery alias Redfour.Shopping.Db #session stuff def find_or_create_session(%{key: key, domain: domain} = args) do case db(:sessions) |> contains(key: key) |> Db.first do nil -> db(:sessions) |> Db.save(struct(%Redfour.Shopping.Session{}, args)) found -> found end end def save_session(session) do db(:sessions) |> Db.save(session) end end Moebius allows you to work with Postgres as a document store - which is a great way to get yourself off the ground. Here I simply need to use Moebius.Database which gives us some macros to play with, including run, first, save. Which is all I need for our Session. With the first method I'm finding or creating (using Postgres 9.5 I could use upsert but I'm using 9.4), with the second I'm just pushing the entire session into the database as a document (Moebius wraps this up for me). OK, so I have a module that "declares a database" if you will. Why did I do all of this? Because my new database module is a GenServer and I'll want to be sure it's supervised within the scope of my app. For that, let's add some code to our start routine: defmodule Redfour.Shopping do use Application import Supervisor.Spec, warn: false #the entry point to start our app def start(_type, _args) do #supervision goes here start_session_supervisor start_database end def start_database do #start the supervised DB db_worker = worker(Redfour.Shopping.Db, [database: "redfour"]) Supervisor.start_link [db_worker], strategy: :one_for_one end #... end We're loading up a worker, passing in a reference to our database. This is a significant change to what Moebius used to do (you would set the connection info in config) - by changing like this we can now have multiple database connections formalized and supervised. Yeeha. Now that I have this in place, let's make our Session data aware: defmodule Redfour.Shopping.Session do use GenServer alias Redfour.Shopping.Db defstruct [ store_id: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [%{entry: "Session Created"}], discounts: [] ] def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__, args, name: key) end def init(%{key: key, domain: domain} = args) do session = Db.find_or_create_session(args) {:ok, session} end #GenServer callbacks def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} |> save_session {:reply, session, session} end #... # privates def save_session(%Redfour.Shopping.Session{} = session, log: log) do %{session | logs: List.insert_at(session.logs, -1, %{entry: log})} |> Db.save_session end end This is a good start, but it's far from a final solution. Things are still a bit too lose. Summary We need to put some guards in place for when things don't go well, and I also need to plug in better date management. Right now you can pass whatever you want to select_item which isn't good... so we'll fix that. Next time! Want to learn Elixir? Learn how to build fast, fault-tolerant applications with Elixir. This is not a traditional, boring tutorial. You'll get an ebook (epub or mobi) as well as 3 hours worth of tightly-edited, lovingly produced Elixir content. You'll learn Elixir while doing Elixir, helping me out at my new fictional job as development lead at Red:4 Aerospace. [Less]
Posted about 8 years ago
Let's implement an intelligent shopping cart - something that tracks what the customer is doing, how they came to our store, etc. I tend to think of these things in terms of a "Session" - a shopping process where a customer selects things, puts them ... [More] back, and eventually (hopefully) buys something. If I do things correctly (to me, at least), I should end up with tight little functions an exactly 0 if statements. Task 1: Proper Initialization For review - we have a struct defined that we can use to hold items, logs, discounts and more. Each customer coming to our store will have a dedicated process which is supervised by the VM (/apps/shopping/lib/shopping/session.ex): defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] end Working with GenServers takes a little time to get used to. What we're doing is actually wrapping GenServer functionality in a standardized way. With the code above we've implemented the first method: start_link. This wraps GenServer.start_link/3. Now we need to open up a public API which will accept calls like add_item, remove_item etc. One problem we have, however, is that functional programming doesn't have the concept of "state", necessarily. It's all about immutability and not changing things - so how the hell are we going to track the items in our cart! That's where GenServers come in. They don't really have state - they have accumulators. The best way to explain this is to just show you what I mean. When you call start_link you pass in the initial "state" in the second position. This can be any data you want. In our case we're matching on %{key: key} = args which means we expect the key named "key", which represents a session key, to be passed in as our argument. The args variable can contain anything else, but at the very least it needs to have a key. GenServer will now set the args as our initial state. But we have a problem - these args can literally be anything and we don't want that. We also don't want to bleed out our struct to calling code (forcing the calling code to initialize only our struct) - let' make this a little friendlier and a bit tighter using the init callback: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end def init(%{key: key} = args) do struct(%Redfour.Shopping.Session{}, args) end #... end In the code above init/1 will be called automatically by GenServer right after start_link/3 and the initial arguments will be passed to it. The result of init/1 will then be the thing stored in "state". All we're doing here is creating the Session struct and passing it back. Why did we do this? Because: We want to be sure we're working with our struct as our state so we can add items, logs, etc We're allowing a map to be passed in instead of the struct, making our API a little easier to use. We don't have to do this - this is just my preference. Now, because I'm completely anal, let's tighten this up: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args), do: GenServer.start_link(__MODULE__,args, name: key) def init(%{key: key} = args), do: struct(%Redfour.Shopping.Session{}, args) #... end You don't have to do it like this - I just kind of dig one-liners as they're a bit easier to read. Now let's add something to the cart. Task 2: Adding, Removing, Updating Calling code shouldn't need to know it's working with a GenServer, it makes things a bit brittle and breaks an otherwise lovely encapsulation. So let's consider our public API: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits # privates end This is just the start, and if you're new to GenServers this will look strange. Basically we're just wrapping the functionality of GenServer here in a nicer API. For the method calls themselves, we're demanding a pid and then whatever arguments we need. I like to use Keyword Lists to identify arguments explicitly. This helps with matching. When appropriate I'll use a struct (and it's appropriate here) - I'll get to that in a minute. The most important part is that we're telling GenServer to issue a call/2 to a process identified by our pid. GenServer will do just that, passing the argument tuple along. But then what happens? This is where OO people might cringe. We're going to "dual purpose" our module here to not only issue the call, but to also pick it up. Which makes sense because this module is also our process - so basically it's calling itself. Why the ceremony? Simple answer: we need state: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits def handle_call({:select_item, item}, _sender, session) do end def handle_call({:remove_item, sku: sku}, _sender, session) do end def handle_call({:change_item, sku: sku}, _sender, session) do end # privates end With the addition of these three handle_call/3 methods, we can now execute calls to our process. Notice the three arguments - the first is the identifier and the data wrapped in a tuple (this is a standard Erlang/Elixir way of passing data around so you can match on it). The second identifies who's calling, which we're ignoring by preceding the variable with an _. Finally we have our session - which is our state. Let's do the simplest implementation: def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} {:reply, session, session} end This code is extremely simple. We're appending the passed in item to the items list on our struct (see above) and passing back a new session. Finally, we're telling GenServer that things are lovely with {:reply, session, session}. Let's talk about that response. If things go wrong we can pass back something else, like: :noreply, session - basically something happened, all went well, here's the new state :stop, reason, [reply], state - something bad happened, stop the process with a reason These are very useful responses for certain scenarios. For instance if an item comes in with a negative price, it might be a good idea to kill the session and send the customer packing fearing some type of hack - you could use :stop for that if you like. For what we're doing, we're replying with the new session and then resetting the new state of our process using that same session. This is the basic structure of our process - now we just need to wire things up. Task 3: Data Access Our Shopping.SessionSupervisor will restart our session if it :exits unceremoniously. We could create a "stash worker" (see Programming Elixir from PragProg, page 214) and have our process seamlessly restarted, or we could be a little safer and use a persistent data store. Note: I originally stated that Supervisor's will restart a service with the last known state. This is incorrect - thanks to Graham Kay for correcting the mistake! I'll sidestep discussions on what's the most appropriate, but let's quickly have a look at our options: A standard database like Postgres. For this we could use Ecto or something a little lighter weight, such as Moebius. A document system, such as Erlang's built-in Mnesia using the Amnesia library. This is a great option for getting things off the ground, but not so good moving forward. The simple reason is that querying is a bit painful but also - it's not partition tolerant. Meaning if one node goes down, writes can't happen. It is ACID compliant, however... We could also use RethinkDB using Peter Hamilton's rethinkdb-elixir library. I've used it, I really like it! We're not allowed to use MongoDB here at Red:4, but you're welcome to have a look at that solution. No persistence at all. This is a bonkers approach. It means that if you restart your server, your customers lose their sessions. When you do the math on this it's actually a very, very small inconvenience to a select few people. Erlang tends to run for long stretches and if you plan your restarts accordingly - well it might not be so bad. Also - losing cart data might be acceptable to you. For me, I'm choosing Postgres and Moebius. I like the ease of use (it's why I wrote it) and if I didn't choose it... well I'd probably be in trouble. If you want to play along, I'll be using the dbworker branch which should be 2.0 in the future. The first thing I need to do is install Moebius in apps/shopping/mix.exs and set it as an application: def application do [applications: [:logger, :moebius], mod: {Redfour.Shopping, []}] end defp deps do [ {:moebius, github: "robconery/moebius", branch: "dbworker"}, ] end Next, I need to setup a database module that will become a worker which will be supervised. I'll add /lib/shopping/db.ex: defmodule Redfour.Shopping.Db do use Moebius.Database import Moebius.DocumentQuery alias Redfour.Shopping.Db #session stuff def find_or_create_session(%{key: key, domain: domain} = args) do case db(:sessions) |> contains(key: key) |> Db.first do nil -> db(:sessions) |> Db.save(struct(%Redfour.Shopping.Session{}, args)) found -> found end end def save_session(session) do db(:sessions) |> Db.save(session) end end Moebius allows you to work with Postgres as a document store - which is a great way to get yourself off the ground. Here I simply need to use Moebius.Database which gives us some macros to play with, including run, first, save. Which is all I need for our Session. With the first method I'm finding or creating (using Postgres 9.5 I could use upsert but I'm using 9.4), with the second I'm just pushing the entire session into the database as a document (Moebius wraps this up for me). OK, so I have a module that "declares a database" if you will. Why did I do all of this? Because my new database module is a GenServer and I'll want to be sure it's supervised within the scope of my app. For that, let's add some code to our start routine: defmodule Redfour.Shopping do use Application import Supervisor.Spec, warn: false #the entry point to start our app def start(_type, _args) do #supervision goes here start_session_supervisor start_database end def start_database do #start the supervised DB db_worker = worker(Redfour.Shopping.Db, [database: "redfour"]) Supervisor.start_link [db_worker], strategy: :one_for_one end #... end We're loading up a worker, passing in a reference to our database. This is a significant change to what Moebius used to do (you would set the connection info in config) - by changing like this we can now have multiple database connections formalized and supervised. Yeeha. Now that I have this in place, let's make our Session data aware: defmodule Redfour.Shopping.Session do use GenServer alias Redfour.Shopping.Db defstruct [ store_id: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [%{entry: "Session Created"}], discounts: [] ] def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__, args, name: key) end def init(%{key: key, domain: domain} = args) do session = Db.find_or_create_session(args) {:ok, session} end #GenServer callbacks def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} |> save_session {:reply, session, session} end #... # privates def save_session(%Redfour.Shopping.Session{} = session, log: log) do %{session | logs: List.insert_at(session.logs, -1, %{entry: log})} |> Db.save_session end end This is a good start, but it's far from a final solution. Things are still a bit too lose. Summary We need to put some guards in place for when things don't go well, and I also need to plug in better date management. Right now you can pass whatever you want to select_item which isn't good... so we'll fix that. Next time! [Less]
Posted about 8 years ago
Let's implement an intelligent shopping cart - something that tracks what the customer is doing, how they came to our store, etc. I tend to think of these things in terms of a "Session" - a shopping process where a customer selects things, puts them ... [More] back, and eventually (hopefully) buys something. If I do things correctly (to me, at least), I should end up with tight little functions an exactly 0 if statements. Task 1: Proper Initialization For review - we have a struct defined that we can use to hold items, logs, discounts and more. Each customer coming to our store will have a dedicated process which is supervised by the VM (/apps/shopping/lib/shopping/session.ex): defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end defstruct [ domain: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [], discounts: [] ] end Working with GenServers takes a little time to get used to. What we're doing is actually wrapping GenServer functionality in a standardized way. With the code above we've implemented the first method: start_link. This wraps GenServer.start_link/3. Now we need to open up a public API which will accept calls like add_item, remove_item etc. One problem we have, however, is that functional programming doesn't have the concept of "state", necessarily. It's all about immutability and not changing things - so how the hell are we going to track the items in our cart! That's where GenServers come in. They don't really have state - they have accumulators. The best way to explain this is to just show you what I mean. When you call start_link you pass in the initial "state" in the second position. This can be any data you want. In our case we're matching on %{key: key} = args which means we expect the key named "key", which represents a session key, to be passed in as our argument. The args variable can contain anything else, but at the very least it needs to have a key. GenServer will now set the args as our initial state. But we have a problem - these args can literally be anything and we don't want that. We also don't want to bleed out our struct to calling code (forcing the calling code to initialize only our struct) - let' make this a little friendlier and a bit tighter using the init callback: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__,args, name: key) end def init(%{key: key} = args) do struct(%Redfour.Shopping.Session{}, args) end #... end In the code above init/1 will be called automatically by GenServer right after start_link/3 and the initial arguments will be passed to it. The result of init/1 will then be the thing stored in "state". All we're doing here is creating the Session struct and passing it back. Why did we do this? Because: We want to be sure we're working with our struct as our state so we can add items, logs, etc We're allowing a map to be passed in instead of the struct, making our API a little easier to use. We don't have to do this - this is just my preference. Now, because I'm completely anal, let's tighten this up: defmodule Redfour.Shopping.Session do use GenServer def start_link(%{key: key} = args), do: GenServer.start_link(__MODULE__,args, name: key) def init(%{key: key} = args), do: struct(%Redfour.Shopping.Session{}, args) #... end You don't have to do it like this - I just kind of dig one-liners as they're a bit easier to read. Now let's add something to the cart. Task 2: Adding, Removing, Updating Calling code shouldn't need to know it's working with a GenServer, it makes things a bit brittle and breaks an otherwise lovely encapsulation. So let's consider our public API: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits # privates end This is just the start, and if you're new to GenServers this will look strange. Basically we're just wrapping the functionality of GenServer here in a nicer API. For the method calls themselves, we're demanding a pid and then whatever arguments we need. I like to use Keyword Lists to identify arguments explicitly. This helps with matching. When appropriate I'll use a struct (and it's appropriate here) - I'll get to that in a minute. The most important part is that we're telling GenServer to issue a call/2 to a process identified by our pid. GenServer will do just that, passing the argument tuple along. But then what happens? This is where OO people might cringe. We're going to "dual purpose" our module here to not only issue the call, but to also pick it up. Which makes sense because this module is also our process - so basically it's calling itself. Why the ceremony? Simple answer: we need state: defmodule Redfour.Shopping.Session do #... initialization stuff # public API def select_item(pid, item), do: GenServer.call(pid, {:select_item, item}) def remove_item(pid, sku: sku), do: GenServer.call(pid, {:remove_item, sku: sku}) def change_item(pid, sku: sku), do: GenServer.call(pid, {:change_item, sku: sku}) # internal GenServer bits def handle_call({:select_item, item}, _sender, session) do end def handle_call({:remove_item, sku: sku}, _sender, session) do end def handle_call({:change_item, sku: sku}, _sender, session) do end # privates end With the addition of these three handle_call/3 methods, we can now execute calls to our process. Notice the three arguments - the first is the identifier and the data wrapped in a tuple (this is a standard Erlang/Elixir way of passing data around so you can match on it). The second identifies who's calling, which we're ignoring by preceding the variable with an _. Finally we have our session - which is our state. Let's do the simplest implementation: def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} {:reply, session, session} end This code is extremely simple. We're appending the passed in item to the items list on our struct (see above) and passing back a new session. Finally, we're telling GenServer that things are lovely with {:reply, session, session}. Let's talk about that response. If things go wrong we can pass back something else, like: :noreply, session - basically something happened, all went well, here's the new state :stop, reason, [reply], state - something bad happened, stop the process with a reason These are very useful responses for certain scenarios. For instance if an item comes in with a negative price, it might be a good idea to kill the session and send the customer packing fearing some type of hack - you could use :stop for that if you like. For what we're doing, we're replying with the new session and then resetting the new state of our process using that same session. This is the basic structure of our process - now we just need to wire things up. Task 3: Data Access Our Shopping.SessionSupervisor will restart our session if it :exits unceremoniously. We could create a "stash worker" (see Programming Elixir from PragProg, page 214) and have our process seamlessly restarted, or we could be a little safer and use a persistent data store. Note: I originally stated that Supervisor's will restart a service with the last known state. This is incorrect - thanks to Graham Kay for correcting the mistake! I'll sidestep discussions on what's the most appropriate, but let's quickly have a look at our options: A standard database like Postgres. For this we could use Ecto or something a little lighter weight, such as Moebius. A document system, such as Erlang's built-in Mnesia using the Amnesia library. This is a great option for getting things off the ground, but not so good moving forward. The simple reason is that querying is a bit painful but also - it's not partition tolerant. Meaning if one node goes down, writes can't happen. It is ACID compliant, however... We could also use RethinkDB using Peter Hamilton's rethinkdb-elixir library. I've used it, I really like it! We're not allowed to use MongoDB here at Red:4, but you're welcome to have a look at that solution. No persistence at all. This is a bonkers approach. It means that if you restart your server, your customers lose their sessions. When you do the math on this it's actually a very, very small inconvenience to a select few people. Erlang tends to run for long stretches and if you plan your restarts accordingly - well it might not be so bad. Also - losing cart data might be acceptable to you. For me, I'm choosing Postgres and Moebius. I like the ease of use (it's why I wrote it) and if I didn't choose it... well I'd probably be in trouble. If you want to play along, I'll be using the dbworker branch which should be 2.0 in the future. The first thing I need to do is install Moebius in apps/shopping/mix.exs and set it as an application: def application do [applications: [:logger, :moebius], mod: {Redfour.Shopping, []}] end defp deps do [ {:moebius, github: "robconery/moebius", branch: "dbworker"}, ] end Next, I need to setup a database module that will become a worker which will be supervised. I'll add /lib/shopping/db.ex: defmodule Redfour.Shopping.Db do use Moebius.Database import Moebius.DocumentQuery alias Redfour.Shopping.Db #session stuff def find_or_create_session(%{key: key, domain: domain} = args) do case db(:sessions) |> contains(key: key) |> Db.first do nil -> db(:sessions) |> Db.save(struct(%Redfour.Shopping.Session{}, args)) found -> found end end def save_session(session) do db(:sessions) |> Db.save(session) end end Moebius allows you to work with Postgres as a document store - which is a great way to get yourself off the ground. Here I simply need to use Moebius.Database which gives us some macros to play with, including run, first, save. Which is all I need for our Session. With the first method I'm finding or creating (using Postgres 9.5 I could use upsert but I'm using 9.4), with the second I'm just pushing the entire session into the database as a document (Moebius wraps this up for me). OK, so I have a module that "declares a database" if you will. Why did I do all of this? Because my new database module is a GenServer and I'll want to be sure it's supervised within the scope of my app. For that, let's add some code to our start routine: defmodule Redfour.Shopping do use Application import Supervisor.Spec, warn: false #the entry point to start our app def start(_type, _args) do #supervision goes here start_session_supervisor start_database end def start_database do #start the supervised DB db_worker = worker(Redfour.Shopping.Db, [database: "redfour"]) Supervisor.start_link [db_worker], strategy: :one_for_one end #... end We're loading up a worker, passing in a reference to our database. This is a significant change to what Moebius used to do (you would set the connection info in config) - by changing like this we can now have multiple database connections formalized and supervised. Yeeha. Now that I have this in place, let's make our Session data aware: defmodule Redfour.Shopping.Session do use GenServer alias Redfour.Shopping.Db defstruct [ store_id: nil, id: nil, key: nil, landing: "/", ip: "127.0.0.1", member_id: nil, items: [], logs: [%{entry: "Session Created"}], discounts: [] ] def start_link(%{key: key} = args) do GenServer.start_link(__MODULE__, args, name: key) end def init(%{key: key, domain: domain} = args) do session = Db.find_or_create_session(args) {:ok, session} end #GenServer callbacks def handle_call({:select_item, item}, _sender, session) do session = %{session | items: List.insert_at(session.items, -1, item)} |> save_session {:reply, session, session} end #... # privates def save_session(%Redfour.Shopping.Session{} = session, log: log) do %{session | logs: List.insert_at(session.logs, -1, %{entry: log})} |> Db.save_session end end This is a good start, but it's far from a final solution. Things are still a bit too lose. Summary We need to put some guards in place for when things don't go well, and I also need to plug in better date management. Right now you can pass whatever you want to select_item which isn't good... so we'll fix that. Next time! [Less]