Responding to an event generated by one app by creating a DB record in another

We have two Elixir applications, I’ll call them P and C. Conceptually C depends on P.

When a particular new record is created by the P application, it sends out a broadcast message within the Elixir ecosystem (using Swarm, but that detail shouldn’t be important). A process in the C application listens for that broadcast and creates an additional record in its repo that is specific to its application’s purposes.

In production, this seems to work fine, but it is wreaking havoc on our ExUnit tests, even though I’ve configured shared ownership of the repos in question.

What I think is happening is roughly this:

  1. The lifetime of any given tests that causes something to happen in P is definite and clearly wraps up in time.
  2. However, the side-effects of the action in P cause database trigger an action in the C repo that is likely to occur after the test’s lifetime has expired. This is essentially a race condition on the expiration of the test and thus the expiration of the ownership.
  3. It’s not really reasonable to have tests wait for the side effect in C because most tests aren’t testing that.

There are two alternative architectures for this problem that I’ve considered, both of which have (IMHO) downsides:

  1. Reverse the dependency, then P can call C and tell it (synchronously) to build the appropriate message. (Downside: This dependency is conceptually wrong.)
  2. Build some sort of configuration mechanism by which P can call a behaviour synchronously; C can then register itself with this mechanism and use that to build the message. (Downside: How to register the behaviour module ID in a world with immutable data?)

Of the two, I definitely prefer #2, but I haven’t figured out how to implement it reliably.

Any advice? Any approaches I should consider, but haven’t listed?

Thanks much …

2 Likes

Shooting from the hip here - how about stopping “test_p” from exiting with a receive/1 (with a suitable timeout)?

  • When running standalone test_p can simply (at the beginnng) send a message to itself with Kernel.send/2 to cause itself to exit.
  • When running coordinated test_p can spawn_link a relay process and Process.register/2 it under a known name like :relay_p_c.
  • test_c can use Process.whereis/2 at the end (any earlier and the relay process may not be registered) to determine whether it is running in coordinated mode. In that case it sends a message to the relay process that test_c is complete. The relay process then emit’s a message to test_p to let it terminate (the link will take the relay process down when test_p terminates).

Caveats:

  • There may be some tweaking necessary to get this to work reliably - like “pausing” test_p (Process.sleep/1) until the relay process is visibly registered.
  • It may be possible to simply register the test_p process under a known name - making it unnecessary to spawn a separate relay process.
  • There may be an entirely simpler approach to deal with this type of problem.

If all this is a terrible idea I’m sure somebody will chime in.

2 Likes