Skip to content

Commit

Permalink
gfs2: Force withdraw to replay journals and wait for it to finish
Browse files Browse the repository at this point in the history
When a node withdraws from a file system, it often leaves its journal
in an incomplete state. This is especially true when the withdraw is
caused by io errors writing to the journal. Before this patch, a
withdraw would try to write a "shutdown" record to the journal, tell
dlm it's done with the file system, and none of the other nodes
know about the problem. Later, when the problem is fixed and the
withdrawn node is rebooted, it would then discover that its own
journal was incomplete, and replay it. However, replaying it at this
point is almost guaranteed to introduce corruption because the other
nodes are likely to have used affected resource groups that appeared
in the journal since the time of the withdraw. Replaying the journal
later will overwrite any changes made, and not through any fault of
dlm, which was instructed during the withdraw to release those
resources.

This patch makes file system withdraws seen by the entire cluster.
Withdrawing nodes dequeue their journal glock to allow recovery.

The remaining nodes check all the journals to see if they are
clean or in need of replay. They try to replay dirty journals, but
only the journals of withdrawn nodes will be "not busy" and
therefore available for replay.

Until the journal replay is complete, no i/o related glocks may be
given out, to ensure that the replay does not cause the
aforementioned corruption: We cannot allow any journal replay to
overwrite blocks associated with a glock once it is held.

The "live" glock which is now used to signal when a withdraw
occurs. When a withdraw occurs, the node signals its withdraw by
dequeueing the "live" glock and trying to enqueue it in EX mode,
thus forcing the other nodes to all see a demote request, by way
of a "1CB" (one callback) try lock. The "live" glock is not
granted in EX; the callback is only just used to indicate a
withdraw has occurred.

Note that all nodes in the cluster must wait for the recovering
node to finish replaying the withdrawing node's journal before
continuing. To this end, it checks that the journals are clean
multiple times in a retry loop.

Also note that the withdraw function may be called from a wide
variety of situations, and therefore, we need to take extra
precautions to make sure pointers are valid before using them in
many circumstances.

We also need to take care when glocks decide to withdraw, since
the withdraw code now uses glocks.

Also, before this patch, if a process encountered an error and
decided to withdraw, if another process was already withdrawing,
the second withdraw would be silently ignored, which set it free
to unlock its glocks. That's correct behavior if the original
withdrawer encounters further errors down the road. But if
secondary waiters don't wait for the journal replay, unlocking
glocks will allow other nodes to use them, despite the fact that
the journal containing those blocks is being replayed. The
replay needs to finish before our glocks are released to other
nodes. IOW, secondary withdraws need to wait for the first
withdraw to finish.

For example, if an rgrp glock is unlocked by a process that didn't
wait for the first withdraw, a journal replay could introduce file
system corruption by replaying a rgrp block that has already been
granted to a different cluster node.

Signed-off-by: Bob Peterson <[email protected]>
  • Loading branch information
AstralBob committed Feb 27, 2020
1 parent a72d240 commit 601ef0d
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 31 deletions.
23 changes: 20 additions & 3 deletions fs/gfs2/glock.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ static void __gfs2_glock_put(struct gfs2_glock *gl)
gfs2_glock_remove_from_lru(gl);
spin_unlock(&gl->gl_lockref.lock);
GLOCK_BUG_ON(gl, !list_empty(&gl->gl_holders));
GLOCK_BUG_ON(gl, mapping && mapping->nrpages);
GLOCK_BUG_ON(gl, mapping && mapping->nrpages && !gfs2_withdrawn(sdp));
trace_gfs2_glock_put(gl);
sdp->sd_lockstruct.ls_ops->lm_put_lock(gl);
}
Expand Down Expand Up @@ -576,7 +576,8 @@ __acquires(&gl->gl_lockref.lock)
unsigned int lck_flags = (unsigned int)(gh ? gh->gh_flags : 0);
int ret;

if (target != LM_ST_UNLOCKED && glock_blocked_by_withdraw(gl))
if (target != LM_ST_UNLOCKED && glock_blocked_by_withdraw(gl) &&
gh && !(gh->gh_flags & LM_FLAG_NOEXP))
return;
lck_flags &= (LM_FLAG_TRY | LM_FLAG_TRY_1CB | LM_FLAG_NOEXP |
LM_FLAG_PRIORITY);
Expand Down Expand Up @@ -1222,7 +1223,7 @@ int gfs2_glock_nq(struct gfs2_holder *gh)
struct gfs2_glock *gl = gh->gh_gl;
int error = 0;

if (glock_blocked_by_withdraw(gl))
if (glock_blocked_by_withdraw(gl) && !(gh->gh_flags & LM_FLAG_NOEXP))
return -EIO;

if (test_bit(GLF_LRU, &gl->gl_flags))
Expand Down Expand Up @@ -1266,10 +1267,26 @@ int gfs2_glock_poll(struct gfs2_holder *gh)
void gfs2_glock_dq(struct gfs2_holder *gh)
{
struct gfs2_glock *gl = gh->gh_gl;
struct gfs2_sbd *sdp = gl->gl_name.ln_sbd;
unsigned delay = 0;
int fast_path = 0;

spin_lock(&gl->gl_lockref.lock);
/*
* If we're in the process of file system withdraw, we cannot just
* dequeue any glocks until our journal is recovered, lest we
* introduce file system corruption. We need two exceptions to this
* rule: We need to allow unlocking of nondisk glocks and the glock
* for our own journal that needs recovery.
*/
if (test_bit(SDF_WITHDRAW_RECOVERY, &sdp->sd_flags) &&
glock_blocked_by_withdraw(gl) &&
gh->gh_gl != sdp->sd_jinode_gl) {
sdp->sd_glock_dqs_held++;
might_sleep();
wait_on_bit(&sdp->sd_flags, SDF_WITHDRAW_RECOVERY,
TASK_UNINTERRUPTIBLE);
}
if (gh->gh_flags & GL_NOCACHE)
handle_callback(gl, LM_ST_UNLOCKED, 0, false);

Expand Down
77 changes: 76 additions & 1 deletion fs/gfs2/glops.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

struct workqueue_struct *gfs2_freeze_wq;

extern struct workqueue_struct *gfs2_control_wq;

static void gfs2_ail_error(struct gfs2_glock *gl, const struct buffer_head *bh)
{
fs_err(gl->gl_name.ln_sbd,
Expand Down Expand Up @@ -496,13 +498,17 @@ static void freeze_go_sync(struct gfs2_glock *gl)
int error = 0;
struct gfs2_sbd *sdp = gl->gl_name.ln_sbd;

if (gl->gl_state == LM_ST_SHARED &&
if (gl->gl_state == LM_ST_SHARED && !gfs2_withdrawn(sdp) &&
test_bit(SDF_JOURNAL_LIVE, &sdp->sd_flags)) {
atomic_set(&sdp->sd_freeze_state, SFS_STARTING_FREEZE);
error = freeze_super(sdp->sd_vfs);
if (error) {
fs_info(sdp, "GFS2: couldn't freeze filesystem: %d\n",
error);
if (gfs2_withdrawn(sdp)) {
atomic_set(&sdp->sd_freeze_state, SFS_UNFROZEN);
return;
}
gfs2_assert_withdraw(sdp, 0);
}
queue_work(gfs2_freeze_wq, &sdp->sd_freeze_work);
Expand Down Expand Up @@ -577,6 +583,73 @@ static void iopen_go_callback(struct gfs2_glock *gl, bool remote)
}
}

/**
* inode_go_free - wake up anyone waiting for dlm's unlock ast to free it
* @gl: glock being freed
*
* For now, this is only used for the journal inode glock. In withdraw
* situations, we need to wait for the glock to be freed so that we know
* other nodes may proceed with recovery / journal replay.
*/
static void inode_go_free(struct gfs2_glock *gl)
{
/* Note that we cannot reference gl_object because it's already set
* to NULL by this point in its lifecycle. */
if (!test_bit(GLF_FREEING, &gl->gl_flags))
return;
clear_bit_unlock(GLF_FREEING, &gl->gl_flags);
wake_up_bit(&gl->gl_flags, GLF_FREEING);
}

/**
* nondisk_go_callback - used to signal when a node did a withdraw
* @gl: the nondisk glock
* @remote: true if this came from a different cluster node
*
*/
static void nondisk_go_callback(struct gfs2_glock *gl, bool remote)
{
struct gfs2_sbd *sdp = gl->gl_name.ln_sbd;

/* Ignore the callback unless it's from another node, and it's the
live lock. */
if (!remote || gl->gl_name.ln_number != GFS2_LIVE_LOCK)
return;

/* First order of business is to cancel the demote request. We don't
* really want to demote a nondisk glock. At best it's just to inform
* us of another node's withdraw. We'll keep it in SH mode. */
clear_bit(GLF_DEMOTE, &gl->gl_flags);
clear_bit(GLF_PENDING_DEMOTE, &gl->gl_flags);

/* Ignore the unlock if we're withdrawn, unmounting, or in recovery. */
if (test_bit(SDF_NORECOVERY, &sdp->sd_flags) ||
test_bit(SDF_WITHDRAWN, &sdp->sd_flags) ||
test_bit(SDF_REMOTE_WITHDRAW, &sdp->sd_flags))
return;

/* We only care when a node wants us to unlock, because that means
* they want a journal recovered. */
if (gl->gl_demote_state != LM_ST_UNLOCKED)
return;

if (sdp->sd_args.ar_spectator) {
fs_warn(sdp, "Spectator node cannot recover journals.\n");
return;
}

fs_warn(sdp, "Some node has withdrawn; checking for recovery.\n");
set_bit(SDF_REMOTE_WITHDRAW, &sdp->sd_flags);
/*
* We can't call remote_withdraw directly here or gfs2_recover_journal
* because this is called from the glock unlock function and the
* remote_withdraw needs to enqueue and dequeue the same "live" glock
* we were called from. So we queue it to the control work queue in
* lock_dlm.
*/
queue_delayed_work(gfs2_control_wq, &sdp->sd_control_work, 0);
}

const struct gfs2_glock_operations gfs2_meta_glops = {
.go_type = LM_TYPE_META,
.go_flags = GLOF_NONDISK,
Expand All @@ -590,6 +663,7 @@ const struct gfs2_glock_operations gfs2_inode_glops = {
.go_dump = inode_go_dump,
.go_type = LM_TYPE_INODE,
.go_flags = GLOF_ASPACE | GLOF_LRU,
.go_free = inode_go_free,
};

const struct gfs2_glock_operations gfs2_rgrp_glops = {
Expand Down Expand Up @@ -623,6 +697,7 @@ const struct gfs2_glock_operations gfs2_flock_glops = {
const struct gfs2_glock_operations gfs2_nondisk_glops = {
.go_type = LM_TYPE_NONDISK,
.go_flags = GLOF_NONDISK,
.go_callback = nondisk_go_callback,
};

const struct gfs2_glock_operations gfs2_quota_glops = {
Expand Down
9 changes: 9 additions & 0 deletions fs/gfs2/incore.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ struct gfs2_glock_operations {
void (*go_dump)(struct seq_file *seq, struct gfs2_glock *gl,
const char *fs_id_buf);
void (*go_callback)(struct gfs2_glock *gl, bool remote);
void (*go_free)(struct gfs2_glock *gl);
const int go_type;
const unsigned long go_flags;
#define GLOF_ASPACE 1 /* address space attached */
Expand Down Expand Up @@ -343,6 +344,7 @@ enum {
GLF_OBJECT = 14, /* Used only for tracing */
GLF_BLOCKING = 15,
GLF_INODE_CREATING = 16, /* Inode creation occurring */
GLF_FREEING = 18, /* Wait for glock to be freed */
};

struct gfs2_glock {
Expand Down Expand Up @@ -619,6 +621,10 @@ enum {
SDF_FORCE_AIL_FLUSH = 9,
SDF_FS_FROZEN = 10,
SDF_WITHDRAWING = 11, /* Will withdraw eventually */
SDF_WITHDRAW_IN_PROG = 12, /* Withdraw is in progress */
SDF_REMOTE_WITHDRAW = 13, /* Performing remote recovery */
SDF_WITHDRAW_RECOVERY = 14, /* Wait for journal recovery when we are
withdrawing */
};

enum gfs2_freeze_state {
Expand Down Expand Up @@ -769,6 +775,7 @@ struct gfs2_sbd {
struct gfs2_jdesc *sd_jdesc;
struct gfs2_holder sd_journal_gh;
struct gfs2_holder sd_jinode_gh;
struct gfs2_glock *sd_jinode_gl;

struct gfs2_holder sd_sc_gh;
struct gfs2_holder sd_qc_gh;
Expand Down Expand Up @@ -830,6 +837,7 @@ struct gfs2_sbd {
struct bio *sd_log_bio;
wait_queue_head_t sd_log_flush_wait;
int sd_log_error; /* First log error */
wait_queue_head_t sd_withdraw_wait;

atomic_t sd_reserving_log;
wait_queue_head_t sd_reserving_log_wait;
Expand All @@ -853,6 +861,7 @@ struct gfs2_sbd {

unsigned long sd_last_warning;
struct dentry *debugfs_dir; /* debugfs directory */
unsigned long sd_glock_dqs_held;
};

static inline void gfs2_glstats_inc(struct gfs2_glock *gl, int which)
Expand Down
34 changes: 34 additions & 0 deletions fs/gfs2/lock_dlm.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

#include "incore.h"
#include "glock.h"
#include "glops.h"
#include "recovery.h"
#include "util.h"
#include "sys.h"
#include "trace_gfs2.h"
Expand Down Expand Up @@ -124,6 +126,8 @@ static void gdlm_ast(void *arg)

switch (gl->gl_lksb.sb_status) {
case -DLM_EUNLOCK: /* Unlocked, so glock can be freed */
if (gl->gl_ops->go_free)
gl->gl_ops->go_free(gl);
gfs2_glock_free(gl);
return;
case -DLM_ECANCEL: /* Cancel while getting lock */
Expand Down Expand Up @@ -323,6 +327,7 @@ static void gdlm_cancel(struct gfs2_glock *gl)
/*
* dlm/gfs2 recovery coordination using dlm_recover callbacks
*
* 0. gfs2 checks for another cluster node withdraw, needing journal replay
* 1. dlm_controld sees lockspace members change
* 2. dlm_controld blocks dlm-kernel locking activity
* 3. dlm_controld within dlm-kernel notifies gfs2 (recover_prep)
Expand Down Expand Up @@ -571,6 +576,28 @@ static int control_lock(struct gfs2_sbd *sdp, int mode, uint32_t flags)
&ls->ls_control_lksb, "control_lock");
}

/**
* remote_withdraw - react to a node withdrawing from the file system
* @sdp: The superblock
*/
static void remote_withdraw(struct gfs2_sbd *sdp)
{
struct gfs2_jdesc *jd;
int ret = 0, count = 0;

list_for_each_entry(jd, &sdp->sd_jindex_list, jd_list) {
if (jd->jd_jid == sdp->sd_lockstruct.ls_jid)
continue;
ret = gfs2_recover_journal(jd, true);
if (ret)
break;
count++;
}

/* Now drop the additional reference we acquired */
fs_err(sdp, "Journals checked: %d, ret = %d.\n", count, ret);
}

static void gfs2_control_func(struct work_struct *work)
{
struct gfs2_sbd *sdp = container_of(work, struct gfs2_sbd, sd_control_work.work);
Expand All @@ -581,6 +608,13 @@ static void gfs2_control_func(struct work_struct *work)
int recover_size;
int i, error;

/* First check for other nodes that may have done a withdraw. */
if (test_bit(SDF_REMOTE_WITHDRAW, &sdp->sd_flags)) {
remote_withdraw(sdp);
clear_bit(SDF_REMOTE_WITHDRAW, &sdp->sd_flags);
return;
}

spin_lock(&ls->ls_recover_spin);
/*
* No MOUNT_DONE means we're still mounting; control_mount()
Expand Down
3 changes: 2 additions & 1 deletion fs/gfs2/meta_io.c
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ int gfs2_meta_read(struct gfs2_glock *gl, u64 blkno, int flags,
struct buffer_head *bh, *bhs[2];
int num = 0;

if (unlikely(gfs2_withdrawn(sdp))) {
if (unlikely(gfs2_withdrawn(sdp)) &&
(!sdp->sd_jdesc || (blkno != sdp->sd_jdesc->jd_no_addr))) {
*bhp = NULL;
return -EIO;
}
Expand Down
11 changes: 8 additions & 3 deletions fs/gfs2/ops_fstype.c
Original file line number Diff line number Diff line change
Expand Up @@ -656,14 +656,16 @@ static int init_journal(struct gfs2_sbd *sdp, int undo)

error = gfs2_glock_nq_num(sdp, sdp->sd_lockstruct.ls_jid,
&gfs2_journal_glops,
LM_ST_EXCLUSIVE, LM_FLAG_NOEXP,
LM_ST_EXCLUSIVE,
LM_FLAG_NOEXP | GL_NOCACHE,
&sdp->sd_journal_gh);
if (error) {
fs_err(sdp, "can't acquire journal glock: %d\n", error);
goto fail_jindex;
}

ip = GFS2_I(sdp->sd_jdesc->jd_inode);
sdp->sd_jinode_gl = ip->i_gl;
error = gfs2_glock_nq_init(ip->i_gl, LM_ST_SHARED,
LM_FLAG_NOEXP | GL_EXACT | GL_NOCACHE,
&sdp->sd_jinode_gh);
Expand Down Expand Up @@ -724,10 +726,13 @@ static int init_journal(struct gfs2_sbd *sdp, int undo)
return 0;

fail_jinode_gh:
if (!sdp->sd_args.ar_spectator)
/* A withdraw may have done dq/uninit so now we need to check it */
if (!sdp->sd_args.ar_spectator &&
gfs2_holder_initialized(&sdp->sd_jinode_gh))
gfs2_glock_dq_uninit(&sdp->sd_jinode_gh);
fail_journal_gh:
if (!sdp->sd_args.ar_spectator)
if (!sdp->sd_args.ar_spectator &&
gfs2_holder_initialized(&sdp->sd_journal_gh))
gfs2_glock_dq_uninit(&sdp->sd_journal_gh);
fail_jindex:
gfs2_jindex_free(sdp);
Expand Down
3 changes: 3 additions & 0 deletions fs/gfs2/quota.c
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,8 @@ int gfs2_quotad(void *data)

while (!kthread_should_stop()) {

if (gfs2_withdrawn(sdp))
goto bypass;
/* Update the master statfs file */
if (sdp->sd_statfs_force_sync) {
int error = gfs2_statfs_sync(sdp->sd_vfs, 0);
Expand All @@ -1561,6 +1563,7 @@ int gfs2_quotad(void *data)

try_to_freeze();

bypass:
t = min(quotad_timeo, statfs_timeo);

prepare_to_wait(&sdp->sd_quota_wait, &wait, TASK_INTERRUPTIBLE);
Expand Down
Loading

0 comments on commit 601ef0d

Please sign in to comment.