Skip to content

Commit

Permalink
Use /run as the default for PID/UNIX-domain sockets (#13)
Browse files Browse the repository at this point in the history
* Use `/run` as the default for PID/UNIX-domain sockets

* Keep DEPUTY_IPC_DIR for testing

* Set project version to 0.1.4

* Drop instructions on using DEPUTY_IPC_DIR

* Formatting

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Use socket extension

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
omus and github-actions[bot] authored Aug 7, 2024
1 parent 973acac commit b62e185
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 61 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "K8sDeputy"
uuid = "2481ae95-212f-4650-bb21-d53ea3caf09f"
authors = ["Beacon Biosignals, Inc"]
version = "0.1.3"
version = "0.1.4"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
12 changes: 5 additions & 7 deletions docs/src/graceful_termination.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Finally, the entrypoint for the container should also not directly use the Julia

### Read-only Filesystem

If you have a read-only filesystem on your container you'll need to configure a writeable volume mount for K8sDeputy.jl. The `DEPUTY_IPC_DIR` environmental variable can be used to instruct K8sDeputy.jl where to store the named pipes it creates for interprocess communication:
If you have a read-only filesystem on your container you'll need to configure a writeable `/run` directory for K8sDeputy.jl so it can create UNIX-domain sockets for interprocess communication:

```yaml
apiVersion: v1
Expand All @@ -78,20 +78,18 @@ spec:
containers:
- name: app
# command: ["/bin/sh", "-c", "julia entrypoint.jl; sleep 1"]
env:
- name: DEPUTY_IPC_DIR
value: /mnt/deputy-ipc
lifecycle:
preStop:
exec:
command: ["julia", "-e", "using K8sDeputy; graceful_terminate()"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /mnt/deputy-ipc
name: deputy-ipc
- name: ipc
mountPath: /run
subPath: app # Set the `subPath` to the container name to ensure per-container isolation
volumes:
- name: deputy-ipc
- name: ipc
emptyDir:
medium: Memory
```
10 changes: 4 additions & 6 deletions docs/src/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ For users who want to get started quickly you can use the following template to
containers:
- name: app
command: ["/bin/sh", "-c", "julia entrypoint.jl; sleep 1"]
env:
- name: DEPUTY_IPC_DIR
value: /mnt/deputy-ipc
ports:
- name: health-check
containerPort: 8081 # Default K8sDeputy.jl heath check port
Expand All @@ -54,11 +51,12 @@ For users who want to get started quickly you can use the following template to
- all
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /mnt/deputy-ipc
name: deputy-ipc
- name: ipc
mountPath: /run
subPath: app # Set the `subPath` to the container name to ensure per-container isolation
terminationGracePeriodSeconds: 30
volumes:
- name: deputy-ipc
- name: ipc
emptyDir:
medium: Memory
```
34 changes: 15 additions & 19 deletions src/graceful_termination.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,21 @@
# from experimenting with this there are a few issues such as being unable to use locks or
# printing (`jl_safe_printf` does work).

# Linux typically stores PID files in `/run` which requires root access. For systems with
# read-only file systems we need to support a user specified writable volume.
_deputy_ipc_dir() = get(tempdir, ENV, "DEPUTY_IPC_DIR")
# Linux stores PID files and UNIX-domain sockets in `/run`. Users with K8s containers
# utilizing read-only file systems should make use of a volume mount to allow K8sDeputy.jl
# to write to `/run`. Users can change the IPC directory by specifying `DEPUTY_IPC_DIR` but
# this is mainly just used for testing.
_deputy_ipc_dir() = get(ENV, "DEPUTY_IPC_DIR", "/run")

# Prefer using UNIX domain sockets but if the `DEPUTY_IPC_DIR` is set assume the file
# system is read-only and use a named pipe instead.
# Write transient UNIX-domain sockets to the IPC directory.
function _graceful_terminator_socket_path(pid::Int32)
name = "graceful-terminator.$pid"
return haskey(ENV, "DEPUTY_IPC_DIR") ? joinpath(_deputy_ipc_dir(), name) : name
return joinpath(_deputy_ipc_dir(), "graceful-terminator.$pid.socket")
end

# Following the Linux convention for pid files:
# https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch03s15.html
entrypoint_pid_file() = joinpath(_deputy_ipc_dir(), "julia-entrypoint.pid")
function set_entrypoint_pid(pid::Integer)
file = entrypoint_pid_file()
mkpath(dirname(file))
return write(file, string(pid) * "\n")
end
set_entrypoint_pid(pid::Integer) = write(entrypoint_pid_file(), string(pid) * "\n")

function entrypoint_pid()
pid_file = entrypoint_pid_file()
Expand Down Expand Up @@ -85,15 +81,15 @@ process and the `preStop` process to cleanly terminate.
function graceful_terminator(f; set_entrypoint::Bool=true)
set_entrypoint && set_entrypoint_pid(getpid())

# Utilize UNIX domain sockets or named pipes for the IPC. Avoid using network sockets
# here as we don't want to allow access to this functionality from outside of the
# localhost. Each process uses a distinct socket name allowing for multiple Julia
# processes to allow independent use of the graceful terminator.
# Utilize UNIX-domain sockets (Linux) or named pipes (Windows) for the IPC. Avoid using
# network sockets here as we don't want to allow access to this functionality from
# outside of the localhost. Each process uses a distinct socket name allowing for
# multiple Julia processes to allow independent use of the graceful terminator.
socket_path = _graceful_terminator_socket_path(getpid())

# Remove any pre-existing named pipe as otherwise this will cause our `listen` call to
# fail. Should be safe to remove this file as it has been reserved for this PID. Only
# should be needed in the scenario where the K8s pod has been restarted and the
# Remove any pre-existing UNIX-domain socket as otherwise this will cause our `listen`
# call to fail. Should be safe to remove this file as it has been reserved for this PID.
# Only should be needed in the scenario where the K8s pod has been restarted and the
# location of the socket exists in a K8s volume.
ispath(socket_path) && rm(socket_path)

Expand Down
63 changes: 36 additions & 27 deletions test/graceful_termination.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@testset "graceful_terminator" begin
deputy_ipc_dir = mktempdir()

@testset "Julia entrypoint" begin
code = quote
using K8sDeputy
Expand All @@ -12,6 +14,7 @@
end

cmd = `$(Base.julia_cmd()) --color=no -e $code`
cmd = addenv(cmd, "DEPUTY_IPC_DIR" => deputy_ipc_dir)
buffer = IOBuffer()
p = run(pipeline(cmd; stdout=buffer, stderr=buffer); wait=false)
@test timedwait(() -> process_running(p), Second(5)) === :ok
Expand All @@ -21,7 +24,9 @@

# When no PID is passed in the process ID is read from the Julia entrypoint file.
# Blocks untils the process terminates.
@test graceful_terminate() === nothing
withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
@test graceful_terminate() === nothing
end

@test process_exited(p)
@test p.exitcode == 2
Expand All @@ -47,6 +52,7 @@
end

cmd = `$(Base.julia_cmd()) --color=no -e $code`
cmd = addenv(cmd, "DEPUTY_IPC_DIR" => deputy_ipc_dir)
buffer1 = IOBuffer()
buffer2 = IOBuffer()
p1 = run(pipeline(cmd; stdout=buffer1, stderr=buffer1); wait=false)
Expand All @@ -57,8 +63,10 @@
sleep(3)

# Blocks untils the process terminates
@test graceful_terminate(getpid(p1)) === nothing
@test graceful_terminate(getpid(p2)) === nothing
withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
@test graceful_terminate(getpid(p1)) === nothing
@test graceful_terminate(getpid(p2)) === nothing
end
@test process_exited(p1)
@test process_exited(p2)

Expand All @@ -73,10 +81,9 @@
end

# When users set `DEPUTY_IPC_DIR` they may be using a K8s volume. As even `emptyDir`
# volumes persist for the lifetime of the pod we may have a named pipe already present
# from a previous restart.
# volumes persist for the lifetime of the pod we may have a UNIX-domain socket already
# present from a previous restart.
@testset "bind after restart" begin
deputy_ipc_dir = mktempdir()
code = quote
using K8sDeputy
using Sockets: listen
Expand All @@ -99,31 +106,33 @@
end

cmd = `$(Base.julia_cmd()) --color=no -e $code`
cmd = addenv(cmd, "DEPUTY_IPC_DIR" => deputy_ipc_dir)
buffer = IOBuffer()
p = run(pipeline(cmd; stdout=buffer, stderr=buffer); wait=false)
@test timedwait(() -> process_running(p), Second(5)) === :ok

withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
buffer = IOBuffer()
p = run(pipeline(cmd; stdout=buffer, stderr=buffer); wait=false)
@test timedwait(() -> process_running(p), Second(5)) === :ok

# Allow some time for Julia to startup and the graceful terminator to be registered.
sleep(3)
# Allow some time for Julia to startup and the graceful terminator to be registered.
sleep(3)

# Socket exists as a named pipe
socket_path = K8sDeputy._graceful_terminator_socket_path(getpid(p))
@test ispath(socket_path)
@test !isfile(socket_path)
# Socket exists as a UNIX-domain socket
socket_path = withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
return K8sDeputy._graceful_terminator_socket_path(getpid(p))
end
@test ispath(socket_path)
@test !isfile(socket_path)

# Blocks untils the process terminates
# Blocks untils the process terminates
withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
@test graceful_terminate(getpid(p)) === nothing
@test process_exited(p)
@test p.exitcode == 2

output = String(take!(buffer))
expected = """
[ Info: GRACEFUL TERMINATION HANDLER
[ Info: SHUTDOWN COMPLETE
"""
@test output == expected
end
@test process_exited(p)
@test p.exitcode == 2

output = String(take!(buffer))
expected = """
[ Info: GRACEFUL TERMINATION HANDLER
[ Info: SHUTDOWN COMPLETE
"""
@test output == expected
end
end
6 changes: 5 additions & 1 deletion test/health.jl
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ end
end

@testset "graceful termination" begin
deputy_ipc_dir = mktempdir()
port = rand(EPHEMERAL_PORT_RANGE)
code = quote
using K8sDeputy, Sockets
Expand All @@ -211,6 +212,7 @@ end
end

cmd = `$(Base.julia_cmd()) --color=no -e $code`
cmd = addenv(cmd, "DEPUTY_IPC_DIR" => deputy_ipc_dir)
buffer = IOBuffer()
p = run(pipeline(cmd; stdout=buffer, stderr=buffer); wait=false)
@test timedwait(() -> process_running(p), Second(5)) === :ok
Expand All @@ -220,7 +222,9 @@ end
end === :ok

# Blocks untils the process terminates
graceful_terminate(getpid(p))
withenv("DEPUTY_IPC_DIR" => deputy_ipc_dir) do
return graceful_terminate(getpid(p))
end
@test process_exited(p)
@test p.exitcode == 1

Expand Down

2 comments on commit b62e185

@kleinschmidt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/112604

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.4 -m "<description of version>" b62e1858a4083ffc8f9f7b10fcb60a77896ae13e
git push origin v0.1.4

Please sign in to comment.