23

I'm having trouble understanding the startup commands for the services in this docker-compose.yml. The two relevant lines from the .yml are:

command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

and

entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

Why send the sleep command to the background and then wait on it? Why not just do sleep 6h directly? Also, is the double dollar sign just escaping the dollar sign in ${!}?

I'm finding other places where sleep and wait are used in conjunction, but none seem to have any explanation of why:

  1. http://www.masteringunixshell.net/qa17/bash-how-to-wait-seconds.html
  2. https://stackoverflow.com/a/13301329/828584
  3. https://superuser.com/a/753984/98583
mowwwalker
  • 16,634
  • 25
  • 104
  • 157
  • see https://stackoverflow.com/questions/13296863/difference-between-wait-and-sleep – LinPy Jul 11 '19 at 05:24
  • 5
    @LinpPy, but none of those explain the point. What difference does it make to do `sleep 10 & wait ${!}` versus `sleep 10`. If you're just going to wait on the sleep command, why make it a background process? – mowwwalker Jul 11 '19 at 05:28
  • 2
    @Edvin no it isn't. That doesn't explain why you'd use a background sleep and a wait together, rather than just a foreground sleep. The asker already knows what each piece does individually, just not why you'd combine them like this. – Joseph Sible-Reinstate Monica Jul 11 '19 at 05:32
  • oh. I just got you – LinPy Jul 11 '19 at 05:40
  • The 3 examples you have listed in the question are using `sleep & wait` as an example; not real code. github link uses it as the actual code. – anishsane Jul 11 '19 at 05:58
  • 1
    Just a wild guess: The wait time is very long, so I don't think in normal circumstances, we would expect that the process **really** should wait for many hours. This means that in the normal case, someone (maybe the process which manages the dockers) has to do some work and if it is finished, it wants the process you want to run with docker-compose to **continue**. Letting the process continue can be done by killing the sleep process. – user1934428 Jul 11 '19 at 06:12

2 Answers2

9

It makes sense to sleep in background and then wait, when one wants to handle signals in a timely manner.

When bash is executing an external command in the foreground, it does not handle any signals received until the foreground process terminates

(detailed explanation here).

While the second example implements a signal handler, for the first one it makes no difference whether the sleep is executed in foreground or not. There is no trap and the signal is not propagated to the nginx process. To make it respond to the SIGTERM signal, the entrypoint should be something this:

/bin/sh -c 'nginx -g \"daemon off;\" & trap exit TERM; while :; do sleep 6h  & wait $${!}; nginx -s reload; done'

To test it:

docker run --name test --rm --entrypoint="/bin/sh" nginx  -c 'nginx -g "daemon off;" & trap exit TERM; while :; do sleep 20 & wait ${!}; echo running; done'

Stop the container

docker stop test

or send the TERM signal (docker stop sends a TERM followed by KILL if the main process does not exit)

docker kill --signal=SIGTERM test

By doing this, the scripts exits immediately. Now if we remove the wait ${!} the trap is executed when sleep ends. All that works well for the second example too.

Note: in both cases the intention is to check certificate renewal every 12h and reload the configuration every 6h as mentioned in the guide The two commands do that just fine. IMHO the additional wait in the first example is just an oversight of the developers.

EDITED:

It seems the rationalization above, which was meant to give possible reasons behind the background sleep, might create some confusion. (There is a related post Why use nginx with “daemon off” in background with docker?).

While the command suggested in the answer above is an improvement over the one in the question it is still flawed because, as mentioned in the linked post, the nginx server should be the main process and not a child. That can be easily achieved using the exec system call. The script becomes:

'while :; do sleep 6h; nginx -s reload; done & exec nginx -g "daemon off;"'

(More info in section Configure app as PID 1 in Docker best practices)

This, IMHO, is far better because not only is nginx monitored but it also handle signals. A configuration reload (nginx -s reload), for example, can also be done manually by simply sending the HUP signal to the docker container (See Controlling nginx).

b0gusb
  • 4,283
  • 2
  • 14
  • 33
  • If that's the case, doesn't that still hold true when `wait` is the foreground process and not `sleep`? – mowwwalker Jul 11 '19 at 14:31
  • 3
    `wait` is a [shell builtin](https://en.wikipedia.org/wiki/Shell_builtin) that can be interrupted. `sleep` instead is a command and is executed as an external process. The point of the sleep and wait is to make `sleep` interruptible. – b0gusb Jul 12 '19 at 10:54
4

The only reason I see:

If you killall -INT sleep, this won't affect main script.

Try this:

while true ;do sleep 12; echo yes;done

Then send a Interrupt signal:

killall -INT sleep

This will break the job!

Try now

while true ;do sleep 12 & wait $! ; echo yes;done

Then again:

killall -INT sleep

Job won't break!

Sample output, hitting killall -INT sleep from another window:

user@myhost:~$ while true ;do sleep 12; echo yes;done
break

user@myhost:~$ while true ;do sleep 12 & wait $! ; echo yes;done
[1] 30632
[1]+  Interrupt               sleep 12
yes
[1] 30636
[1]+  Interrupt               sleep 12
yes
[1] 30638
[1]+  Interrupt               sleep 12
yes
[1] 30640
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • I tried this in a script: `sleep 60; echo second line`. executed that script and did a `killall sleep` from another terminal. It did not kill script. I did get the echo for the second line. Maybe you are right, but perhaps, it is system dependent. – anishsane Jul 11 '19 at 06:01
  • @F.Hauri : anishsane is right: A kill of a process does not also kill the parent process. If it would, this would be pretty weird. – user1934428 Jul 11 '19 at 06:08
  • I tried to reproduce it, but I can't. With my tests, in both cases only the `sleep` is killed. BTW, even on your system, you should be able to do it without a `wait`: If you run the sleep in a subshell, i.e. `(sleep 12)`, does killall still kill your parent? – user1934428 Jul 11 '19 at 06:36
  • @user1934428 I'ts a feature of `INT` signal! You have to `killall -INT sleep`! – F. Hauri - Give Up GitHub Jul 11 '19 at 06:46
  • I killed using SIGINT, but as in your "background" case, I only get the message "Interrupted", and the loop continues. – user1934428 Jul 11 '19 at 08:56