Detecting "sibling" nix-shells for a given project

I wrote a shell.nix for my project, and it uses shellHook to initialize/start a postgres server when you enter the shell, and shutdown postgres when you exit.

shellHook = ''
  export PGDATA=$PWD/nix/pgdata \
         PGPORT=5433

  trap 'pg_ctl stop --silent -W' EXIT

  if [ ! -d $PGDATA ]; then
      pg_ctl initdb --silent -o '--auth=trust'
      setsid pg_ctl start --silent -w --log $PWD/log/pg.log
  else
      setsid pg_ctl start --silent --log $PWD/log/pg.log
  fi
'';

If I start a second nix-shell when there’s one already running (in a different terminal), pg_ctl start complains that postgres is already running. This was just a minor annoyance, but was easy enough to fix:

pg_ctl status >/dev/null
if (( $? == 3 )); then
    setsid pg_ctl start --silent --log $PWD/log/pg.log
fi

The more interesting problem is fixing the shutdown logic. If I have two nix-shells running, whichever one exits first will shutdown postgres, leaving the other without a database.

Solutions I’m considering:

  • pid files in a pids/ directory
    • on entering nix-shell, create pids/$$
    • on exiting, remove pids/$$ and check for any remaining files in pids/
  • use postgres to maintain a table of pids
    • on entering nix-shell, insert pid
    • on exiting, delete pid and check for any remaining rows in the table
  • use ps to query for nix-shell processes
    • I’d need a way to distinguish which directory the nix-shell processes were run from

Or maybe there’s another approach I’m overlooking? I wonder if there’s a way to leverage process groups or sessions to register the relationship between these shells.

2 Likes

I’ve written something like this for Hydra once, the solution I’ve choosen is a semaphore that uses the PID of the shells as the key. That way when the last shell stops, the database is also shut down and if another shell is already running with a database, it’s not started again. So it’s similar to your first point.

Here is the code:

2 Likes

Cool, thanks for sharing. It looks like we ran into some of the same issues. I like your use of PGDATABASE too, I didn’t know about that.

I ended up implementing the first two solutions in order to compare. The first one was essentially the same as your semaphores . I like your $(find "$hydraDevDir/sema" -prune -empty), btw. The second was pretty straightforward too – I maintain a table of pids in postgres, rather than a directory of pidfiles on the filesystem, but the end result is the same. I liked the fact that this avoided the need to create a pids directory.

I haven’t found anything to argue for one approach over the other. It’s nice to have options, I guess : )

I also ran into the Control-C thing. I was solving it with setsid, though I’d overlooked setsid's -w option. In one spot, I had:

setsid pg_ctl start --silent -w --log "path/to/log" &&
    createuser --createdb postgres        

So despite telling pg_ctl to wait (-w), the setsid call would return immediately and createuser could be invoked before the server’s up. I never actually ran into this, though.

I also overlooked the fact that setsid isn’t available on macOS. I found another solution that doesn’t require setsid:

set -m
pg_ctl start ...

I’m still mystified as to why this works, since the m shell option is already on according to $-. I ended up running it in a subshell just in case.

(set -m
pg_ctl start --silent -w --log "path/to/log") &&
    createuser --createdb postgres        

I also noticed you’re using Unix domain sockets, rather than TCP sockets. It sounds like that might be more performant on localhost so I’m going to try it.

1 Like

Hm, that’s weird, because all which set -m does is setting SHELLOPTS accordingly, no other magic involved from reading the source, so it shouldn’t make any difference when monitor is already set.