Deadlock between files

The context

I’m building my own library of code for dealing with ElasticSearch, using the Tirexs package. This is my first time heading deep into macros, dependencies, use, import and several other of the most advanced things Elixir offers.

For that I have define a Document struct which looks roughly like this, (/lib/data/document.ex)

defmodule Document do
  use Document.ORM
  import Document.Builder, only: [build_document: 2]
  import Document.Builder.Validations, only: [is_document_valid?: 1, collect_errors: 1]

  defstruct [valid?: false, errors: nil, fields: %{}]

  def something do
    # uses imported functions from Document.Builder and Documen.Builder.Validations
  end
end

The Document module then uses several other functions from the Document.ORM module which don’t seem to be the cause of the error.

The problem

My error is the following

Compilation failed because of a deadlock between files.
dataobj_1          | The following files depended on the following modules:
dataobj_1          | 
dataobj_1          |   web/controllers/document_controller.ex => Document
dataobj_1          |             lib/data/document/builder.ex => Document.Builder.AuxiliarBuilder
dataobj_1          |                     lib/data/document.ex => Document.Builder
dataobj_1          |      lib/data/document/orm/bulk/utils.ex => Document
dataobj_1          |    lib/data/document/builder/auxiliar.ex => Document

There’s a deadlock which I don’t know how to approach, I’m sure I’m doing something wrong.

The first dependency, the document_controller one occurs (I think) because it both refers to the module Document and the %Document struct in different places:

defmodule Data.DocumentController do
  use Data.Web, :controller

  def create(conn, %{"document" => document_params}) do
      {:ok, doc} = document_params 
        |> Document.new
      case Document.save(doc) do
        {:ok, record} ->
          conn
          |> put_status(201)
          |> render(Data.DocumentView, "document.json", payload: Document.find(record._id))
        {:error, map_of_errors} ->
          conn
          |> put_status(422)
          |> render(Data.ErrorView, "422.json", errors: map_of_errors)
      end
  end
  def update(conn, %{"id" => id, "document" => document}) do
    case Document.update(id, document) do
      %Document{valid?: false, errors: errors} ->
        conn
        |> put_status(422)
        |> render(Data.ErrorView, "422.json", errors: errors)
      docset ->
        conn
        |> put_status(200)
        |> render(Data.DocumentView, "update.json", payload: docset)
    end
  end

The other dependencies also refer to both the module and the struct, so I’m thinking the deadlock has to do with this. But I’m lost about what to do.

If there’s necessary I can share more code, but to start the topic I think it is enough.

Thanks in advance!

I’m not 100% sure, but it looks like you have a circular dependency problem, and that is what is causing the error. At first glance it seems that lib/data/document.ex depends on Document.Builder, then lib/data/document/builder.ex depends on Document.Builder.AuxiliarBuilder and finally lib/data/document/builder/auxiliar.ex depends on Document, completing your circle.

Fixing this dependency circle should fix the deadlock error.

1 Like

Yesl that’s exactly the error I’m getting. I do need to solve the dependency cycle, that’s what I’m asking: in which way I should order my structs and module definition to avoid this.

Consider putting your functions like new and save in a context module (e.g. Documents), so that the Document module only defines the struct itself. This way you separate the public API from the implementation details, and fix the cycle as a bonus.

2 Likes

Apparently the circular dependency was happening because of the definition of the struct, namely %Document, and confusion with the module name.

Moving the struct definition into its own separate file (first couple of lines here were the hint), and changing the module name from Document to Data.Document (%Data.Document) was enough to solve all circular dependencies.

I’m going to accept your answer since it was technically the same thing I did.

Right, I misunderstood the question then. My next suggestion was going to be exactly what @dom said. The Document module is handling too much, it should only be the struct definition, kind of like with Phoenix Contexts where you define the Ecto Schema in a module Document and then all the functions that actually do something with a Document would go on Documents.

I’m glad you got it solved.

1 Like

Nice advice about the Phoenix Contexts, thank you!

1 Like