Here at DNSimple we have agreed to use Elixir typespecs and dialyzer in every Elixir project we have to help catch bugs early. Part of this agreement is to maintain good, up-to-date type specs and proper types for our functions. As an additional check, Dialyzer runs within our CI pipeline on Travis CI for every commit we push and makes the whole build fail if it detects an error.

As a result, quite often we are confronted with these error messages:

Proceeding with analysis...
config.ex:64: The call ets:insert('Elixir.MyApp.Config',{'Elixir.MyApp.Config',_}) might have an unintended effect due to a possible race condition caused by its combination withthe ets:lookup('Elixir.MyApp.Config','Elixir.MyApp.Config') call in config.ex on line 26
config.ex:79: Guard test is_binary(_@5::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed
config.ex:79: Guard test is_atom(_@6::#{'__exception__':='true', '__struct__':=_, _=>_}) can never succeed
done in 0m1.32s
done (warnings were emitted)

Example output of a mix dialyzer run.

If you, like me, always have a hard time understanding these errors, I highly recommend switching to dialyxir version 1. This is another tool following the growing industry trend to really pay attention to the readability and utility of error messages. (At the time of this posting, dialyxir RC2 has been out for a few days.)

But what's worse than understanding these errors is that running mix dialyzer can add about 10 - 15 minutes to your build time,

So when we added this

# .travis.yml
script:
  - mix test
  - mix dialyzer --halt-exit-status

to our .travis.yml build configuration we had to tackle these long build times.

Poor-man's solution

When we are facing a problem my co-worker, Corey, usually says:

Do the simplest thing first.

so our first approach was to cache the _build and ~/.mix/ directory.

While this solutions works in practice and reduces the build times back to ~3 minutes it also breaks with the idea that every build should be fresh and provide a guarantee the software and setup is documented and works as expected. Because developer machines usually end up in the weirdest not at all production like states.

Understanding the matter and caching the right things

Going through the README of dialyxir it contains a pretty good explanation of the different PLT files and how they are used. These files are basically the core files for Erlang and Elixir and the project-specific ones. In the mix dialyzer run, the core files are copied into the project-specific ones. You can specify the project-specific file via the plt_file keyword. That file and it's corresponding hash file then can be added to the build cache. This maintains swift CI runs while building the whole project fresh for every run.

This is a diff and the corresponding recent contribution we just made to eventstore to have faster dialyzer runs for their CI.

diff --git a/.gitignore b/.gitignore
index 7d26fdc..ef9ade5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
 /cover
 /deps
 /doc
+/priv/plts/*.plt
+/priv/plts/*.plt.hash
 erl_crash.dump
 *.ez
 *.dump
diff --git a/.travis.yml b/.travis.yml
index ddb6671..5cc32e5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,9 @@
 language: elixir

+cache:
+  directories:
+  - priv/plts
+
 elixir:
   - 1.6.5

diff --git a/mix.exs b/mix.exs
index 0c2176d..c6a17b0 100644
--- a/mix.exs
+++ b/mix.exs
@@ -122,7 +122,8 @@ EventStore using PostgreSQL for persistence.
   defp dialyzer do
     [
       plt_add_apps: [:poison, :ex_unit],
-      plt_add_deps: :app_tree
+      plt_add_deps: :app_tree,
+      plt_file: {:no_warn, "priv/plts/eventstore.plt"}
     ]
   end

If you not only want to experience fast CI runs, but also like fast customer support and fast global DNS record distribution you should try our DNSimple domain automation tools with your 30-day free trial.