Fixed noop sync broadcasting, added more specific sync tests
There are a number of nuanced cases to watch out for when mixing sync,
desync, and "noop syncs" (sync when no write operation has occured):
1. Noop-sync after unrelated write:
op a state b state
in-sync in-sync
write(a) unsync in-sync
sync(b) in-sync in-sync
In this case, a should be clobbered by b when b syncs. But this
gets tricky since b is still up to date with the disk, so b's
unsynced flag is not set.
The solution here is to just unconditionally broadcast all sync
operations irregardless of on-disk state. This is all in-device
anyways, so it shouldn't really add any overhead.
2. Noop-sync after unrelated write-sync after desync:
op a state b state
in-sync in-sync
desync(b) in-sync desync
write(a) unsync desync
sync(a) in-sync' desync
sync(b) in-sync in-sync
In this case, a should again be clobbered by b, even though a is
in-sync with the disk. This is not tricky because of a's state, but
because b doesn't know it is no longer in-sync with the disk.
The solution here is to set the unsynced flag on all desynced files
when an unrelated file is synced. This way, b knows it needs to
update disk if sync is called. We already scan all opened files to
update in-sync files, so this has very little cost.
3. Readonly-sync after unrelated write-sync after desync?
This is basically the same as 2., but involves a readonly file:
op a state b state (rdonly)
in-sync in-sync
desync(b) in-sync desync
write(a) unsync desync
sync(a) in-sync' desync
sync(b) ??? in-sync
In this case, I have no idea what should happen.
I would guess the least surprising result would be for b to write
its contents to a/disk? Bringing everything in-sync?
But this implies that b, a readonly file, should write to disk.
This isn't the only place a read operation would result in a write.
RDWR files, for example, can flush buffers during a file read. But at
least there, the file is open RDWR, not strictly RDONLY.
It seems like writing during sync on a readonly file breaks some sort
of invariant users expect.
But the alternative: Dropping the current state of b in favor of a's
state, is inconsistent with sync on WRONLY/RDWR files, and seems like
it breaks some sort of invariant about sync modifying the current
file's state...
Given this situation, I think the best course of action is to just
disallow sync on readonly files. It is now an assert.
There is some precedent for this, upstream we already omit sync when
compiled in LFS_READONLY mode. Though this does deviate from POSIX
behavior...
Worst case, by asserting, this leaves us free to introduce different
readonly-sync behavior in the future without breaking backwards
compatibility.
---
Maybe there should be some sort of lfsr_file_resync function to
discard current changes? Though this can be done with a close+open
cycle, so I think the value would be low.
Added tests over these cases and fixed where they broke, except for 3.,
lfsr_file_sync and lfsr_file_flush get asserts now to prevent their use
on readonly files.
Also added a couple more specific tests to cover cases I was concerned
about.
This commit is contained in:
@@ -5118,20 +5118,6 @@ static int lfsr_mdir_lookupwide(lfs_t *lfs, const lfsr_mdir_t *mdir,
|
||||
}
|
||||
|
||||
// track opened mdirs to keep state in-sync
|
||||
static void lfsr_addopened(lfs_t *lfs, lfsr_opened_t *opened) {
|
||||
opened->next = lfs->opened;
|
||||
lfs->opened = opened;
|
||||
}
|
||||
|
||||
static void lfsr_removeopened(lfs_t *lfs, lfsr_opened_t *opened) {
|
||||
for (lfsr_opened_t **p = &lfs->opened; *p; p = &(*p)->next) {
|
||||
if (*p == opened) {
|
||||
*p = (*p)->next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool lfsr_isopened(lfs_t *lfs, const lfsr_opened_t *opened) {
|
||||
for (lfsr_opened_t *p = lfs->opened; p; p = p->next) {
|
||||
if (p == opened) {
|
||||
@@ -5142,6 +5128,22 @@ static bool lfsr_isopened(lfs_t *lfs, const lfsr_opened_t *opened) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static void lfsr_addopened(lfs_t *lfs, lfsr_opened_t *opened) {
|
||||
LFS_ASSERT(!lfsr_isopened(lfs, opened));
|
||||
opened->next = lfs->opened;
|
||||
lfs->opened = opened;
|
||||
}
|
||||
|
||||
static void lfsr_removeopened(lfs_t *lfs, lfsr_opened_t *opened) {
|
||||
LFS_ASSERT(lfsr_isopened(lfs, opened));
|
||||
for (lfsr_opened_t **p = &lfs->opened; *p; p = &(*p)->next) {
|
||||
if (*p == opened) {
|
||||
*p = (*p)->next;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Metadata-tree things ///
|
||||
@@ -10415,6 +10417,7 @@ static int lfsr_file_flush_(lfs_t *lfs, lfsr_file_t *file,
|
||||
|
||||
lfs_ssize_t lfsr_file_read(lfs_t *lfs, lfsr_file_t *file,
|
||||
void *buffer, lfs_size_t size) {
|
||||
// can't read from writeonly files
|
||||
LFS_ASSERT(lfsr_o_isreadable(file->flags));
|
||||
LFS_ASSERT(file->pos + size <= 0x7fffffff);
|
||||
|
||||
@@ -10505,6 +10508,7 @@ lfs_ssize_t lfsr_file_read(lfs_t *lfs, lfsr_file_t *file,
|
||||
|
||||
lfs_ssize_t lfsr_file_write(lfs_t *lfs, lfsr_file_t *file,
|
||||
const void *buffer, lfs_size_t size) {
|
||||
// can't write to readonly files
|
||||
LFS_ASSERT(lfsr_o_iswriteable(file->flags));
|
||||
|
||||
// would this write make our file larger than our size limit?
|
||||
@@ -10655,14 +10659,11 @@ failed:;
|
||||
}
|
||||
|
||||
int lfsr_file_flush(lfs_t *lfs, lfsr_file_t *file) {
|
||||
// do nothing if our file is readonly
|
||||
if (!lfsr_o_iswriteable(file->flags)) {
|
||||
LFS_ASSERT(!lfsr_f_isunflushed(file->flags)
|
||||
|| (lfsr_file_size_(file) <= lfs->cfg->cache_size
|
||||
&& lfsr_file_size_(file) <= lfs->cfg->inline_size
|
||||
&& lfsr_file_size_(file) <= lfs->cfg->fragment_size));
|
||||
return 0;
|
||||
}
|
||||
// flushing readonly files is not supported
|
||||
//
|
||||
// in theory we could make this a noop, but that would be inconsistent
|
||||
// with lfsr_file_sync
|
||||
LFS_ASSERT(lfsr_o_iswriteable(file->flags));
|
||||
|
||||
// do nothing if our file is already flushed
|
||||
if (!lfsr_f_isunflushed(file->flags)) {
|
||||
@@ -10706,19 +10707,17 @@ failed:;
|
||||
}
|
||||
|
||||
int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) {
|
||||
// syncing readonly files is not supported
|
||||
//
|
||||
// in theory we could make this a noop, but then syncing desynced
|
||||
// readonly files would require disk writes
|
||||
LFS_ASSERT(lfsr_o_iswriteable(file->flags));
|
||||
|
||||
// do nothing if our file has been removed
|
||||
if (file->mdir.mid == -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// do nothing if our file is readonly
|
||||
if (!lfsr_o_iswriteable(file->flags)) {
|
||||
LFS_ASSERT(!lfsr_f_isunsynced(file->flags));
|
||||
// but do clear desync flag
|
||||
file->flags &= ~LFS_O_DESYNC;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first flush any data in our buffer, this is a noop if already
|
||||
// flushed
|
||||
//
|
||||
@@ -10750,14 +10749,14 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) {
|
||||
&& file->buffer_size <= lfs->cfg->inline_size
|
||||
&& file->buffer_size <= lfs->cfg->fragment_size));
|
||||
|
||||
// checkpoint the allocator again
|
||||
lfs_alloc_ckpoint(lfs);
|
||||
|
||||
// don't write to disk if disk is already in-sync
|
||||
// don't write to disk if our disk is already in-sync
|
||||
if (lfsr_f_isunsynced(file->flags)) {
|
||||
// checkpoint the allocator again
|
||||
lfs_alloc_ckpoint(lfs);
|
||||
|
||||
// commit our file's metadata
|
||||
uint8_t buf[LFSR_BTREE_DSIZE];
|
||||
int err = lfsr_mdir_commit(lfs, &file->mdir, LFSR_ATTRS(
|
||||
err = lfsr_mdir_commit(lfs, &file->mdir, LFSR_ATTRS(
|
||||
(lfsr_f_isunflushed(file->flags) && file->buffer_size == 0)
|
||||
? LFSR_ATTR(file->mdir.mid,
|
||||
WIDE(RM(STRUCT)), 0,
|
||||
@@ -10774,7 +10773,7 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) {
|
||||
WIDE(BTREE), 0,
|
||||
FROMBTREE(&file->ftree.u.btree, buf))));
|
||||
if (err) {
|
||||
return err;
|
||||
goto failed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10786,20 +10785,25 @@ int lfsr_file_sync(lfs_t *lfs, lfsr_file_t *file) {
|
||||
if (file_->type == LFS_TYPE_REG
|
||||
&& file_->mdir.mid == file->mdir.mid
|
||||
// don't double update
|
||||
&& file_ != file
|
||||
// don't update desynced file handles
|
||||
&& !lfsr_o_isdesync(file_->flags)) {
|
||||
file_->flags &= ~LFS_F_UNSYNCED;
|
||||
if (lfsr_f_isunflushed(file->flags)) {
|
||||
file_->flags |= LFS_F_UNFLUSHED;
|
||||
&& file_ != file) {
|
||||
// mark desynced files an unsynced
|
||||
if (lfsr_o_isdesync(file_->flags)) {
|
||||
file_->flags |= LFS_F_UNSYNCED;
|
||||
|
||||
// update synced files
|
||||
} else {
|
||||
file_->flags &= ~LFS_F_UNFLUSHED;
|
||||
file_->flags &= ~LFS_F_UNSYNCED;
|
||||
if (lfsr_f_isunflushed(file->flags)) {
|
||||
file_->flags |= LFS_F_UNFLUSHED;
|
||||
} else {
|
||||
file_->flags &= ~LFS_F_UNFLUSHED;
|
||||
}
|
||||
file_->ftree = file->ftree;
|
||||
file_->buffer_pos = file->buffer_pos;
|
||||
LFS_ASSERT(file->buffer_size <= lfs->cfg->cache_size);
|
||||
memcpy(file_->buffer, file->buffer, file->buffer_size);
|
||||
file_->buffer_size = file->buffer_size;
|
||||
}
|
||||
file_->ftree = file->ftree;
|
||||
file_->buffer_pos = file->buffer_pos;
|
||||
LFS_ASSERT(file->buffer_size <= lfs->cfg->cache_size);
|
||||
memcpy(file_->buffer, file->buffer, file->buffer_size);
|
||||
file_->buffer_size = file->buffer_size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user