nathany

nathany

GitHub Action Cache Elixir always recompiles dependencies (Elixir 1.13.3)

Looking to speed up our build on GitHub Actions. Right now the slowest task is compiling dependencies, which takes over 3 minutes. This is surprising, because the _build folder should be cached, but it behaves as though it isn’t.

I’ve found that the command mix deps.compile does a full recompile locally too, even if I run it twice in a row. Not sure if this is a regression in Elixir 1.13 or expected behaviour?

I’ll walk through the relevant portions of the GitHub Actions config.

setup-beam

For starts, using erlef/setup-beam, which is fairly standard. I don’t know if we need to specify anything extra for rebar? This is a fairly standard Phoenix app.

env:
  MIX_ENV: test
steps:
  - uses: actions/checkout@v2
  - uses: erlef/setup-beam@v1
    with:
      otp-version: "24.2.1"
      elixir-version: "1.13.3"

cache

Then onto actions/cache which is using the standard example for Elixir, other than that we’ve added a v2- prefix at some point to bust an old cache.

- uses: actions/cache@v2
  id: cache
  with:
    path: |
      deps
      _build
    key: v2-${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
    restore-keys: |
      v2-${{ runner.os }}-mix-

download dependencies

Downloading dependencies, including some private ones, is a step that we can reliably skip if there is a cache-hit. If the lock file hasn’t changed, everything still works and these two steps take zero seconds, proving that the cache is working.

- name: Authenticating Hex
  if: steps.cache.outputs.cache-hit != 'true'
  run: mix hex.organization auth ${ORG_NAME} --key ${HEX_ORG_KEY}
  env:
    HEX_ORG_KEY: ${{ secrets.HEX_ORG_KEY }}
    ORG_NAME: ${{ secrets.ORG_NAME }}
- name: Install dependencies
  if: steps.cache.outputs.cache-hit != 'true'
  # NOTE: AppSignal has its hooks in other dependencies, so we tell it to compile first
  run: |
    mix do deps.get, deps.compile appsignal --include-children

We do have some special handling for AppSignal here (which should maybe be moved into a separate step).

code formatting

We found that we can check code formatting before compiling, which saves some cycles by “failing fast.”

- name: Check code formatting
  run: mix format --check-formatted

Compiling

The main event. I recently separated out the compiling of dependencies, just to see the time they take vs. our app. The dependencies take 3-4 minutes to compile, whereas our app takes about 30 seconds or so.

- name: Compile dependencies
  run: |
    mix deps.compile
- name: Compile
  run: |
    mix compile --warnings-as-errors

If I skip the deps.compile step then compile will compile all the deps.

If the mix.lock file hasn’t changed, and if the _build folder is cached, then I would hope that deps.compile would be very fast or could even be skipped.

But that isn’t the case right now. :cry:

Further steps

There are further steps for static code analysis and running the tests (which incidentally takes less time than compiling dependencies). I’m going to stop here because it’s the compilation that I’m interested in.

If you have any suggestions, please let me know. Also happy to open a bug with Elixir if you think it’s a regression.

Marked As Solved

nathany

nathany

After experimenting with the previously linked stand-alone repository, I made the following changes:

  • Combine Elixir & JS tests into one job. This reduces likelihood of contention on the cache, which can result in a “another job may be creating this cache” error (our JS tests need Elixir & Phoenix).
  • Remove AppSignal recompile fix :crossed_fingers:t2:
  • Don’t compile dependencies separately, as this always recompiles Erlang deps
  • Rev GitHub Actions cache to build a fresh cache

With the cache working properly, the compile step is taking around 20 seconds instead of 3.5 minutes or so.

Thanks again for the assistance and examples,
Nathan.

Also Liked

josevalim

josevalim

Creator of Elixir

There is also mix loadpaths, which is actually what is called by mix compile. It is a public task although it does not show up on mix help. It won’t compile the current app but it will check and compile missing dependencies. Another option is to provide something like mix compile --only-deps.

josevalim

josevalim

Creator of Elixir

mix deps.compile will attempt to compile all dependencies. Rebar3 logs, but the truth is that all dependencies are being checked, which does not occur on mix compile. So unless someone contributes a mix deps.compile --only-stale or something of sorts, a mix deps.compile step will always do more work than necessary.

josevalim

josevalim

Creator of Elixir

If you can isolate this error into a smaller app with reproduction steps, I will be glad to take a look at it!

aifrak

aifrak

I was curious so I tried to play with your repository example but I could not reproduce the issue.

However, I think I have found the cause: the cache is not created yet when the next workflow is trying to access it. It is more a Github Action issue.

Commit 4fa8fe4 pushed at 11 Feb 2022, 22:22 GMT:

Commit 206f14b pushed at 11 Feb 2022, 22:25 GMT:

Both are trying to get a cache with the key Linux-mix-ca19570116549225dad4bcc705d9f4fe0f00a3bca16e60d7e730c353018faeb2. But the first workflow has not finished to create the cache. Not the end of the message “another job may be creating this cache.”

Potential solution:

  1. Split caches between deps and _build
  2. Cache key only for deps should be end with ${{ hashFiles('**/mix.lock') }}
  3. Cache key only for _build should be end with ${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
  4. (optional) Adapt the glob patterns in hashFiles for your needs

Here an example of the final result:

      - name: Cache Elixir deps
        id: elixir-deps-cache
        uses: actions/cache@v2.1.7
        with:
          path: |
            **/deps
          key: |
            ${{ runner.os }}-elixir-deps-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            ${{ runner.os }}-elixir-deps-
      - name: Cache Elixir build
        id: elixir-build-cache
        uses: actions/cache@v2.1.7
        with:
          path: |
            **/_build
          key: |
            ${{ runner.os }}-elixir-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
          restore-keys: |
            ${{ runner.os }}-elixir-build-${{ hashFiles('**/mix.lock') }}-
            ${{ runner.os }}-elixir-build-

I don’t remember where I have seen this, but the reason is that the cache from deps should depend on changes from mix.lock only. Whereas _build should depend on changes from “the rest” (application code, config, …). For _build cache, a new hash will be generated each time lib, config or mix.exs are changed, which will avoid potential conflicts between workflows when getting/creating a cache key.

josevalim

josevalim

Creator of Elixir

I can reproduce mix deps.compile recompiling Erlang dependencies on Elixir v1.13. But it doesn’t recompile Elixir deps and it doesn’t recompile when running mix compile. Does this match the behaviour you see?

EDIT: to be clear, it doesn’t recompile Erlang dependencies. It invokes Rebar3 but it is mostly a no-op.

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New

We're in Beta

About us Mission Statement