408546 (2) [Avatar] Offline
#1
The first thing I noticed when I hit Chapter 5 and started working the examples was that I would have the code successfully compile but then not function correctly (listings 5.1-6.) Up until that point every time there was an error I'd get a compile error. These errors were simply typos that I found by reading back over the code I entered and noticed a misspelled word. But this raised the question for me: how do you go about debugging server processes when they so easily compile without throwing errors over typos? Is there a good way to go about this? Or maybe this is covered further along in the book... The examples are small amounts of relatively simple code so they are easy to eyeball but it seems like this might be an issue with longer more complex code.
sjuric (86) [Avatar] Offline
#2
It's unclear which typos did you exactly make. Frequently, a typo will result in a compile error. For example, if you try to invoke "fo()" instead of "foo()", a compiler will complain that there's no such function.

In some cases, you might invoke "fo" which is available, but you really want to invoke "foo". That will probably cause a runtime error, which will be displayed in console (with a reason and a stack trace), so you can see it and do something about it.

Yet another type of error can happen in the message format. For example, let's say that the server process awaits a particular type of a message:

receive do
  {:run_query, caller, query_def} ->
    send(caller, {:query_result, run_query(query_def)})
end


And in the client code you mistype a message:

def run_async(server_pid, query_def) do
  send(server_pid, {:run_qury, self, query_def})
end


This will be silently ignored. A server code awaits one type of message, while a different message is sent. So basically nothing happens.

This error is easy to make when using plain spawn, send, and receive. An idiomatic solution is to accept every message, and pattern match on it in a multiclause function:

receive do
  message -> handle_message(message)
end
...

defp handle_message({:run_query, caller, query_def}) do
  ...
end


The difference here is that we're accepting any message, and then pattern matching on it in handle_message. If a message format is invalid, an error will be raised since no clause of handle_message will match it. That will make an error explicit.

In subsequent chapters you'll learn about a GenServer abstraction which simplifies some of the lower level mechanics of server processes. You'll also learn about supervisors which allow you to detect failures and react to them.

That ties together with this idea of raising a runtime error. In Elixir (and Erlang), it's idiomatic to fail fast if something is wrong. Using pattern matching, we can frequently assert the kind of data we can deal with, and raise if the data is invalid. Such errors will be logged (so you can see them), while supervisors will detect them and react to them, allowing the system to heal.

So the whole point is: sometimes you'll get a compile-time error, while in other cases, you should write your code to raise on unknown input. To keep distractions away, I didn't do that in listings 5.1 - 5.6, but it will happen later on. But as you'll learn, in production you usually won't write these loops, but rather use a GenServer. That will usually push you in direction of explicit failing on invalid input, since GenServer receives all messages, and you have to explicitly match the ones you know how to handle. That will in turn raise a runtime error on all other types of messages.

Finally, I provide a brief mention of some debugging techniques in the final chapter. But by far the most useful thing you can do is to fail explicitly on unknown input. That failure will be logged so you'll be able to see it and do something about it.
408546 (2) [Avatar] Offline
#3
Thank you, that very effectively answers my question. My first error was that I left out caller just as you illustrated there, and I think the second one was that I typed

send(from_pid, {:query_result, qeury_result})

instead of

send(from_pid, {:query_result, query_result})


Both caused the five second error timeout so I knew something was wrong but it was hard to know where to start troubleshooting at first glance. So you've provided an effective solution for this immediate concern and it sounds like the more sophisticated abstractions around server processes will do a better job of catching such mistakes in the future.

Thanks again.
sjuric (86) [Avatar] Offline
#4
408546 wrote: and I think the second one was that I typed

send(from_pid, {:query_result, qeury_result})

instead of

send(from_pid, {:query_result, query_result})



This one is a bit nasty, since you can't guard against it in the client process. Namely, in the client process you need to match only the expected response message, leaving everything else in the mailbox. The reason is that those other messages might be requests from other processes, since the client process might itself act as a server process to other processes in the system.

The good news is that such errors won't happen with GenServer (which is the topic of the next chapter), since it abstracts away low level message protocol details, and thus makes sure that the response message corresponds to what the client process expects.

When it comes to troubleshooting, a cheap quick solution I tend to use frequently is to sprinkle IO.inspect around the code. A nice thing about IO.inspect is that it returns the input value, so you can put it in the code without changing the behavior. So if you have say:

send(foo, bar)


Changing it to:

send(foo, bar)
|> IO.inspect


Will print the result of send and return it. I find this simple technique useful to poke around and see if some piece of code is executed and what's its return value.

There are also more sophisticated solutions, such as pry, tracing, and even a GUI debugger. You can find a bit more in this blog post.

Those techniques are certainly useful for more elaborate scenarios, but for me personally, IO.inspect is the approach I tend to use most frequently, because it's cheap and usually helps me to quickly diagnose the issue smilie