Merge pull request #2348 from dbnicholson/mt-locking

Improve multi-threaded locking
This commit is contained in:
Colin Walters 2021-06-05 11:34:09 -04:00 committed by GitHub
commit 5523aee082
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 638 additions and 220 deletions

View File

@ -173,9 +173,9 @@ endif # USE_GPGME
symbol_files = $(top_srcdir)/src/libostree/libostree-released.sym
# Uncomment this include when adding new development symbols.
#if BUILDOPT_IS_DEVEL_BUILD
#symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
#endif
if BUILDOPT_IS_DEVEL_BUILD
symbol_files += $(top_srcdir)/src/libostree/libostree-devel.sym
endif
# http://blog.jgc.org/2007/06/escaping-comma-and-space-in-gnu-make.html
wl_versionscript_arg = -Wl,--version-script=

View File

@ -31,7 +31,7 @@ AM_CPPFLAGS += -DDATADIR='"$(datadir)"' -DLIBEXECDIR='"$(libexecdir)"' \
-DOSTREE_COMPILATION \
-DG_LOG_DOMAIN=\"OSTree\" \
-DOSTREE_GITREV='"$(OSTREE_GITREV)"' \
-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_40 '-DGLIB_VERSION_MAX_ALLOWED=G_ENCODE_VERSION(2,50)' \
-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_44 '-DGLIB_VERSION_MAX_ALLOWED=G_ENCODE_VERSION(2,50)' \
-DSOUP_VERSION_MIN_REQUIRED=SOUP_VERSION_2_40 '-DSOUP_VERSION_MAX_ALLOWED=G_ENCODE_VERSION(2,48)'
# For strict aliasing, see https://bugzilla.gnome.org/show_bug.cgi?id=791622
AM_CFLAGS += -std=gnu99 -fno-strict-aliasing $(WARN_CFLAGS)

View File

@ -319,6 +319,12 @@ ostree_repo_get_min_free_space_bytes
ostree_repo_get_config
ostree_repo_get_dfd
ostree_repo_get_default_repo_finders
OstreeRepoLockType
ostree_repo_lock_pop
ostree_repo_lock_push
OstreeRepoAutoLock
ostree_repo_auto_lock_push
ostree_repo_auto_lock_cleanup
ostree_repo_hash
ostree_repo_equal
ostree_repo_copy_config

View File

@ -109,7 +109,7 @@ AM_PATH_GLIB_2_0(,,AC_MSG_ERROR([GLib not found]))
dnl When bumping the gio-unix-2.0 dependency (or glib-2.0 in general),
dnl remember to bump GLIB_VERSION_MIN_REQUIRED and
dnl GLIB_VERSION_MAX_ALLOWED in Makefile.am
GIO_DEPENDENCY="gio-unix-2.0 >= 2.40.0"
GIO_DEPENDENCY="gio-unix-2.0 >= 2.44.0"
PKG_CHECK_MODULES(OT_DEP_GIO_UNIX, $GIO_DEPENDENCY)
dnl 5.1.0 is an arbitrary version here

View File

@ -22,6 +22,14 @@
- uncomment the include in Makefile-libostree.am
*/
LIBOSTREE_2021.3 {
global:
ostree_repo_auto_lock_push;
ostree_repo_auto_lock_cleanup;
ostree_repo_lock_push;
ostree_repo_lock_pop;
} LIBOSTREE_2021.2;
/* Stub section for the stable release *after* this development one; don't
* edit this other than to update the year. This is just a copy/paste
* source. Replace $LASTSTABLE with the last stable version, and $NEWVERSION

View File

@ -1684,8 +1684,8 @@ ostree_repo_prepare_transaction (OstreeRepo *self,
memset (&self->txn.stats, 0, sizeof (OstreeRepoTransactionStats));
self->txn_locked = _ostree_repo_lock_push (self, OSTREE_REPO_LOCK_SHARED,
cancellable, error);
self->txn_locked = ostree_repo_lock_push (self, OSTREE_REPO_LOCK_SHARED,
cancellable, error);
if (!self->txn_locked)
return FALSE;
@ -2341,7 +2341,7 @@ ostree_repo_commit_transaction (OstreeRepo *self,
if (self->txn_locked)
{
if (!_ostree_repo_lock_pop (self, cancellable, error))
if (!ostree_repo_lock_pop (self, OSTREE_REPO_LOCK_SHARED, cancellable, error))
return FALSE;
self->txn_locked = FALSE;
}
@ -2399,7 +2399,7 @@ ostree_repo_abort_transaction (OstreeRepo *self,
if (self->txn_locked)
{
if (!_ostree_repo_lock_pop (self, cancellable, error))
if (!ostree_repo_lock_pop (self, OSTREE_REPO_LOCK_SHARED, cancellable, error))
return FALSE;
self->txn_locked = FALSE;
}

View File

@ -104,6 +104,13 @@ typedef struct {
fsblkcnt_t max_blocks;
} OstreeRepoTxn;
typedef struct {
GMutex mutex; /* All other members should only be accessed with this held */
int fd; /* The open file or flock file descriptor */
guint shared; /* Number of shared locks curently held */
guint exclusive; /* Number of exclusive locks currently held */
} OstreeRepoLock;
typedef enum {
_OSTREE_FEATURE_NO,
_OSTREE_FEATURE_MAYBE,
@ -159,6 +166,8 @@ struct OstreeRepo {
GWeakRef sysroot; /* Weak to avoid a circular ref; see also `is_system` */
char *remotes_config_dir;
OstreeRepoLock lock;
GMutex txn_lock;
OstreeRepoTxn txn;
gboolean txn_locked;
@ -506,30 +515,6 @@ _ostree_repo_maybe_regenerate_summary (OstreeRepo *self,
GCancellable *cancellable,
GError **error);
/* Locking APIs are currently private.
* See https://github.com/ostreedev/ostree/pull/1555
*/
typedef enum {
OSTREE_REPO_LOCK_SHARED,
OSTREE_REPO_LOCK_EXCLUSIVE
} OstreeRepoLockType;
gboolean _ostree_repo_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error);
gboolean _ostree_repo_lock_pop (OstreeRepo *self,
GCancellable *cancellable,
GError **error);
typedef OstreeRepo OstreeRepoAutoLock;
OstreeRepoAutoLock * _ostree_repo_auto_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error);
void _ostree_repo_auto_lock_cleanup (OstreeRepoAutoLock *lock);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoAutoLock, _ostree_repo_auto_lock_cleanup)
gboolean _ostree_repo_parse_fsverity_config (OstreeRepo *self, GError **error);

View File

@ -204,7 +204,7 @@ ostree_repo_prune_static_deltas (OstreeRepo *self, const char *commit,
GError **error)
{
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
if (!lock)
return FALSE;
@ -325,7 +325,7 @@ ostree_repo_traverse_reachable_refs (OstreeRepo *self,
GError **error)
{
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_SHARED, cancellable, error);
ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_SHARED, cancellable, error);
if (!lock)
return FALSE;
@ -400,7 +400,7 @@ ostree_repo_prune (OstreeRepo *self,
GError **error)
{
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
if (!lock)
return FALSE;
@ -486,7 +486,7 @@ ostree_repo_prune_from_reachable (OstreeRepo *self,
GError **error)
{
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
if (!lock)
return FALSE;

View File

@ -1270,7 +1270,7 @@ ostree_repo_static_delta_reindex (OstreeRepo *repo,
/* Protect against parallel prune operation */
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_SHARED, cancellable, error);
ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_SHARED, cancellable, error);
if (!lock)
return FALSE;

View File

@ -172,98 +172,84 @@ G_DEFINE_TYPE (OstreeRepo, ostree_repo, G_TYPE_OBJECT)
/* Repository locking
*
* To guard against objects being deleted (e.g., prune) while they're in
* use by another operation is accessing them (e.g., commit), the
* use by another operation that is accessing them (e.g., commit), the
* repository must be locked by concurrent writers.
*
* The locking is implemented by maintaining a thread local table of
* lock stacks per repository. This allows thread safe locking since
* each thread maintains its own lock stack. See the OstreeRepoLock type
* below.
* The repository locking has several important features:
*
* The actual locking is done using either open file descriptor locks or
* flock locks. This allows the locking to work with concurrent
* processes. The lock file is held on the ".lock" file within the
* repository.
* * There are 2 states - shared and exclusive. Multiple users can hold
* a shared lock concurrently while only one user can hold an
* exclusive lock.
*
* * The lock can be taken recursively so long as each acquisition is paired
* with a matching release. The recursion is also latched to the strongest
* state. Once an exclusive lock has been taken, it will remain exclusive
* until all exclusive locks have been released.
*
* * It is both multiprocess- and multithread-safe. Threads that share
* an OstreeRepo use the lock cooperatively while processes and
* threads using separate OstreeRepo structures will block when
* acquiring incompatible lock states.
*
* The actual locking is implemented using either open file descriptor
* locks or flock locks. This allows the locking to work with concurrent
* processes or concurrent threads using a separate OstreeRepo. The lock
* file is held on the ".lock" file within the repository.
*
* The intended usage is to take a shared lock when writing objects or
* reading objects in critical sections. Exclusive locks are taken when
* deleting objects.
*
* To allow fine grained locking within libostree, the lock is
* maintained as a stack. The core APIs then push or pop from the stack.
* When pushing or popping a lock state identical to the existing or
* next state, the stack is simply updated. Only when upgrading or
* downgrading the lock (changing to/from unlocked, pushing exclusive on
* shared or popping exclusive to shared) are actual locking operations
* performed.
* To allow fine grained locking, the lock state is maintained in shared and
* exclusive counters. Callers then push or pop lock types to increment or
* decrement the counters. When pushing or popping a lock type identical to
* the existing or next state, the lock state is simply updated. Only when
* upgrading or downgrading the lock (changing to/from unlocked, pushing
* exclusive on shared or popping exclusive to shared) are actual locking
* operations performed.
*/
static void
free_repo_lock_table (gpointer data)
{
GHashTable *lock_table = data;
if (lock_table != NULL)
{
g_debug ("Free lock table");
g_hash_table_destroy (lock_table);
}
}
static GPrivate repo_lock_table = G_PRIVATE_INIT (free_repo_lock_table);
typedef struct {
int fd;
GQueue stack;
} OstreeRepoLock;
typedef struct {
guint len;
int state;
const char *name;
} OstreeRepoLockInfo;
static void
repo_lock_info (OstreeRepoLock *lock, OstreeRepoLockInfo *out_info)
static const char *
lock_state_name (int state)
{
g_assert (lock != NULL);
g_assert (out_info != NULL);
OstreeRepoLockInfo info;
info.len = g_queue_get_length (&lock->stack);
if (info.len == 0)
switch (state)
{
info.state = LOCK_UN;
info.name = "unlocked";
case LOCK_EX:
return "exclusive";
case LOCK_SH:
return "shared";
case LOCK_UN:
return "unlocked";
default:
g_assert_not_reached ();
}
else
{
info.state = GPOINTER_TO_INT (g_queue_peek_head (&lock->stack));
info.name = (info.state == LOCK_EX) ? "exclusive" : "shared";
}
*out_info = info;
}
static void
free_repo_lock (gpointer data)
repo_lock_info (OstreeRepo *self, GMutexLocker *locker,
OstreeRepoLockInfo *out_info)
{
OstreeRepoLock *lock = data;
g_assert (self != NULL);
g_assert (locker != NULL);
g_assert (out_info != NULL);
if (lock != NULL)
{
OstreeRepoLockInfo info;
repo_lock_info (lock, &info);
OstreeRepoLockInfo info;
info.len = self->lock.shared + self->lock.exclusive;
if (info.len == 0)
info.state = LOCK_UN;
else if (self->lock.exclusive > 0)
info.state = LOCK_EX;
else
info.state = LOCK_SH;
info.name = lock_state_name (info.state);
g_debug ("Free lock: state=%s, depth=%u", info.name, info.len);
g_queue_clear (&lock->stack);
if (lock->fd >= 0)
{
g_debug ("Closing repo lock file");
(void) close (lock->fd);
}
g_free (lock);
}
*out_info = info;
}
/* Wrapper to handle flock vs OFD locking based on GLnxLockFile */
@ -339,125 +325,148 @@ push_repo_lock (OstreeRepo *self,
GError **error)
{
int flags = (lock_type == OSTREE_REPO_LOCK_EXCLUSIVE) ? LOCK_EX : LOCK_SH;
int next_state = flags;
if (!blocking)
flags |= LOCK_NB;
GHashTable *lock_table = g_private_get (&repo_lock_table);
if (lock_table == NULL)
{
g_debug ("Creating repo lock table");
lock_table = g_hash_table_new_full (NULL, NULL, NULL,
(GDestroyNotify)free_repo_lock);
g_private_set (&repo_lock_table, lock_table);
}
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->lock.mutex);
OstreeRepoLock *lock = g_hash_table_lookup (lock_table, self);
if (lock == NULL)
if (self->lock.fd == -1)
{
lock = g_new0 (OstreeRepoLock, 1);
g_queue_init (&lock->stack);
g_debug ("Opening repo lock file");
lock->fd = TEMP_FAILURE_RETRY (openat (self->repo_dir_fd, ".lock",
O_CREAT | O_RDWR | O_CLOEXEC,
DEFAULT_REGFILE_MODE));
if (lock->fd < 0)
{
free_repo_lock (lock);
return glnx_throw_errno_prefix (error,
"Opening lock file %s/.lock failed",
gs_file_get_path_cached (self->repodir));
}
g_hash_table_insert (lock_table, self, lock);
self->lock.fd = TEMP_FAILURE_RETRY (openat (self->repo_dir_fd, ".lock",
O_CREAT | O_RDWR | O_CLOEXEC,
DEFAULT_REGFILE_MODE));
if (self->lock.fd < 0)
return glnx_throw_errno_prefix (error,
"Opening lock file %s/.lock failed",
gs_file_get_path_cached (self->repodir));
}
OstreeRepoLockInfo info;
repo_lock_info (lock, &info);
repo_lock_info (self, locker, &info);
g_debug ("Push lock: state=%s, depth=%u", info.name, info.len);
if (info.state == LOCK_EX)
guint *counter;
if (next_state == LOCK_EX)
counter = &(self->lock.exclusive);
else
counter = &(self->lock.shared);
/* Check for overflow */
if (*counter == G_MAXUINT)
g_error ("Repo lock %s counter would overflow", lock_state_name (next_state));
if (info.state == LOCK_EX || info.state == next_state)
{
g_debug ("Repo already locked exclusively, extending stack");
g_queue_push_head (&lock->stack, GINT_TO_POINTER (LOCK_EX));
g_debug ("Repo already locked %s, maintaining state", info.name);
}
else
{
int next_state = (flags & LOCK_EX) ? LOCK_EX : LOCK_SH;
const char *next_state_name = (flags & LOCK_EX) ? "exclusive" : "shared";
/* We should never upgrade from exclusive to shared */
g_assert (!(info.state == LOCK_EX && next_state == LOCK_SH));
const char *next_state_name = lock_state_name (next_state);
g_debug ("Locking repo %s", next_state_name);
if (!do_repo_lock (lock->fd, flags))
if (!do_repo_lock (self->lock.fd, flags))
return glnx_throw_errno_prefix (error, "Locking repo %s failed",
next_state_name);
g_queue_push_head (&lock->stack, GINT_TO_POINTER (next_state));
}
/* Update state */
(*counter)++;
return TRUE;
}
static gboolean
pop_repo_lock (OstreeRepo *self,
gboolean blocking,
GError **error)
pop_repo_lock (OstreeRepo *self,
OstreeRepoLockType lock_type,
gboolean blocking,
GError **error)
{
int flags = blocking ? 0 : LOCK_NB;
GHashTable *lock_table = g_private_get (&repo_lock_table);
g_return_val_if_fail (lock_table != NULL, FALSE);
OstreeRepoLock *lock = g_hash_table_lookup (lock_table, self);
g_return_val_if_fail (lock != NULL, FALSE);
g_return_val_if_fail (lock->fd != -1, FALSE);
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->lock.mutex);
if (self->lock.fd == -1)
g_error ("Cannot pop repo never locked repo lock");
OstreeRepoLockInfo info;
repo_lock_info (lock, &info);
g_return_val_if_fail (info.len > 0, FALSE);
repo_lock_info (self, locker, &info);
g_debug ("Pop lock: state=%s, depth=%u", info.name, info.len);
if (info.len > 1)
{
int next_state = GPOINTER_TO_INT (g_queue_peek_nth (&lock->stack, 1));
/* Drop back to the previous lock state if it differs */
if (next_state != info.state)
{
/* We should never drop from shared to exclusive */
g_return_val_if_fail (next_state == LOCK_SH, FALSE);
g_debug ("Returning lock state to shared");
if (!do_repo_lock (lock->fd, next_state | flags))
return glnx_throw_errno_prefix (error,
"Setting repo lock to shared failed");
}
else
g_debug ("Maintaining lock state as %s", info.name);
if (info.len == 0 || info.state == LOCK_UN)
g_error ("Cannot pop already unlocked repo lock");
int state_to_drop;
guint *counter;
if (lock_type == OSTREE_REPO_LOCK_EXCLUSIVE)
{
state_to_drop = LOCK_EX;
counter = &(self->lock.exclusive);
}
else
{
/* Lock stack will be empty, unlock */
g_debug ("Unlocking repo");
if (!do_repo_unlock (lock->fd, flags))
return glnx_throw_errno_prefix (error, "Unlocking repo failed");
state_to_drop = LOCK_SH;
counter = &(self->lock.shared);
}
g_queue_pop_head (&lock->stack);
/* Make sure caller specified a valid type to release */
if (*counter == 0)
g_error ("Repo %s lock pop requested, but none have been taken",
lock_state_name (state_to_drop));
int next_state;
if (info.len == 1)
{
/* Lock counters will be empty, unlock */
next_state = LOCK_UN;
}
else if (state_to_drop == LOCK_EX)
next_state = (self->lock.exclusive > 1) ? LOCK_EX : LOCK_SH;
else
next_state = (self->lock.exclusive > 0) ? LOCK_EX : LOCK_SH;
if (next_state == LOCK_UN)
{
g_debug ("Unlocking repo");
if (!do_repo_unlock (self->lock.fd, flags))
return glnx_throw_errno_prefix (error, "Unlocking repo failed");
}
else if (info.state == next_state)
{
g_debug ("Maintaining lock state as %s", info.name);
}
else
{
/* We should never drop from shared to exclusive */
g_assert (next_state == LOCK_SH);
g_debug ("Returning lock state to shared");
if (!do_repo_lock (self->lock.fd, next_state | flags))
return glnx_throw_errno_prefix (error,
"Setting repo lock to shared failed");
}
/* Update state */
(*counter)--;
return TRUE;
}
/*
/**
* ostree_repo_lock_push:
* @self: a #OstreeRepo
* @lock_type: the type of lock to acquire
* @cancellable: a #GCancellable
* @error: a #GError
*
* Takes a lock on the repository and adds it to the lock stack. If @lock_type
* Takes a lock on the repository and adds it to the lock state. If @lock_type
* is %OSTREE_REPO_LOCK_SHARED, a shared lock is taken. If @lock_type is
* %OSTREE_REPO_LOCK_EXCLUSIVE, an exclusive lock is taken. The actual lock
* state is only changed when locking a previously unlocked repository or
* upgrading the lock from shared to exclusive. If the requested lock state is
* upgrading the lock from shared to exclusive. If the requested lock type is
* unchanged or would represent a downgrade (exclusive to shared), the lock
* state is not changed and the stack is simply updated.
* state is not changed.
*
* ostree_repo_lock_push() waits for the lock depending on the repository's
* lock-timeout-secs configuration. When lock-timeout-secs is -1, a blocking lock is
@ -470,12 +479,13 @@ pop_repo_lock (OstreeRepo *self,
* %TRUE is returned.
*
* Returns: %TRUE on success, otherwise %FALSE with @error set
* Since: 2021.3
*/
gboolean
_ostree_repo_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error)
ostree_repo_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (self != NULL, FALSE);
g_return_val_if_fail (OSTREE_IS_REPO (self), FALSE);
@ -538,16 +548,19 @@ _ostree_repo_lock_push (OstreeRepo *self,
}
}
/*
* _ostree_repo_lock_pop:
/**
* ostree_repo_lock_pop:
* @self: a #OstreeRepo
* @lock_type: the type of lock to release
* @cancellable: a #GCancellable
* @error: a #GError
*
* Remove the current repository lock state from the lock stack. If the lock
* stack becomes empty, the repository is unlocked. Otherwise, the lock state
* only changes when transitioning from an exclusive lock back to a shared
* lock.
* Release a lock of type @lock_type from the lock state. If the lock state
* becomes empty, the repository is unlocked. Otherwise, the lock state only
* changes when transitioning from an exclusive lock back to a shared lock. The
* requested @lock_type must be the same type that was requested in the call to
* ostree_repo_lock_push(). It is a programmer error if these do not match and
* the program may abort if the lock would reach an invalid state.
*
* ostree_repo_lock_pop() waits for the lock depending on the repository's
* lock-timeout-secs configuration. When lock-timeout-secs is -1, a blocking lock is
@ -560,11 +573,13 @@ _ostree_repo_lock_push (OstreeRepo *self,
* %TRUE is returned.
*
* Returns: %TRUE on success, otherwise %FALSE with @error set
* Since: 2021.3
*/
gboolean
_ostree_repo_lock_pop (OstreeRepo *self,
GCancellable *cancellable,
GError **error)
ostree_repo_lock_pop (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error)
{
g_return_val_if_fail (self != NULL, FALSE);
g_return_val_if_fail (OSTREE_IS_REPO (self), FALSE);
@ -581,7 +596,7 @@ _ostree_repo_lock_pop (OstreeRepo *self,
else if (self->lock_timeout_seconds == REPO_LOCK_BLOCKING)
{
g_debug ("Popping lock blocking");
return pop_repo_lock (self, TRUE, error);
return pop_repo_lock (self, lock_type, TRUE, error);
}
else
{
@ -596,7 +611,7 @@ _ostree_repo_lock_pop (OstreeRepo *self,
return FALSE;
g_autoptr(GError) local_error = NULL;
if (pop_repo_lock (self, FALSE, &local_error))
if (pop_repo_lock (self, lock_type, FALSE, &local_error))
return TRUE;
if (!g_error_matches (local_error, G_IO_ERROR,
@ -627,60 +642,72 @@ _ostree_repo_lock_pop (OstreeRepo *self,
}
}
/*
* _ostree_repo_auto_lock_push: (skip)
struct OstreeRepoAutoLock {
OstreeRepo *repo;
OstreeRepoLockType lock_type;
};
/**
* ostree_repo_auto_lock_push: (skip)
* @self: a #OstreeRepo
* @lock_type: the type of lock to acquire
* @cancellable: a #GCancellable
* @error: a #GError
*
* Like ostree_repo_lock_push(), but for usage with #OstreeRepoAutoLock.
* The intended usage is to declare the #OstreeRepoAutoLock with
* g_autoptr() so that ostree_repo_auto_lock_cleanup() is called when it
* goes out of scope. This will automatically pop the lock status off
* the stack if it was acquired successfully.
* Like ostree_repo_lock_push(), but for usage with #OstreeRepoAutoLock. The
* intended usage is to declare the #OstreeRepoAutoLock with g_autoptr() so
* that ostree_repo_auto_lock_cleanup() is called when it goes out of scope.
* This will automatically release the lock if it was acquired successfully.
*
* |[<!-- language="C" -->
* g_autoptr(OstreeRepoAutoLock) lock = NULL;
* lock = _ostree_repo_auto_lock_push (repo, lock_type, cancellable, error);
* lock = ostree_repo_auto_lock_push (repo, lock_type, cancellable, error);
* if (!lock)
* return FALSE;
* ]|
*
* Returns: @self on success, otherwise %NULL with @error set
* Since: 2021.3
*/
OstreeRepoAutoLock *
_ostree_repo_auto_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error)
ostree_repo_auto_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error)
{
if (!_ostree_repo_lock_push (self, lock_type, cancellable, error))
if (!ostree_repo_lock_push (self, lock_type, cancellable, error))
return NULL;
return (OstreeRepoAutoLock *)self;
OstreeRepoAutoLock *auto_lock = g_slice_new (OstreeRepoAutoLock);
auto_lock->repo = self;
auto_lock->lock_type = lock_type;
return auto_lock;
}
/*
* _ostree_repo_auto_lock_cleanup: (skip)
/**
* ostree_repo_auto_lock_cleanup: (skip)
* @lock: a #OstreeRepoAutoLock
*
* A cleanup handler for use with ostree_repo_auto_lock_push(). If @lock is
* not %NULL, ostree_repo_lock_pop() will be called on it. If
* ostree_repo_lock_pop() fails, a critical warning will be emitted.
*
* Since: 2021.3
*/
void
_ostree_repo_auto_lock_cleanup (OstreeRepoAutoLock *lock)
ostree_repo_auto_lock_cleanup (OstreeRepoAutoLock *auto_lock)
{
OstreeRepo *repo = lock;
if (repo)
if (auto_lock != NULL)
{
g_autoptr(GError) error = NULL;
int errsv = errno;
if (!_ostree_repo_lock_pop (repo, NULL, &error))
if (!ostree_repo_lock_pop (auto_lock->repo, auto_lock->lock_type, NULL, &error))
g_critical ("Cleanup repo lock failed: %s", error->message);
errno = errsv;
g_slice_free (OstreeRepoAutoLock, auto_lock);
}
}
@ -1052,13 +1079,8 @@ ostree_repo_finalize (GObject *object)
g_clear_pointer (&self->remotes, g_hash_table_destroy);
g_mutex_clear (&self->remotes_lock);
GHashTable *lock_table = g_private_get (&repo_lock_table);
if (lock_table)
{
g_hash_table_remove (lock_table, self);
if (g_hash_table_size (lock_table) == 0)
g_private_replace (&repo_lock_table, NULL);
}
glnx_close_fd (&self->lock.fd);
g_mutex_clear (&self->lock.mutex);
G_OBJECT_CLASS (ostree_repo_parent_class)->finalize (object);
}
@ -1220,6 +1242,7 @@ ostree_repo_init (OstreeRepo *self)
self->test_error_flags = g_parse_debug_string (g_getenv ("OSTREE_REPO_TEST_ERROR"),
test_error_keys, G_N_ELEMENTS (test_error_keys));
g_mutex_init (&self->lock.mutex);
g_mutex_init (&self->cache_lock);
g_mutex_init (&self->txn_lock);
@ -1233,6 +1256,7 @@ ostree_repo_init (OstreeRepo *self)
self->tmp_dir_fd = -1;
self->objects_dir_fd = -1;
self->uncompressed_objects_dir_fd = -1;
self->lock.fd = -1;
self->sysroot_kind = OSTREE_REPO_SYSROOT_KIND_UNKNOWN;
}
@ -5791,8 +5815,8 @@ ostree_repo_regenerate_summary (OstreeRepo *self,
g_autoptr(OstreeRepoAutoLock) lock = NULL;
gboolean no_deltas_in_summary = FALSE;
lock = _ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE,
cancellable, error);
lock = ostree_repo_auto_lock_push (self, OSTREE_REPO_LOCK_EXCLUSIVE,
cancellable, error);
if (!lock)
return FALSE;

View File

@ -1498,6 +1498,57 @@ gboolean ostree_repo_regenerate_summary (OstreeRepo *self,
GCancellable *cancellable,
GError **error);
/**
* OstreeRepoLockType:
* @OSTREE_REPO_LOCK_SHARED: A "read only" lock; multiple readers are allowed.
* @OSTREE_REPO_LOCK_EXCLUSIVE: A writable lock at most one writer can be active, and zero readers.
*
* Flags controlling repository locking.
*
* Since: 2021.3
*/
typedef enum {
OSTREE_REPO_LOCK_SHARED,
OSTREE_REPO_LOCK_EXCLUSIVE
} OstreeRepoLockType;
_OSTREE_PUBLIC
gboolean ostree_repo_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error);
_OSTREE_PUBLIC
gboolean ostree_repo_lock_pop (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error);
/* C convenience API only */
#ifndef __GI_SCANNER__
/**
* OstreeRepoAutoLock: (skip)
*
* An opaque type for use with ostree_repo_auto_lock_push().
*
* Since: 2021.3
*/
typedef struct OstreeRepoAutoLock OstreeRepoAutoLock;
_OSTREE_PUBLIC
OstreeRepoAutoLock * ostree_repo_auto_lock_push (OstreeRepo *self,
OstreeRepoLockType lock_type,
GCancellable *cancellable,
GError **error);
_OSTREE_PUBLIC
void ostree_repo_auto_lock_cleanup (OstreeRepoAutoLock *lock);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoAutoLock, ostree_repo_auto_lock_cleanup)
#endif
/**
* OSTREE_REPO_METADATA_REF:
*

View File

@ -505,7 +505,7 @@ ostree_sysroot_cleanup_prune_repo (OstreeSysroot *sysroot,
* the prune.
*/
g_autoptr(OstreeRepoAutoLock) lock =
_ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_EXCLUSIVE, cancellable, error);
if (!lock)
return FALSE;

View File

@ -40,11 +40,15 @@ def mktree(dname, serial=0):
f.write('{} {} {}\n'.format(dname, serial, v))
subprocess.check_call(['ostree', '--repo=repo', 'init', '--mode=bare'])
# like the bit in libtest, but let's do it unconditionally since it's simpler,
# and we don't need xattr coverage for this
with open('repo/config', 'a') as f:
# like the bit in libtest, but let's do it unconditionally since
# it's simpler, and we don't need xattr coverage for this
f.write('disable-xattrs=true\n')
# Make any locking errors fail quickly instead of blocking the test
# for 30 seconds.
f.write('lock-timeout-secs=5\n')
def commit(v):
tdir='tree{}'.format(v)
cmd = ['ostree', '--repo=repo', 'commit', '--fsync=0', '-b', tdir, '--tree=dir='+tdir]

View File

@ -134,4 +134,12 @@ w.write(inline_content.slice(10), null)
let actual_checksum = w.finish(null)
assertEquals(actual_checksum, networks_checksum)
// Basic locking API sanity test
repo.lock_push(OSTree.RepoLockType.SHARED, null);
repo.lock_push(OSTree.RepoLockType.SHARED, null);
repo.lock_pop(OSTree.RepoLockType.SHARED, null);
repo.lock_pop(OSTree.RepoLockType.SHARED, null);
repo.lock_push(OSTree.RepoLockType.EXCLUSIVE, null);
repo.lock_pop(OSTree.RepoLockType.EXCLUSIVE, null);
print("ok test-core");

View File

@ -51,6 +51,29 @@ setup (Fixture *fixture,
g_test_message ("Using temporary directory: %s", fixture->tmpdir.path);
}
/* Common setup for locking tests. Create an archive repo in the tmpdir and
* set the locking timeout to 0 so lock failures don't block.
*/
static void
lock_setup (Fixture *fixture,
gconstpointer test_data)
{
setup (fixture, test_data);
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_create_at (fixture->tmpdir.fd, ".",
OSTREE_REPO_MODE_ARCHIVE,
NULL,
NULL, &error);
g_assert_no_error (error);
/* Set the lock timeout to 0 so failures don't block the test */
g_autoptr(GKeyFile) config = ostree_repo_copy_config (repo);
g_key_file_set_integer (config, "core", "lock-timeout-secs", 0);
ostree_repo_write_config (repo, config, &error);
g_assert_no_error (error);
}
static void
teardown (Fixture *fixture,
gconstpointer test_data)
@ -249,6 +272,301 @@ test_write_regfile_api (Fixture *fixture,
g_assert_cmpstr (checksum, ==, "23a2e97d21d960ac7a4e39a8721b1baff7b213e00e5e5641334f50506012fcff");
}
/* Just a sanity check of the C autolocking API */
static void
test_repo_autolock (Fixture *fixture,
gconstpointer test_data)
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_create_at (fixture->tmpdir.fd, ".",
OSTREE_REPO_MODE_ARCHIVE,
NULL,
NULL, &error);
g_assert_no_error (error);
{
g_autoptr(OstreeRepoAutoLock) lock = ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
}
g_autoptr(OstreeRepoAutoLock) lock1 = ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
g_autoptr(OstreeRepoAutoLock) lock2 = ostree_repo_auto_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
}
/* Locking from single thread with a single OstreeRepo */
static void
test_repo_lock_single (Fixture *fixture,
gconstpointer test_data)
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
/* Single thread on a single repo can freely recurse in any state */
ostree_repo_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
}
/* Unlocking without having ever locked */
static void
test_repo_lock_unlock_never_locked (Fixture *fixture,
gconstpointer test_data)
{
if (g_test_subprocess ())
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
return;
}
g_test_trap_subprocess (NULL, 0, 0);
g_test_trap_assert_failed ();
g_test_trap_assert_stderr ("*ERROR*Cannot pop repo never locked repo lock\n");
}
/* Unlocking after already unlocked */
static void
test_repo_lock_double_unlock (Fixture *fixture,
gconstpointer test_data)
{
if (g_test_subprocess ())
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
return;
}
g_test_trap_subprocess (NULL, 0, 0);
g_test_trap_assert_failed ();
g_test_trap_assert_stderr ("*ERROR*Cannot pop already unlocked repo lock\n");
}
/* Unlocking the wrong type */
static void
test_repo_lock_unlock_wrong_type (Fixture *fixture,
gconstpointer test_data)
{
if (g_test_subprocess ())
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
return;
}
g_test_trap_subprocess (NULL, 0, 0);
g_test_trap_assert_failed ();
g_test_trap_assert_stderr ("*ERROR*Repo exclusive lock pop requested, but none have been taken\n");
}
/* Locking with single thread and multiple OstreeRepos */
static void
test_repo_lock_multi_repo (Fixture *fixture,
gconstpointer test_data)
{
g_autoptr(GError) error = NULL;
/* Open two OstreeRepo instances */
g_autoptr(OstreeRepo) repo1 = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
g_autoptr(OstreeRepo) repo2 = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
/* Single thread with multiple OstreeRepo's conflict */
ostree_repo_lock_push (repo1, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo1, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);
g_clear_error (&error);
ostree_repo_lock_pop (repo1, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
/* Recursive lock should stay exclusive once acquired */
ostree_repo_lock_push (repo1, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo1, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);
g_clear_error (&error);
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);
g_clear_error (&error);
ostree_repo_lock_pop (repo1, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
ostree_repo_lock_pop (repo1, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
}
/* Locking from multiple threads with a single OstreeRepo */
typedef struct {
OstreeRepo *repo;
guint step;
} LockThreadData;
static gpointer
lock_thread1 (gpointer thread_data)
{
LockThreadData *data = thread_data;
g_autoptr(GError) error = NULL;
/* Step 0: Take an exclusive lock */
g_assert_cmpuint (data->step, ==, 0);
g_test_message ("Thread 1: Push exclusive lock");
ostree_repo_lock_push (data->repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
data->step++;
/* Step 2: Take a shared lock */
while (data->step != 2)
g_thread_yield ();
g_test_message ("Thread 1: Push shared lock");
ostree_repo_lock_push (data->repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
data->step++;
/* Step 4: Pop both locks */
while (data->step != 4)
g_thread_yield ();
g_test_message ("Thread 1: Pop shared lock");
ostree_repo_lock_pop (data->repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
g_test_message ("Thread 1: Pop exclusive lock");
ostree_repo_lock_pop (data->repo, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
data->step++;
return NULL;
}
static gpointer
lock_thread2 (gpointer thread_data)
{
LockThreadData *data = thread_data;
g_autoptr(GError) error = NULL;
/* Step 1: Wait for the other thread to acquire a lock and then take a
* shared lock.
*/
while (data->step != 1)
g_thread_yield ();
g_test_message ("Thread 2: Push shared lock");
ostree_repo_lock_push (data->repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
data->step++;
/* Step 6: Pop lock */
while (data->step != 6)
g_thread_yield ();
g_test_message ("Thread 2: Pop shared lock");
ostree_repo_lock_pop (data->repo, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
data->step++;
return NULL;
}
static void
test_repo_lock_multi_thread (Fixture *fixture,
gconstpointer test_data)
{
g_autoptr(GError) error = NULL;
g_autoptr(OstreeRepo) repo1 = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
g_autoptr(OstreeRepo) repo2 = ostree_repo_open_at (fixture->tmpdir.fd, ".",
NULL, &error);
g_assert_no_error (error);
LockThreadData thread_data = {repo1, 0};
GThread *thread1 = g_thread_new ("lock-thread-1", lock_thread1, &thread_data);
GThread *thread2 = g_thread_new ("lock-thread-2", lock_thread2, &thread_data);
/* Step 3: Try to take a shared lock on repo2. This should fail since
* thread1 still has an exclusive lock.
*/
while (thread_data.step != 3)
g_thread_yield ();
g_test_message ("Repo 2: Push failing shared lock");
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);
g_clear_error (&error);
thread_data.step++;
/* Step 5: Try to a lock on repo2. A shared lock should succeed since
* thread1 has dropped its exclusive lock.
*/
while (thread_data.step != 5)
g_thread_yield ();
g_test_message ("Repo 2: Push shared lock");
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
g_test_message ("Repo 2: Push failing exclusive lock");
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_error (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK);
g_clear_error (&error);
thread_data.step++;
/* Step 7: Now both threads have dropped their locks and taking an exclusive
* lock should succeed.
*/
while (thread_data.step != 7)
g_thread_yield ();
g_test_message ("Repo 2: Push exclusive lock");
ostree_repo_lock_push (repo2, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
g_test_message ("Repo 2: Pop exclusive lock");
ostree_repo_lock_pop (repo2, OSTREE_REPO_LOCK_EXCLUSIVE, NULL, &error);
g_assert_no_error (error);
g_test_message ("Repo 2: Pop shared lock");
ostree_repo_lock_pop (repo2, OSTREE_REPO_LOCK_SHARED, NULL, &error);
g_assert_no_error (error);
thread_data.step++;
g_thread_join (thread1);
g_thread_join (thread2);
}
int
main (int argc,
char **argv)
@ -266,6 +584,20 @@ main (int argc,
test_repo_get_min_free_space, teardown);
g_test_add ("/repo/write_regfile_api", Fixture, NULL, setup,
test_write_regfile_api, teardown);
g_test_add ("/repo/autolock", Fixture, NULL, setup,
test_repo_autolock, teardown);
g_test_add ("/repo/lock/single", Fixture, NULL, lock_setup,
test_repo_lock_single, teardown);
g_test_add ("/repo/lock/unlock-never-locked", Fixture, NULL, lock_setup,
test_repo_lock_unlock_never_locked, teardown);
g_test_add ("/repo/lock/double-unlock", Fixture, NULL, lock_setup,
test_repo_lock_double_unlock, teardown);
g_test_add ("/repo/lock/unlock-wrong-type", Fixture, NULL, lock_setup,
test_repo_lock_unlock_wrong_type, teardown);
g_test_add ("/repo/lock/multi-repo", Fixture, NULL, lock_setup,
test_repo_lock_multi_repo, teardown);
g_test_add ("/repo/lock/multi-thread", Fixture, NULL, lock_setup,
test_repo_lock_multi_thread, teardown);
return g_test_run ();
}