Skip to content

Commit

Permalink
feat(Store): a generic file-based lock type
Browse files Browse the repository at this point in the history
I have a feeling I'll want to end up using this for more than just
inventory writing in time.
  • Loading branch information
tecosaur committed Oct 12, 2024
1 parent 154715e commit 4fae198
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 1 deletion.
1 change: 1 addition & 0 deletions Store/src/DataToolkitStore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using Compat

@compat public load_inventory, fetch!

include("lockfile.jl")
include("types.jl")

const INVENTORY_VERSION = 0
Expand Down
4 changes: 3 additions & 1 deletion Store/src/inventory.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}()),
Expand Down
76 changes: 76 additions & 0 deletions Store/src/lockfile.jl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Store/src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ end

mutable struct Inventory
const file::MonitoredFile
const lock::LockFile
const merkles::CachedMerkles
config::InventoryConfig
collections::Vector{CollectionInfo}
Expand Down

0 comments on commit 4fae198

Please sign in to comment.