Absinthe goes into infinite loop^H^H^H^Hrecursion

Hi,

Is this a bug in Absinthe I’ve just ran into? Here’s an SDL schema:

    interface A { bs: [B!]!  }
    interface B { a: A!  }
    
    type A1 implements A { bs: [B!]! }
    type A2 implements A { bs: [B!]! }  # the same as A1 for now
    
    type B1 implements B { a: A! }
    type B2 implements B { a: A! }  # this is the same as B1

This is a perfectly fine, valid SDL, and Absinthe is able to use it. So far so good.

We have two interfaces here called A and B. The types implementing interface A have to have a field called bs containing a list of values of any type implementing B. The types implementing interface B have to have a field called a containing a value of a type that implements A. We also have four types that implement these interfaces. A1 and A2 implement interface A, while B1 and B2 implement interface B.

At this point A1 and A2, just like B1 and B2 have the exact same SDL definitions. Let’s play around with the idea that A1 has to do something with B1 and also that A2 has to do something with B2:

Let’s say that the field a of A1 can be specified more accurately. Instead of just containing Bs in general, we know it always holds B1 values. The same goes for the relationship of A2 and B2, so let’s replace the identical definition with this more specific one:

    type A1 implements A { bs: [B1!]! }
    type A2 implements A { bs: [B2!]! }

This is also a valid SDL, and Absinthe deals with it. B1 and B2 both implement B, so the interface requirement is fulfilled. bs holds values of types implementing interface B.

Just like when we undo the above change, and replace the definition of B1 and B2 with this:

    type B1 implements B { a: A1! }
    type B2 implements B { a: A2! }

This is also valid, because A1 and A2 implement A, so the interface that B1 and B2 implement (that is interface B) is satisfied.

The problem comes up when both the above modifications are done, like this:

    interface A { bs: [B!]!  }
    interface B { a: A!  }

    type A1 implements A { bs: [B1!]! }
    type A2 implements A { bs: [B2!]! }

    type B1 implements B { a: A1! }
    type B2 implements B { a: A2! }

Absinthe cannot compile the schema module with an SDL like this. It just eats up the memory of the machine and then the OS kills the compilation process. (I bet this is due to mutually referencing types.)

This might seem like an artificial problem, but actually this was discovered in a real life application with more meaningful types and interfaces that these As and Bs. The only workaround I found is that we only replace interfaces with types from one direction, not both.

Is this an Absinthe problem or this is something impossible to resolve in general?

Is there any way we can keep the specific types (instead of the generic interfaces) in our schema?

1 Like

Hey @sandorbedo Absinthe should always either compile happily or return an error, never recurse infinitely. Please feel free to file an issue. It will help a lot if you can present a complete runnable module or script just to make reproducing this very easy.

1 Like

Here’s how to reproduce it in a minimalist way:

    $ mix new infinite_recursion
    $ cd infinite_recursion/

Add {:absinthe, "~> 1.7.6"} to the deps function in mix.exs and then create a schema module:

    $ cat > lib/schema.ex <<EOF
    defmodule Schema do
      use Absinthe.Schema
    
      import_sdl """
        schema {
          query: Query
        }
    
        type Query {
          a1: A1!
          b1: B1!
        }
    
        interface A { bs: [B!]!  }
        interface B { a: A!  }
    
        type A1 implements A { bs: [B1!]! }
        type A2 implements A { bs: [B2!]! }
    
        type B1 implements B { a: A1! }
        type B2 implements B { a: A2! }
      """
    end
    EOF

Then get the deps and compile them.

    $ mix deps.get
    $ mix deps.compile

Here comes the problem, when you try to compile this app:

    $ MIX_DEBUG=1 mix compile --verbose ; echo "Exit code was $?"

    -> Running mix loadconfig (inside InfiniteRecursion.MixProject)
    <- Ran mix loadconfig in 0ms
    -> Running mix compile --verbose (inside InfiniteRecursion.MixProject)
    -> Running mix loadpaths --verbose (inside InfiniteRecursion.MixProject)
    -> Running mix archive.check --verbose (inside InfiniteRecursion.MixProject)
    <- Ran mix archive.check in 0ms
    -> Running mix deps.loadpaths --verbose (inside InfiniteRecursion.MixProject)
    <- Ran mix deps.loadpaths in 53ms
    <- Ran mix loadpaths in 54ms
    -> Running mix compile.all --verbose (inside InfiniteRecursion.MixProject)
    -> Running mix compile.yecc --verbose (inside InfiniteRecursion.MixProject)
    <- Ran mix compile.yecc in 1ms
    -> Running mix compile.leex --verbose (inside InfiniteRecursion.MixProject)
    <- Ran mix compile.leex in 0ms
    -> Running mix compile.erlang --verbose (inside InfiniteRecursion.MixProject)
    <- Ran mix compile.erlang in 0ms
    -> Running mix compile.elixir --verbose (inside InfiniteRecursion.MixProject)
    Compiling 2 files (.ex)
    Compiled lib/infinite_recursion.ex
    
    Killed
    Exit code was 137

It waits a long time before the Killed line is printed. The line Compiled bin/schema.ex is never printed.

Tried on a linux machine (x86_64) with:

    Erlang/OTP  24.3.4
    Elixir      1.14.5 

and also with

    Erlang/OTP  26.2.1
    Elixir      1.16.0 

and then with

    Erlang/OTP  26.2.2
    Elixir      1.16.1 

The infinite compilation is the same with all versions.