Tinkering With Cross-Container Domino Addins
May 16, 2021, 1:35 PM
A good chunk of my work lately involves running distinct processes with a Domino runtime, either run from Domino or standalone for development or CI use. Something that had been percolating in the back of my mind was another step in this: running these "addin-ish" programs in Docker in a separate container from Domino, but participating in that active Domino runtime.
Domino addins in general are really just separate processes and, while they gain some special properties when run via load foo
on the console or OSLoadProgram
in the C API, that's not a hard requirement to getting a lot of things working.
I figured I could get this working and, armed with basically no knowledge about how this would work, I set out to try it.
Scaffolding
My working project at hand is a webapp run with the standard open-liberty
Docker images. Though I'm using that as a starting point, I had to bring in the Notes runtime. Whether you use the official Domino Docker images from Flexnet or build your own, the only true requirement is that it match the version used in the running server, since libnotes does a version check on init. My Dockerfile looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | FROM --platform=linux/amd64 open-liberty:beta USER root RUN useradd -u 1000 notes RUN chown -R notes /opt/ol RUN chown -R notes /logs # Bring in the Domino runtime COPY --from=domino-docker:V1200_03252021prod /opt/hcl/domino/notes/latest/linux /opt/hcl/domino/notes/latest/linux COPY --from=domino-docker:V1200_03252021prod /local/notesdata /local/notesdata # Bring in the Liberty app and configuration COPY --chown=notes:users /target/jnx-example-webapp.war /apps/ COPY --chown=notes:users config/* /config/ COPY --chown=notes:users exec.sh /opt/ RUN chmod +x /opt/exec.sh USER notes ENV LD_LIBRARY_PATH "/opt/hcl/domino/notes/latest/linux" ENV NotesINI "/local/notesdata/notes.ini" ENV Notes_ExecDirectory "/opt/hcl/domino/notes/latest/linux" ENV Directory "/local/notesdata" ENV PATH="${PATH}:/opt/hcl/domino/notes/latest/linux:/opt/hcl/domino/notes/latest/linux/res/C" EXPOSE 8080 8443 ENTRYPOINT ["/opt/exec.sh"] |
I'll get to the "exec.sh" business later, but the pertinent parts now are:
- Adding a
notes
user (to avoid permissions trouble with the data dir, if it comes up) - Tweaking the Liberty container's ownership to account for this
- Bringing in the Domino runtime
- Copying in my WAR file from the project and associated config files (common for Liberty containers)
- Setting environment variables to tell the app how to init
So far, that's largely the same as how I run standalone Notes-runtime-enabled apps that don't talk to Domino. The only main difference is that, instead of copying in an ID and notes.ini, I instead mount the data volume to this container as I do with the main Domino one.
Shared Memory
The big new hurdle here is getting the separate apps to participate in Domino's shared memory pool. Now, going in, I had a very vague notion of what shared memory is and an even vaguer one of how it works. Certainly, the name is straightforward, and I know it in Domino's case mostly as "the thing that stops Notes from launching after a crash sometimes", but I'd need to figure out some more to get this working. Is it entirely a filesystem thing, as the Notes problem implies? Is it an OS-level thing with true memory? Well, both, apparently.
Fortunately, Docker has this covered: the --ipc
flag for docker run
. It has two main modes: you can participate in the host's IPC pool (essentially like what a normal, non-contained process does) or join another container specifically. I opted for the latter, which involved changing both the Domino launch arguments.
For Domino, I added --ipc=shareable
to the argument list, basically registering it as an available host for other containers to glom on to.
For the separate app, I added --ipc=container:domino
, where "domino" is the name of the Domino container.
With those in place, the "addin" process was able to see Domino and do addin-type stuff, like adding a status line and calling AddinLogMessageText
to display a message on the server's console.
Great: this proved that it's possible. However, there were still a few show-stopping problems to overcome.
PIDs
From what I gather, Notes keeps track of processes sharing its memory by their reported process IDs. If you have a process that joins the pool and then exits (maybe only if it exits abruptly; I'm not sure) and then tries to rejoin with the same PID, it will fail on init with a complaint that the PID is already registered.
Normally, this isn't a problem, as the OS hands out distinct PIDs all the time. This is trouble with Docker, though: by default, in general, the direct process in a Docker container sees itself as PID 1, and will start as such each time. In the Domino container, its PID 1 is "start.sh", and that's still going, and it's not going to hear otherwise from some other process calling itself the same.
Fortunately, this was a quick fix: Docker's -pid
option. Though the documentation for this is uncharacteristically slight, it turns out that the syntax for my needs is the same as the last option. Thus: --pid=container:domino
. Once I set that, the running app got a distinct PID from the pool. That was pleasantly simple.
SIGTERM
And now we come to the toughest problem. As it turns out, dealing with SIGTERM - the signal sent by docker stop
- is a whole big deal in the Java world. I banged my head at this for a while, with most of the posts I've found being not quite applicable, not working at all for me, or technically working but only in an unsustainable way.
For whatever reason, the Open Liberty Docker image doesn't handle this terribly well - when given a SIGTERM order, it doesn't stop the servlet context before dying, which means the contextDestroyed
method in my ServletContextListener
(such as this one) doesn't fire.
In many webapp cases, this is fine, but Domino is extremely finicky when it comes to memory-sharing processes needing to exit cleanly. If a process calls NotesInit
but doesn't properly call NotesTerm
(and close all its Notes-enabled threads), the server panics and dies. This is... not great behavior, but it is what it is, and I needed to figure out how to work with it. Unfortunately, the Liberty Docker container wasn't doing me any favors.
One option is to use Runtime.getRuntime().addShutdownHook(...)
. This lets you specify a Thread
to execute when a SIGTERM is received, and it can work in some cases. It's a little shaky sometimes, though, and it's bad form to riddle otherwise-normal webapps with such things: ideally, even webapps that you intend to run in a container should be written such that they can participate in a normal multi-app environment.
What I ended up settling on was based on this blog post, which (like a number of others) uses a shell script as the main entrypoint. That's a common idiom in general, and Open Liberty's image does it, but its script doesn't account for this, apparently. I tweaked that post's shell script to use the Liberty start/stop commands and ended up with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/usr/bin/env bash set -x term_handler() { /opt/ol/helpers/runtime/docker-server.sh /opt/ol/wlp/bin/server stop exit 143; # 128 + 15 -- SIGTERM } trap 'kill ${!}; term_handler' SIGTERM /opt/ol/helpers/runtime/docker-server.sh /opt/ol/wlp/bin/server start defaultServer # echo the Liberty console tail -f /logs/console.log & while true do tail -f /dev/null & wait ${!} done |
Now, when I issue a docker stop
to the container, the script issues an orderly shutdown of the Liberty instance, which properly calls the contextDestroyed
method and allows my code to close down its ExecutorService
and call NotesTerm
. Better still, Domino keeps running without crashing!
Conclusion
My final docker run
scripts ended up being:
Domino
1 2 3 4 5 6 7 8 9 | docker run --name domino \ -d \ -p 1352:1352 \ -v notesdata:/local/notesdata \ -v notesmisc:/local/notesmisc \ --cap-add=SYS_PTRACE \ --ipc=shareable \ --restart=always \ iksg-domino-12beta3 |
Webapp
1 2 3 4 5 6 7 8 9 10 | docker build . -t example-webapp docker run --name example-webapp \ -it \ --rm \ -p 8080:8080 \ -v notesdata:/local/notesdata \ -v notesmisc:/local/notesmisc \ --ipc=container:domino \ --pid=container:domino \ example-webapp |
(Here, the webapp is run to be temporary and tied to the console, hence -it
, --rm
, and no -d
)
One nice thing to note is that there's nothing webapp- or Java-specific here. One of the nice things about Docker is that it removes a lot of the hurdles to running whatever-the-heck type of program you want, so long as there's a Linux Docker image for it. I just happen to default to Java webapps for basically everything nowadays. The above script could be tweaked to work with most anything: the original post had it working with a Node app.
Now, considering that I was starting from nearly scratch here, I certainly can't say whether this is a bulletproof setup or even a reasonable idea in general. Still, it seems to work, and that's good enough for me for now.