Skip to content

Commit 163c032

Browse files
authored
Distributed: Propagate package environment to local workers. (#43270)
Local workers now inherit the package environment of the main process, i.e. the active project, LOAD_PATH, and DEPOT_PATH. This behavior can be overridden by passing the new `env` keyword argument, or by passing `--project` in the `exeflags` keyword argument. Fixes #28781, and closes #42089.
1 parent 6b29ebd commit 163c032

File tree

4 files changed

+149
-5
lines changed

4 files changed

+149
-5
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ Standard library changes
6464

6565
#### Distributed
6666

67+
* The package environment (active project, `LOAD_PATH`, `DEPOT_PATH`) are now propagated
68+
when adding *local* workers (e.g. with `addprocs(N::Int)` or through the `--procs=N`
69+
command line flag) ([#43270]).
70+
* `addprocs` for local workers now accept the `env` keyword argument for passing
71+
environment variables to the workers processes. This was already supported for
72+
remote workers ([#43270]).
73+
6774
#### UUIDs
6875

6976
#### Mmap

stdlib/Distributed/src/cluster.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ default_addprocs_params() = Dict{Symbol,Any}(
533533
:dir => pwd(),
534534
:exename => joinpath(Sys.BINDIR, julia_exename()),
535535
:exeflags => ``,
536+
:env => [],
536537
:enable_threaded_blas => false,
537538
:lazy => true)
538539

stdlib/Distributed/src/managers.jl

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ It is possible to launch multiple processes on a remote host by using a tuple in
6565
workers to be launched on the specified host. Passing `:auto` as the worker count will
6666
launch as many workers as the number of CPU threads on the remote host.
6767
68-
Examples:
68+
**Examples**:
6969
```julia
7070
addprocs([
7171
"remote1", # one worker on 'remote1' logging in with the current username
@@ -76,7 +76,7 @@ addprocs([
7676
])
7777
```
7878
79-
Keyword arguments:
79+
**Keyword arguments**:
8080
8181
* `tunnel`: if `true` then SSH tunneling will be used to connect to the worker from the
8282
master process. Default is `false`.
@@ -445,10 +445,17 @@ end
445445
446446
Launch `np` workers on the local host using the in-built `LocalManager`.
447447
448-
*Keyword arguments:*
448+
Local workers inherit the current package environment (i.e., active project,
449+
[`LOAD_PATH`](@ref), and [`DEPOT_PATH`](@ref)) from the main process.
450+
451+
**Keyword arguments**:
449452
- `restrict::Bool`: if `true` (default) binding is restricted to `127.0.0.1`.
450-
- `dir`, `exename`, `exeflags`, `topology`, `lazy`, `enable_threaded_blas`: same effect
453+
- `dir`, `exename`, `exeflags`, `env`, `topology`, `lazy`, `enable_threaded_blas`: same effect
451454
as for `SSHManager`, see documentation for [`addprocs(machines::AbstractVector)`](@ref).
455+
456+
!!! compat "Julia 1.9"
457+
The inheriting of the package environment and the `env` keyword argument were
458+
added in Julia 1.9.
452459
"""
453460
function addprocs(np::Integer=Sys.CPU_THREADS; restrict=true, kwargs...)
454461
manager = LocalManager(np, restrict)
@@ -463,10 +470,32 @@ function launch(manager::LocalManager, params::Dict, launched::Array, c::Conditi
463470
exename = params[:exename]
464471
exeflags = params[:exeflags]
465472
bind_to = manager.restrict ? `127.0.0.1` : `$(LPROC.bind_addr)`
473+
env = Dict{String,String}(params[:env])
474+
475+
# TODO: Maybe this belongs in base/initdefs.jl as a package_environment() function
476+
# together with load_path() etc. Might be useful to have when spawning julia
477+
# processes outside of Distributed.jl too.
478+
# JULIA_(LOAD|DEPOT)_PATH are used to populate (LOAD|DEPOT)_PATH on startup,
479+
# but since (LOAD|DEPOT)_PATH might have changed they are re-serialized here.
480+
# Users can opt-out of this by passing `env = ...` to addprocs(...).
481+
pathsep = Sys.iswindows() ? ";" : ":"
482+
if get(env, "JULIA_LOAD_PATH", nothing) === nothing
483+
env["JULIA_LOAD_PATH"] = join(LOAD_PATH, pathsep)
484+
end
485+
if get(env, "JULIA_DEPOT_PATH", nothing) === nothing
486+
env["JULIA_DEPOT_PATH"] = join(DEPOT_PATH, pathsep)
487+
end
488+
# Set the active project on workers using JULIA_PROJECT.
489+
# Users can opt-out of this by (i) passing `env = ...` or (ii) passing
490+
# `--project=...` as `exeflags` to addprocs(...).
491+
project = Base.ACTIVE_PROJECT[]
492+
if project !== nothing && get(env, "JULIA_PROJECT", nothing) === nothing
493+
env["JULIA_PROJECT"] = project
494+
end
466495

467496
for i in 1:manager.np
468497
cmd = `$(julia_cmd(exename)) $exeflags --bind-to $bind_to --worker`
469-
io = open(detach(setenv(cmd, dir=dir)), "r+")
498+
io = open(detach(setenv(addenv(cmd, env), dir=dir)), "r+")
470499
write_cookie(io)
471500

472501
wconfig = WorkerConfig()

stdlib/Distributed/test/distributed_exec.jl

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,113 @@ for p in procs()
17391739
@test @fetchfrom(p, i27429) == 27429
17401740
end
17411741

1742+
# Propagation of package environments for local workers (#28781)
1743+
let julia = `$(Base.julia_cmd()) --startup-file=no`; mktempdir() do tmp
1744+
project = mkdir(joinpath(tmp, "project"))
1745+
depots = [mkdir(joinpath(tmp, "depot1")), mkdir(joinpath(tmp, "depot2"))]
1746+
load_path = [mkdir(joinpath(tmp, "load_path")), "@stdlib", "@"]
1747+
pathsep = Sys.iswindows() ? ";" : ":"
1748+
env = Dict(
1749+
"JULIA_DEPOT_PATH" => join(depots, pathsep),
1750+
"JULIA_LOAD_PATH" => join(load_path, pathsep),
1751+
)
1752+
setupcode = """
1753+
using Distributed, Test
1754+
@everywhere begin
1755+
depot_path() = DEPOT_PATH
1756+
load_path() = LOAD_PATH
1757+
active_project() = Base.ACTIVE_PROJECT[]
1758+
end
1759+
"""
1760+
testcode = setupcode * """
1761+
for w in workers()
1762+
@test remotecall_fetch(depot_path, w) == DEPOT_PATH
1763+
@test remotecall_fetch(load_path, w) == LOAD_PATH
1764+
@test remotecall_fetch(Base.load_path, w) == Base.load_path()
1765+
@test remotecall_fetch(active_project, w) == Base.ACTIVE_PROJECT[]
1766+
@test remotecall_fetch(Base.active_project, w) == Base.active_project()
1767+
end
1768+
"""
1769+
# No active project
1770+
extracode = """
1771+
for w in workers()
1772+
@test remotecall_fetch(active_project, w) === Base.ACTIVE_PROJECT[] === nothing
1773+
end
1774+
"""
1775+
cmd = setenv(`$(julia) -p1 -e $(testcode * extracode)`, env)
1776+
@test success(cmd)
1777+
# --project
1778+
extracode = """
1779+
for w in workers()
1780+
@test remotecall_fetch(active_project, w) == Base.ACTIVE_PROJECT[] ==
1781+
$(repr(project))
1782+
end
1783+
"""
1784+
cmd = setenv(`$(julia) --project=$(project) -p1 -e $(testcode * extracode)`, env)
1785+
@test success(cmd)
1786+
# JULIA_PROJECT
1787+
cmd = setenv(`$(julia) -p1 -e $(testcode * extracode)`,
1788+
(env["JULIA_PROJECT"] = project; env))
1789+
@test success(cmd)
1790+
# Pkg.activate(...)
1791+
activateish = """
1792+
Base.ACTIVE_PROJECT[] = $(repr(project))
1793+
using Distributed
1794+
addprocs(1)
1795+
"""
1796+
cmd = setenv(`$(julia) -e $(activateish * testcode * extracode)`, env)
1797+
@test success(cmd)
1798+
# JULIA_(LOAD|DEPOT)_PATH
1799+
shufflecode = """
1800+
d = reverse(DEPOT_PATH)
1801+
append!(empty!(DEPOT_PATH), d)
1802+
l = reverse(LOAD_PATH)
1803+
append!(empty!(LOAD_PATH), l)
1804+
"""
1805+
addcode = """
1806+
using Distributed
1807+
addprocs(1) # after shuffling
1808+
"""
1809+
extracode = """
1810+
for w in workers()
1811+
@test remotecall_fetch(load_path, w) == $(repr(reverse(load_path)))
1812+
@test remotecall_fetch(depot_path, w) == $(repr(reverse(depots)))
1813+
end
1814+
"""
1815+
cmd = setenv(`$(julia) -e $(shufflecode * addcode * testcode * extracode)`, env)
1816+
@test success(cmd)
1817+
# Mismatch when shuffling after proc addition
1818+
failcode = shufflecode * setupcode * """
1819+
for w in workers()
1820+
@test remotecall_fetch(load_path, w) == reverse(LOAD_PATH) == $(repr(load_path))
1821+
@test remotecall_fetch(depot_path, w) == reverse(DEPOT_PATH) == $(repr(depots))
1822+
end
1823+
"""
1824+
cmd = setenv(`$(julia) -p1 -e $(failcode)`, env)
1825+
@test success(cmd)
1826+
# Passing env or exeflags to addprocs(...) to override defaults
1827+
envcode = """
1828+
using Distributed
1829+
project = mktempdir()
1830+
env = Dict(
1831+
"JULIA_LOAD_PATH" => LOAD_PATH[1],
1832+
"JULIA_DEPOT_PATH" => DEPOT_PATH[1],
1833+
)
1834+
addprocs(1; env = env, exeflags = `--project=\$(project)`)
1835+
env["JULIA_PROJECT"] = project
1836+
addprocs(1; env = env)
1837+
""" * setupcode * """
1838+
for w in workers()
1839+
@test remotecall_fetch(depot_path, w) == [DEPOT_PATH[1]]
1840+
@test remotecall_fetch(load_path, w) == [LOAD_PATH[1]]
1841+
@test remotecall_fetch(active_project, w) == project
1842+
@test remotecall_fetch(Base.active_project, w) == joinpath(project, "Project.toml")
1843+
end
1844+
"""
1845+
cmd = setenv(`$(julia) -e $(envcode)`, env)
1846+
@test success(cmd)
1847+
end end
1848+
17421849
include("splitrange.jl")
17431850

17441851
# Run topology tests last after removing all workers, since a given

0 commit comments

Comments
 (0)