diff --git a/Store/src/DataToolkitStore.jl b/Store/src/DataToolkitStore.jl index 00d8a40e..c55fe37f 100644 --- a/Store/src/DataToolkitStore.jl +++ b/Store/src/DataToolkitStore.jl @@ -10,6 +10,7 @@ using Compat @compat public load_inventory, fetch! +include("lockfile.jl") include("types.jl") const INVENTORY_VERSION = 0 diff --git a/Store/src/inventory.jl b/Store/src/inventory.jl index 1811b47b..3045a284 100644 --- a/Store/src/inventory.jl +++ b/Store/src/inventory.jl @@ -29,10 +29,12 @@ function load_inventory(path::String, create::Bool=true) for (key, val) in get(data, "collections", Dict{String, Any}[])] stores = map(s -> convert(StoreSource, s), get(data, "store", Dict{String, Any}[])) caches = map(c -> convert(CacheSource, c), get(data, "cache", Dict{String, Any}[])) - Inventory(file, cmerkle, config, collections, stores, caches, last_gc) + Inventory(file, LockFile(path, "Inventory"), cmerkle, + config, collections, stores, caches, last_gc) elseif create inventory = Inventory( MonitoredFile(path), + LockFile(path, "Inventory"), CachedMerkles(MonitoredFile( joinpath(dirname(path), DEFAULT_INVENTORY_CONFIG.store_dir, MERKLE_FILENAME)), []), convert(InventoryConfig, Dict{String, Any}()), diff --git a/Store/src/lockfile.jl b/Store/src/lockfile.jl new file mode 100644 index 00000000..955cb24f --- /dev/null +++ b/Store/src/lockfile.jl @@ -0,0 +1,76 @@ +""" + LockFile(path::String) -> LockFile + +A file-based lock that can be used to synchronise access to a resource across +multiple processes. The lock is implemented using a file at `path` that is +created when the lock is acquired and deleted when the lock is released. +""" +struct LockFile <: Base.AbstractLock + path::String + owned::ReentrantLock +end + +function LockFile(prefix::String, target) + path = BaseDirs.User.runtime( + PROJECT_SUBPATH, + prefix * "-" * string(hash(inv.file.path), base=32) * ".lock") + LockFile(path, ReentrantLock()) +end + +function Base.islocked(lf::LockFile) + islocked(lf.owned) && return true + isfile(lf.path) || return false + pid = open(io -> if eof(io) 0 else read(io, Int) end, lf.path) + iszero(@ccall uv_kill(pid::Cint, 0::Cint)::Cint) +end + +function Base.trylock(lf::LockFile) + islocked(lf.owned) && return trylock(lf.owned) + islocked(lf) && return false + ispath(dirname(lf.path)) || mkpath(dirname(lf.path)) + try + write(lf.path, UInt(getpid())) + chmod(lf.path, 0o444) + catch _ + return false + end + lock(lf.owned) + true +end + +function Base.lock(lf::LockFile) + backoff = 0.00001 # 10μs, given that it takes 5μs lock + unlock on my machine + while !trylock(lf) + quicksleep(backoff) + backoff = min(0.05, backoff * 2) + end +end + +function Base.unlock(lf::LockFile) + backoff = 0.00001 # 10μs, given that it takes 5μs to lock + unlock on my machine + if !islocked(lf.owned) + while islocked(lf) + quicksleep(backoff) + backoff = min(0.05, backoff * 2) + end + end + rm(lf.path, force=true) + unlock(lf.owned) +end + +# REVIEW: Only needed until something like <https://github.com/JuliaLang/julia/pull/55163> lands. +""" + quicksleep(period::Real) + +Sleep for `period` seconds, but use a busy loop for short periods (< 2ms). +""" +function quicksleep(period::Real) + if period < 0.02 + start = time() + while time() - start <= period + yield() + end + else + sleep(period) + end +end diff --git a/Store/src/types.jl b/Store/src/types.jl index cac7baa6..bcecb579 100644 --- a/Store/src/types.jl +++ b/Store/src/types.jl @@ -57,6 +57,7 @@ end mutable struct Inventory const file::MonitoredFile + const lock::LockFile const merkles::CachedMerkles config::InventoryConfig collections::Vector{CollectionInfo}