Right now we wait until we've committed changes to the primary superblock before we initialise any of the new secondary superblocks. This means that if we have any write errors for new secondary superblocks we end up with garbage in place rather than zeros or even an "in progress" superblock to indicate a grow operation is being done. To ensure we can write the secondary superblocks, initialise them earlier in the same loop that initialises the AG headers. We stamp the new secondary superblocks here with the old geometry, but set the "sb_inprogress" field to indicate that updates are being done to the superblock so they cannot be used. This will result in the secondary superblock fields being updated or triggering errors that will abort the grow before we commit any permanent changes. This also means we can change the update mechanism of the secondary superblocks. We know that we are going to wholly overwrite the information in the struct xfs_sb in the buffer, so there's no point reading it from disk. Just allocate an uncached buffer, zero it in memory, stamp the new superblock structure in it and write it out. If we fail to write it out, then we'll leave the existing sb (old or new w/ inprogress) on disk for repair to deal with later. Signed-Off-By: Dave Chinner <dchinner@redhat.com> Reviewed-by: Darrick J. Wong <darrick.wong@oracle.com> Signed-off-by: Darrick J. Wong <darrick.wong@oracle.com>
1116 lines
29 KiB
C
1116 lines
29 KiB
C
/*
|
|
* Copyright (c) 2000-2005 Silicon Graphics, Inc.
|
|
* All Rights Reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it would be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
#include "xfs.h"
|
|
#include "xfs_fs.h"
|
|
#include "xfs_shared.h"
|
|
#include "xfs_format.h"
|
|
#include "xfs_log_format.h"
|
|
#include "xfs_trans_resv.h"
|
|
#include "xfs_sb.h"
|
|
#include "xfs_mount.h"
|
|
#include "xfs_defer.h"
|
|
#include "xfs_da_format.h"
|
|
#include "xfs_da_btree.h"
|
|
#include "xfs_inode.h"
|
|
#include "xfs_trans.h"
|
|
#include "xfs_inode_item.h"
|
|
#include "xfs_error.h"
|
|
#include "xfs_btree.h"
|
|
#include "xfs_alloc_btree.h"
|
|
#include "xfs_alloc.h"
|
|
#include "xfs_rmap_btree.h"
|
|
#include "xfs_ialloc.h"
|
|
#include "xfs_fsops.h"
|
|
#include "xfs_itable.h"
|
|
#include "xfs_trans_space.h"
|
|
#include "xfs_rtalloc.h"
|
|
#include "xfs_trace.h"
|
|
#include "xfs_log.h"
|
|
#include "xfs_filestream.h"
|
|
#include "xfs_rmap.h"
|
|
#include "xfs_ag_resv.h"
|
|
|
|
/*
|
|
* File system operations
|
|
*/
|
|
|
|
static struct xfs_buf *
|
|
xfs_growfs_get_hdr_buf(
|
|
struct xfs_mount *mp,
|
|
xfs_daddr_t blkno,
|
|
size_t numblks,
|
|
int flags,
|
|
const struct xfs_buf_ops *ops)
|
|
{
|
|
struct xfs_buf *bp;
|
|
|
|
bp = xfs_buf_get_uncached(mp->m_ddev_targp, numblks, flags);
|
|
if (!bp)
|
|
return NULL;
|
|
|
|
xfs_buf_zero(bp, 0, BBTOB(bp->b_length));
|
|
bp->b_bn = blkno;
|
|
bp->b_maps[0].bm_bn = blkno;
|
|
bp->b_ops = ops;
|
|
|
|
return bp;
|
|
}
|
|
|
|
struct aghdr_init_data {
|
|
/* per ag data */
|
|
xfs_agnumber_t agno; /* ag to init */
|
|
xfs_extlen_t agsize; /* new AG size */
|
|
struct list_head buffer_list; /* buffer writeback list */
|
|
xfs_rfsblock_t nfree; /* cumulative new free space */
|
|
|
|
/* per header data */
|
|
xfs_daddr_t daddr; /* header location */
|
|
size_t numblks; /* size of header */
|
|
xfs_btnum_t type; /* type of btree root block */
|
|
};
|
|
|
|
/*
|
|
* Generic btree root block init function
|
|
*/
|
|
static void
|
|
xfs_btroot_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
xfs_btree_init_block(mp, bp, id->type, 0, 0, id->agno, 0);
|
|
}
|
|
|
|
/*
|
|
* Alloc btree root block init functions
|
|
*/
|
|
static void
|
|
xfs_bnoroot_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_alloc_rec *arec;
|
|
|
|
xfs_btree_init_block(mp, bp, XFS_BTNUM_BNO, 0, 1, id->agno, 0);
|
|
arec = XFS_ALLOC_REC_ADDR(mp, XFS_BUF_TO_BLOCK(bp), 1);
|
|
arec->ar_startblock = cpu_to_be32(mp->m_ag_prealloc_blocks);
|
|
arec->ar_blockcount = cpu_to_be32(id->agsize -
|
|
be32_to_cpu(arec->ar_startblock));
|
|
}
|
|
|
|
static void
|
|
xfs_cntroot_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_alloc_rec *arec;
|
|
|
|
xfs_btree_init_block(mp, bp, XFS_BTNUM_CNT, 0, 1, id->agno, 0);
|
|
arec = XFS_ALLOC_REC_ADDR(mp, XFS_BUF_TO_BLOCK(bp), 1);
|
|
arec->ar_startblock = cpu_to_be32(mp->m_ag_prealloc_blocks);
|
|
arec->ar_blockcount = cpu_to_be32(id->agsize -
|
|
be32_to_cpu(arec->ar_startblock));
|
|
}
|
|
|
|
/*
|
|
* Reverse map root block init
|
|
*/
|
|
static void
|
|
xfs_rmaproot_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_btree_block *block = XFS_BUF_TO_BLOCK(bp);
|
|
struct xfs_rmap_rec *rrec;
|
|
|
|
xfs_btree_init_block(mp, bp, XFS_BTNUM_RMAP, 0, 4, id->agno, 0);
|
|
|
|
/*
|
|
* mark the AG header regions as static metadata The BNO
|
|
* btree block is the first block after the headers, so
|
|
* it's location defines the size of region the static
|
|
* metadata consumes.
|
|
*
|
|
* Note: unlike mkfs, we never have to account for log
|
|
* space when growing the data regions
|
|
*/
|
|
rrec = XFS_RMAP_REC_ADDR(block, 1);
|
|
rrec->rm_startblock = 0;
|
|
rrec->rm_blockcount = cpu_to_be32(XFS_BNO_BLOCK(mp));
|
|
rrec->rm_owner = cpu_to_be64(XFS_RMAP_OWN_FS);
|
|
rrec->rm_offset = 0;
|
|
|
|
/* account freespace btree root blocks */
|
|
rrec = XFS_RMAP_REC_ADDR(block, 2);
|
|
rrec->rm_startblock = cpu_to_be32(XFS_BNO_BLOCK(mp));
|
|
rrec->rm_blockcount = cpu_to_be32(2);
|
|
rrec->rm_owner = cpu_to_be64(XFS_RMAP_OWN_AG);
|
|
rrec->rm_offset = 0;
|
|
|
|
/* account inode btree root blocks */
|
|
rrec = XFS_RMAP_REC_ADDR(block, 3);
|
|
rrec->rm_startblock = cpu_to_be32(XFS_IBT_BLOCK(mp));
|
|
rrec->rm_blockcount = cpu_to_be32(XFS_RMAP_BLOCK(mp) -
|
|
XFS_IBT_BLOCK(mp));
|
|
rrec->rm_owner = cpu_to_be64(XFS_RMAP_OWN_INOBT);
|
|
rrec->rm_offset = 0;
|
|
|
|
/* account for rmap btree root */
|
|
rrec = XFS_RMAP_REC_ADDR(block, 4);
|
|
rrec->rm_startblock = cpu_to_be32(XFS_RMAP_BLOCK(mp));
|
|
rrec->rm_blockcount = cpu_to_be32(1);
|
|
rrec->rm_owner = cpu_to_be64(XFS_RMAP_OWN_AG);
|
|
rrec->rm_offset = 0;
|
|
|
|
/* account for refc btree root */
|
|
if (xfs_sb_version_hasreflink(&mp->m_sb)) {
|
|
rrec = XFS_RMAP_REC_ADDR(block, 5);
|
|
rrec->rm_startblock = cpu_to_be32(xfs_refc_block(mp));
|
|
rrec->rm_blockcount = cpu_to_be32(1);
|
|
rrec->rm_owner = cpu_to_be64(XFS_RMAP_OWN_REFC);
|
|
rrec->rm_offset = 0;
|
|
be16_add_cpu(&block->bb_numrecs, 1);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Initialise new secondary superblocks with the pre-grow geometry, but mark
|
|
* them as "in progress" so we know they haven't yet been activated. This will
|
|
* get cleared when the update with the new geometry information is done after
|
|
* changes to the primary are committed. This isn't strictly necessary, but we
|
|
* get it for free with the delayed buffer write lists and it means we can tell
|
|
* if a grow operation didn't complete properly after the fact.
|
|
*/
|
|
static void
|
|
xfs_sbblock_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_dsb *dsb = XFS_BUF_TO_SBP(bp);
|
|
|
|
xfs_sb_to_disk(dsb, &mp->m_sb);
|
|
dsb->sb_inprogress = 1;
|
|
}
|
|
|
|
static void
|
|
xfs_agfblock_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_agf *agf = XFS_BUF_TO_AGF(bp);
|
|
xfs_extlen_t tmpsize;
|
|
|
|
agf->agf_magicnum = cpu_to_be32(XFS_AGF_MAGIC);
|
|
agf->agf_versionnum = cpu_to_be32(XFS_AGF_VERSION);
|
|
agf->agf_seqno = cpu_to_be32(id->agno);
|
|
agf->agf_length = cpu_to_be32(id->agsize);
|
|
agf->agf_roots[XFS_BTNUM_BNOi] = cpu_to_be32(XFS_BNO_BLOCK(mp));
|
|
agf->agf_roots[XFS_BTNUM_CNTi] = cpu_to_be32(XFS_CNT_BLOCK(mp));
|
|
agf->agf_levels[XFS_BTNUM_BNOi] = cpu_to_be32(1);
|
|
agf->agf_levels[XFS_BTNUM_CNTi] = cpu_to_be32(1);
|
|
if (xfs_sb_version_hasrmapbt(&mp->m_sb)) {
|
|
agf->agf_roots[XFS_BTNUM_RMAPi] =
|
|
cpu_to_be32(XFS_RMAP_BLOCK(mp));
|
|
agf->agf_levels[XFS_BTNUM_RMAPi] = cpu_to_be32(1);
|
|
agf->agf_rmap_blocks = cpu_to_be32(1);
|
|
}
|
|
|
|
agf->agf_flfirst = cpu_to_be32(1);
|
|
agf->agf_fllast = 0;
|
|
agf->agf_flcount = 0;
|
|
tmpsize = id->agsize - mp->m_ag_prealloc_blocks;
|
|
agf->agf_freeblks = cpu_to_be32(tmpsize);
|
|
agf->agf_longest = cpu_to_be32(tmpsize);
|
|
if (xfs_sb_version_hascrc(&mp->m_sb))
|
|
uuid_copy(&agf->agf_uuid, &mp->m_sb.sb_meta_uuid);
|
|
if (xfs_sb_version_hasreflink(&mp->m_sb)) {
|
|
agf->agf_refcount_root = cpu_to_be32(
|
|
xfs_refc_block(mp));
|
|
agf->agf_refcount_level = cpu_to_be32(1);
|
|
agf->agf_refcount_blocks = cpu_to_be32(1);
|
|
}
|
|
}
|
|
|
|
static void
|
|
xfs_agflblock_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_agfl *agfl = XFS_BUF_TO_AGFL(bp);
|
|
__be32 *agfl_bno;
|
|
int bucket;
|
|
|
|
if (xfs_sb_version_hascrc(&mp->m_sb)) {
|
|
agfl->agfl_magicnum = cpu_to_be32(XFS_AGFL_MAGIC);
|
|
agfl->agfl_seqno = cpu_to_be32(id->agno);
|
|
uuid_copy(&agfl->agfl_uuid, &mp->m_sb.sb_meta_uuid);
|
|
}
|
|
|
|
agfl_bno = XFS_BUF_TO_AGFL_BNO(mp, bp);
|
|
for (bucket = 0; bucket < xfs_agfl_size(mp); bucket++)
|
|
agfl_bno[bucket] = cpu_to_be32(NULLAGBLOCK);
|
|
}
|
|
|
|
static void
|
|
xfs_agiblock_init(
|
|
struct xfs_mount *mp,
|
|
struct xfs_buf *bp,
|
|
struct aghdr_init_data *id)
|
|
{
|
|
struct xfs_agi *agi = XFS_BUF_TO_AGI(bp);
|
|
int bucket;
|
|
|
|
agi->agi_magicnum = cpu_to_be32(XFS_AGI_MAGIC);
|
|
agi->agi_versionnum = cpu_to_be32(XFS_AGI_VERSION);
|
|
agi->agi_seqno = cpu_to_be32(id->agno);
|
|
agi->agi_length = cpu_to_be32(id->agsize);
|
|
agi->agi_count = 0;
|
|
agi->agi_root = cpu_to_be32(XFS_IBT_BLOCK(mp));
|
|
agi->agi_level = cpu_to_be32(1);
|
|
agi->agi_freecount = 0;
|
|
agi->agi_newino = cpu_to_be32(NULLAGINO);
|
|
agi->agi_dirino = cpu_to_be32(NULLAGINO);
|
|
if (xfs_sb_version_hascrc(&mp->m_sb))
|
|
uuid_copy(&agi->agi_uuid, &mp->m_sb.sb_meta_uuid);
|
|
if (xfs_sb_version_hasfinobt(&mp->m_sb)) {
|
|
agi->agi_free_root = cpu_to_be32(XFS_FIBT_BLOCK(mp));
|
|
agi->agi_free_level = cpu_to_be32(1);
|
|
}
|
|
for (bucket = 0; bucket < XFS_AGI_UNLINKED_BUCKETS; bucket++)
|
|
agi->agi_unlinked[bucket] = cpu_to_be32(NULLAGINO);
|
|
}
|
|
|
|
typedef void (*aghdr_init_work_f)(struct xfs_mount *mp, struct xfs_buf *bp,
|
|
struct aghdr_init_data *id);
|
|
static int
|
|
xfs_growfs_init_aghdr(
|
|
struct xfs_mount *mp,
|
|
struct aghdr_init_data *id,
|
|
aghdr_init_work_f work,
|
|
const struct xfs_buf_ops *ops)
|
|
|
|
{
|
|
struct xfs_buf *bp;
|
|
|
|
bp = xfs_growfs_get_hdr_buf(mp, id->daddr, id->numblks, 0, ops);
|
|
if (!bp)
|
|
return -ENOMEM;
|
|
|
|
(*work)(mp, bp, id);
|
|
|
|
xfs_buf_delwri_queue(bp, &id->buffer_list);
|
|
xfs_buf_relse(bp);
|
|
return 0;
|
|
}
|
|
|
|
struct xfs_aghdr_grow_data {
|
|
xfs_daddr_t daddr;
|
|
size_t numblks;
|
|
const struct xfs_buf_ops *ops;
|
|
aghdr_init_work_f work;
|
|
xfs_btnum_t type;
|
|
bool need_init;
|
|
};
|
|
|
|
/*
|
|
* Write new AG headers to disk. Non-transactional, but written
|
|
* synchronously so they are completed prior to the growfs transaction
|
|
* being logged.
|
|
*/
|
|
static int
|
|
xfs_grow_ag_headers(
|
|
struct xfs_mount *mp,
|
|
struct aghdr_init_data *id)
|
|
|
|
{
|
|
struct xfs_aghdr_grow_data aghdr_data[] = {
|
|
{ /* SB */
|
|
.daddr = XFS_AG_DADDR(mp, id->agno, XFS_SB_DADDR),
|
|
.numblks = XFS_FSS_TO_BB(mp, 1),
|
|
.ops = &xfs_sb_buf_ops,
|
|
.work = &xfs_sbblock_init,
|
|
.need_init = true
|
|
},
|
|
{ /* AGF */
|
|
.daddr = XFS_AG_DADDR(mp, id->agno, XFS_AGF_DADDR(mp)),
|
|
.numblks = XFS_FSS_TO_BB(mp, 1),
|
|
.ops = &xfs_agf_buf_ops,
|
|
.work = &xfs_agfblock_init,
|
|
.need_init = true
|
|
},
|
|
{ /* AGFL */
|
|
.daddr = XFS_AG_DADDR(mp, id->agno, XFS_AGFL_DADDR(mp)),
|
|
.numblks = XFS_FSS_TO_BB(mp, 1),
|
|
.ops = &xfs_agfl_buf_ops,
|
|
.work = &xfs_agflblock_init,
|
|
.need_init = true
|
|
},
|
|
{ /* AGI */
|
|
.daddr = XFS_AG_DADDR(mp, id->agno, XFS_AGI_DADDR(mp)),
|
|
.numblks = XFS_FSS_TO_BB(mp, 1),
|
|
.ops = &xfs_agi_buf_ops,
|
|
.work = &xfs_agiblock_init,
|
|
.need_init = true
|
|
},
|
|
{ /* BNO root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, XFS_BNO_BLOCK(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_allocbt_buf_ops,
|
|
.work = &xfs_bnoroot_init,
|
|
.need_init = true
|
|
},
|
|
{ /* CNT root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, XFS_CNT_BLOCK(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_allocbt_buf_ops,
|
|
.work = &xfs_cntroot_init,
|
|
.need_init = true
|
|
},
|
|
{ /* INO root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, XFS_IBT_BLOCK(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_inobt_buf_ops,
|
|
.work = &xfs_btroot_init,
|
|
.type = XFS_BTNUM_INO,
|
|
.need_init = true
|
|
},
|
|
{ /* FINO root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, XFS_FIBT_BLOCK(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_inobt_buf_ops,
|
|
.work = &xfs_btroot_init,
|
|
.type = XFS_BTNUM_FINO,
|
|
.need_init = xfs_sb_version_hasfinobt(&mp->m_sb)
|
|
},
|
|
{ /* RMAP root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, XFS_RMAP_BLOCK(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_rmapbt_buf_ops,
|
|
.work = &xfs_rmaproot_init,
|
|
.need_init = xfs_sb_version_hasrmapbt(&mp->m_sb)
|
|
},
|
|
{ /* REFC root block */
|
|
.daddr = XFS_AGB_TO_DADDR(mp, id->agno, xfs_refc_block(mp)),
|
|
.numblks = BTOBB(mp->m_sb.sb_blocksize),
|
|
.ops = &xfs_refcountbt_buf_ops,
|
|
.work = &xfs_btroot_init,
|
|
.type = XFS_BTNUM_REFC,
|
|
.need_init = xfs_sb_version_hasreflink(&mp->m_sb)
|
|
},
|
|
{ /* NULL terminating block */
|
|
.daddr = XFS_BUF_DADDR_NULL,
|
|
}
|
|
};
|
|
struct xfs_aghdr_grow_data *dp;
|
|
int error = 0;
|
|
|
|
/* Account for AG free space in new AG */
|
|
id->nfree += id->agsize - mp->m_ag_prealloc_blocks;
|
|
for (dp = &aghdr_data[0]; dp->daddr != XFS_BUF_DADDR_NULL; dp++) {
|
|
if (!dp->need_init)
|
|
continue;
|
|
|
|
id->daddr = dp->daddr;
|
|
id->numblks = dp->numblks;
|
|
id->type = dp->type;
|
|
error = xfs_growfs_init_aghdr(mp, id, dp->work, dp->ops);
|
|
if (error)
|
|
break;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
static int
|
|
xfs_growfs_data_private(
|
|
xfs_mount_t *mp, /* mount point for filesystem */
|
|
xfs_growfs_data_t *in) /* growfs data input struct */
|
|
{
|
|
xfs_agf_t *agf;
|
|
xfs_agi_t *agi;
|
|
xfs_buf_t *bp;
|
|
int error;
|
|
xfs_agnumber_t nagcount;
|
|
xfs_agnumber_t nagimax = 0;
|
|
xfs_rfsblock_t nb, nb_mod;
|
|
xfs_rfsblock_t new;
|
|
xfs_agnumber_t oagcount;
|
|
xfs_trans_t *tp;
|
|
LIST_HEAD (buffer_list);
|
|
struct aghdr_init_data id = {};
|
|
|
|
nb = in->newblocks;
|
|
if (nb < mp->m_sb.sb_dblocks)
|
|
return -EINVAL;
|
|
if ((error = xfs_sb_validate_fsb_count(&mp->m_sb, nb)))
|
|
return error;
|
|
error = xfs_buf_read_uncached(mp->m_ddev_targp,
|
|
XFS_FSB_TO_BB(mp, nb) - XFS_FSS_TO_BB(mp, 1),
|
|
XFS_FSS_TO_BB(mp, 1), 0, &bp, NULL);
|
|
if (error)
|
|
return error;
|
|
xfs_buf_relse(bp);
|
|
|
|
new = nb; /* use new as a temporary here */
|
|
nb_mod = do_div(new, mp->m_sb.sb_agblocks);
|
|
nagcount = new + (nb_mod != 0);
|
|
if (nb_mod && nb_mod < XFS_MIN_AG_BLOCKS) {
|
|
nagcount--;
|
|
nb = (xfs_rfsblock_t)nagcount * mp->m_sb.sb_agblocks;
|
|
if (nb < mp->m_sb.sb_dblocks)
|
|
return -EINVAL;
|
|
}
|
|
new = nb - mp->m_sb.sb_dblocks;
|
|
oagcount = mp->m_sb.sb_agcount;
|
|
|
|
/* allocate the new per-ag structures */
|
|
if (nagcount > oagcount) {
|
|
error = xfs_initialize_perag(mp, nagcount, &nagimax);
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
error = xfs_trans_alloc(mp, &M_RES(mp)->tr_growdata,
|
|
XFS_GROWFS_SPACE_RES(mp), 0, XFS_TRANS_RESERVE, &tp);
|
|
if (error)
|
|
return error;
|
|
|
|
/*
|
|
* Write new AG headers to disk. Non-transactional, but need to be
|
|
* written and completed prior to the growfs transaction being logged.
|
|
* To do this, we use a delayed write buffer list and wait for
|
|
* submission and IO completion of the list as a whole. This allows the
|
|
* IO subsystem to merge all the AG headers in a single AG into a single
|
|
* IO and hide most of the latency of the IO from us.
|
|
*
|
|
* This also means that if we get an error whilst building the buffer
|
|
* list to write, we can cancel the entire list without having written
|
|
* anything.
|
|
*/
|
|
INIT_LIST_HEAD(&id.buffer_list);
|
|
for (id.agno = nagcount - 1;
|
|
id.agno >= oagcount;
|
|
id.agno--, new -= id.agsize) {
|
|
|
|
if (id.agno == nagcount - 1)
|
|
id.agsize = nb -
|
|
(id.agno * (xfs_rfsblock_t)mp->m_sb.sb_agblocks);
|
|
else
|
|
id.agsize = mp->m_sb.sb_agblocks;
|
|
|
|
error = xfs_grow_ag_headers(mp, &id);
|
|
if (error) {
|
|
xfs_buf_delwri_cancel(&id.buffer_list);
|
|
goto out_trans_cancel;
|
|
}
|
|
}
|
|
error = xfs_buf_delwri_submit(&id.buffer_list);
|
|
if (error)
|
|
goto out_trans_cancel;
|
|
|
|
xfs_trans_agblocks_delta(tp, id.nfree);
|
|
|
|
/*
|
|
* There are new blocks in the old last a.g.
|
|
*/
|
|
if (new) {
|
|
struct xfs_owner_info oinfo;
|
|
|
|
/*
|
|
* Change the agi length.
|
|
*/
|
|
error = xfs_ialloc_read_agi(mp, tp, id.agno, &bp);
|
|
if (error)
|
|
goto out_trans_cancel;
|
|
|
|
ASSERT(bp);
|
|
agi = XFS_BUF_TO_AGI(bp);
|
|
be32_add_cpu(&agi->agi_length, new);
|
|
ASSERT(nagcount == oagcount ||
|
|
be32_to_cpu(agi->agi_length) == mp->m_sb.sb_agblocks);
|
|
xfs_ialloc_log_agi(tp, bp, XFS_AGI_LENGTH);
|
|
|
|
/*
|
|
* Change agf length.
|
|
*/
|
|
error = xfs_alloc_read_agf(mp, tp, id.agno, 0, &bp);
|
|
if (error)
|
|
goto out_trans_cancel;
|
|
|
|
ASSERT(bp);
|
|
agf = XFS_BUF_TO_AGF(bp);
|
|
be32_add_cpu(&agf->agf_length, new);
|
|
ASSERT(be32_to_cpu(agf->agf_length) ==
|
|
be32_to_cpu(agi->agi_length));
|
|
|
|
xfs_alloc_log_agf(tp, bp, XFS_AGF_LENGTH);
|
|
|
|
/*
|
|
* Free the new space.
|
|
*
|
|
* XFS_RMAP_OWN_NULL is used here to tell the rmap btree that
|
|
* this doesn't actually exist in the rmap btree.
|
|
*/
|
|
xfs_rmap_ag_owner(&oinfo, XFS_RMAP_OWN_NULL);
|
|
error = xfs_rmap_free(tp, bp, id.agno,
|
|
be32_to_cpu(agf->agf_length) - new,
|
|
new, &oinfo);
|
|
if (error)
|
|
goto out_trans_cancel;
|
|
error = xfs_free_extent(tp,
|
|
XFS_AGB_TO_FSB(mp, id.agno,
|
|
be32_to_cpu(agf->agf_length) - new),
|
|
new, &oinfo, XFS_AG_RESV_NONE);
|
|
if (error)
|
|
goto out_trans_cancel;
|
|
}
|
|
|
|
/*
|
|
* Update changed superblock fields transactionally. These are not
|
|
* seen by the rest of the world until the transaction commit applies
|
|
* them atomically to the superblock.
|
|
*/
|
|
if (nagcount > oagcount)
|
|
xfs_trans_mod_sb(tp, XFS_TRANS_SB_AGCOUNT, nagcount - oagcount);
|
|
if (nb > mp->m_sb.sb_dblocks)
|
|
xfs_trans_mod_sb(tp, XFS_TRANS_SB_DBLOCKS,
|
|
nb - mp->m_sb.sb_dblocks);
|
|
if (id.nfree)
|
|
xfs_trans_mod_sb(tp, XFS_TRANS_SB_FDBLOCKS, id.nfree);
|
|
xfs_trans_set_sync(tp);
|
|
error = xfs_trans_commit(tp);
|
|
if (error)
|
|
return error;
|
|
|
|
/* New allocation groups fully initialized, so update mount struct */
|
|
if (nagimax)
|
|
mp->m_maxagi = nagimax;
|
|
xfs_set_low_space_thresholds(mp);
|
|
mp->m_alloc_set_aside = xfs_alloc_set_aside(mp);
|
|
|
|
/*
|
|
* If we expanded the last AG, free the per-AG reservation
|
|
* so we can reinitialize it with the new size.
|
|
*/
|
|
if (new) {
|
|
struct xfs_perag *pag;
|
|
|
|
pag = xfs_perag_get(mp, id.agno);
|
|
error = xfs_ag_resv_free(pag);
|
|
xfs_perag_put(pag);
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* Reserve AG metadata blocks. ENOSPC here does not mean there was a
|
|
* growfs failure, just that there still isn't space for new user data
|
|
* after the grow has been run.
|
|
*/
|
|
error = xfs_fs_reserve_ag_blocks(mp);
|
|
if (error == -ENOSPC)
|
|
error = 0;
|
|
return error;
|
|
|
|
out_trans_cancel:
|
|
xfs_trans_cancel(tp);
|
|
return error;
|
|
}
|
|
|
|
static int
|
|
xfs_growfs_log_private(
|
|
xfs_mount_t *mp, /* mount point for filesystem */
|
|
xfs_growfs_log_t *in) /* growfs log input struct */
|
|
{
|
|
xfs_extlen_t nb;
|
|
|
|
nb = in->newblocks;
|
|
if (nb < XFS_MIN_LOG_BLOCKS || nb < XFS_B_TO_FSB(mp, XFS_MIN_LOG_BYTES))
|
|
return -EINVAL;
|
|
if (nb == mp->m_sb.sb_logblocks &&
|
|
in->isint == (mp->m_sb.sb_logstart != 0))
|
|
return -EINVAL;
|
|
/*
|
|
* Moving the log is hard, need new interfaces to sync
|
|
* the log first, hold off all activity while moving it.
|
|
* Can have shorter or longer log in the same space,
|
|
* or transform internal to external log or vice versa.
|
|
*/
|
|
return -ENOSYS;
|
|
}
|
|
|
|
static int
|
|
xfs_growfs_imaxpct(
|
|
struct xfs_mount *mp,
|
|
__u32 imaxpct)
|
|
{
|
|
struct xfs_trans *tp;
|
|
int dpct;
|
|
int error;
|
|
|
|
if (imaxpct > 100)
|
|
return -EINVAL;
|
|
|
|
error = xfs_trans_alloc(mp, &M_RES(mp)->tr_growdata,
|
|
XFS_GROWFS_SPACE_RES(mp), 0, XFS_TRANS_RESERVE, &tp);
|
|
if (error)
|
|
return error;
|
|
|
|
dpct = imaxpct - mp->m_sb.sb_imax_pct;
|
|
xfs_trans_mod_sb(tp, XFS_TRANS_SB_IMAXPCT, dpct);
|
|
xfs_trans_set_sync(tp);
|
|
return xfs_trans_commit(tp);
|
|
}
|
|
|
|
/*
|
|
* After a grow operation, we need to update all the secondary superblocks
|
|
* to match the new state of the primary. Because we are completely overwriting
|
|
* all the existing fields in the secondary superblock buffers, there is no need
|
|
* to read them in from disk. Just get a new buffer, stamp it and write it.
|
|
*
|
|
* The sb buffers need to be cached here so that we serialise against scrub
|
|
* scanning secondary superblocks, but we don't want to keep it in memory once
|
|
* it is written so we mark it as a one-shot buffer.
|
|
*/
|
|
static int
|
|
xfs_growfs_update_superblocks(
|
|
struct xfs_mount *mp)
|
|
{
|
|
xfs_agnumber_t agno;
|
|
int saved_error = 0;
|
|
int error = 0;
|
|
LIST_HEAD (buffer_list);
|
|
|
|
/* update secondary superblocks. */
|
|
for (agno = 1; agno < mp->m_sb.sb_agcount; agno++) {
|
|
struct xfs_buf *bp;
|
|
|
|
bp = xfs_buf_get(mp->m_ddev_targp,
|
|
XFS_AG_DADDR(mp, agno, XFS_SB_DADDR),
|
|
XFS_FSS_TO_BB(mp, 1), 0);
|
|
/*
|
|
* If we get an error reading or writing alternate superblocks,
|
|
* continue. xfs_repair chooses the "best" superblock based
|
|
* on most matches; if we break early, we'll leave more
|
|
* superblocks un-updated than updated, and xfs_repair may
|
|
* pick them over the properly-updated primary.
|
|
*/
|
|
if (!bp) {
|
|
xfs_warn(mp,
|
|
"error allocating secondary superblock for ag %d",
|
|
agno);
|
|
if (!saved_error)
|
|
saved_error = -ENOMEM;
|
|
continue;
|
|
}
|
|
|
|
bp->b_ops = &xfs_sb_buf_ops;
|
|
xfs_buf_oneshot(bp);
|
|
xfs_buf_zero(bp, 0, BBTOB(bp->b_length));
|
|
xfs_sb_to_disk(XFS_BUF_TO_SBP(bp), &mp->m_sb);
|
|
xfs_buf_delwri_queue(bp, &buffer_list);
|
|
xfs_buf_relse(bp);
|
|
|
|
/* don't hold too many buffers at once */
|
|
if (agno % 16)
|
|
continue;
|
|
|
|
error = xfs_buf_delwri_submit(&buffer_list);
|
|
if (error) {
|
|
xfs_warn(mp,
|
|
"write error %d updating a secondary superblock near ag %d",
|
|
error, agno);
|
|
if (!saved_error)
|
|
saved_error = error;
|
|
continue;
|
|
}
|
|
}
|
|
error = xfs_buf_delwri_submit(&buffer_list);
|
|
if (error) {
|
|
xfs_warn(mp,
|
|
"write error %d updating a secondary superblock near ag %d",
|
|
error, agno);
|
|
}
|
|
|
|
return saved_error ? saved_error : error;
|
|
}
|
|
|
|
/*
|
|
* protected versions of growfs function acquire and release locks on the mount
|
|
* point - exported through ioctls: XFS_IOC_FSGROWFSDATA, XFS_IOC_FSGROWFSLOG,
|
|
* XFS_IOC_FSGROWFSRT
|
|
*/
|
|
int
|
|
xfs_growfs_data(
|
|
struct xfs_mount *mp,
|
|
struct xfs_growfs_data *in)
|
|
{
|
|
int error = 0;
|
|
|
|
if (!capable(CAP_SYS_ADMIN))
|
|
return -EPERM;
|
|
if (!mutex_trylock(&mp->m_growlock))
|
|
return -EWOULDBLOCK;
|
|
|
|
/* update imaxpct separately to the physical grow of the filesystem */
|
|
if (in->imaxpct != mp->m_sb.sb_imax_pct) {
|
|
error = xfs_growfs_imaxpct(mp, in->imaxpct);
|
|
if (error)
|
|
goto out_error;
|
|
}
|
|
|
|
if (in->newblocks != mp->m_sb.sb_dblocks) {
|
|
error = xfs_growfs_data_private(mp, in);
|
|
if (error)
|
|
goto out_error;
|
|
}
|
|
|
|
/* Post growfs calculations needed to reflect new state in operations */
|
|
if (mp->m_sb.sb_imax_pct) {
|
|
uint64_t icount = mp->m_sb.sb_dblocks * mp->m_sb.sb_imax_pct;
|
|
do_div(icount, 100);
|
|
mp->m_maxicount = icount << mp->m_sb.sb_inopblog;
|
|
} else
|
|
mp->m_maxicount = 0;
|
|
|
|
/* Update secondary superblocks now the physical grow has completed */
|
|
error = xfs_growfs_update_superblocks(mp);
|
|
|
|
out_error:
|
|
/*
|
|
* Increment the generation unconditionally, the error could be from
|
|
* updating the secondary superblocks, in which case the new size
|
|
* is live already.
|
|
*/
|
|
mp->m_generation++;
|
|
mutex_unlock(&mp->m_growlock);
|
|
return error;
|
|
}
|
|
|
|
int
|
|
xfs_growfs_log(
|
|
xfs_mount_t *mp,
|
|
xfs_growfs_log_t *in)
|
|
{
|
|
int error;
|
|
|
|
if (!capable(CAP_SYS_ADMIN))
|
|
return -EPERM;
|
|
if (!mutex_trylock(&mp->m_growlock))
|
|
return -EWOULDBLOCK;
|
|
error = xfs_growfs_log_private(mp, in);
|
|
mutex_unlock(&mp->m_growlock);
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* exported through ioctl XFS_IOC_FSCOUNTS
|
|
*/
|
|
|
|
int
|
|
xfs_fs_counts(
|
|
xfs_mount_t *mp,
|
|
xfs_fsop_counts_t *cnt)
|
|
{
|
|
cnt->allocino = percpu_counter_read_positive(&mp->m_icount);
|
|
cnt->freeino = percpu_counter_read_positive(&mp->m_ifree);
|
|
cnt->freedata = percpu_counter_read_positive(&mp->m_fdblocks) -
|
|
mp->m_alloc_set_aside;
|
|
|
|
spin_lock(&mp->m_sb_lock);
|
|
cnt->freertx = mp->m_sb.sb_frextents;
|
|
spin_unlock(&mp->m_sb_lock);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* exported through ioctl XFS_IOC_SET_RESBLKS & XFS_IOC_GET_RESBLKS
|
|
*
|
|
* xfs_reserve_blocks is called to set m_resblks
|
|
* in the in-core mount table. The number of unused reserved blocks
|
|
* is kept in m_resblks_avail.
|
|
*
|
|
* Reserve the requested number of blocks if available. Otherwise return
|
|
* as many as possible to satisfy the request. The actual number
|
|
* reserved are returned in outval
|
|
*
|
|
* A null inval pointer indicates that only the current reserved blocks
|
|
* available should be returned no settings are changed.
|
|
*/
|
|
|
|
int
|
|
xfs_reserve_blocks(
|
|
xfs_mount_t *mp,
|
|
uint64_t *inval,
|
|
xfs_fsop_resblks_t *outval)
|
|
{
|
|
int64_t lcounter, delta;
|
|
int64_t fdblks_delta = 0;
|
|
uint64_t request;
|
|
int64_t free;
|
|
int error = 0;
|
|
|
|
/* If inval is null, report current values and return */
|
|
if (inval == (uint64_t *)NULL) {
|
|
if (!outval)
|
|
return -EINVAL;
|
|
outval->resblks = mp->m_resblks;
|
|
outval->resblks_avail = mp->m_resblks_avail;
|
|
return 0;
|
|
}
|
|
|
|
request = *inval;
|
|
|
|
/*
|
|
* With per-cpu counters, this becomes an interesting problem. we need
|
|
* to work out if we are freeing or allocation blocks first, then we can
|
|
* do the modification as necessary.
|
|
*
|
|
* We do this under the m_sb_lock so that if we are near ENOSPC, we will
|
|
* hold out any changes while we work out what to do. This means that
|
|
* the amount of free space can change while we do this, so we need to
|
|
* retry if we end up trying to reserve more space than is available.
|
|
*/
|
|
spin_lock(&mp->m_sb_lock);
|
|
|
|
/*
|
|
* If our previous reservation was larger than the current value,
|
|
* then move any unused blocks back to the free pool. Modify the resblks
|
|
* counters directly since we shouldn't have any problems unreserving
|
|
* space.
|
|
*/
|
|
if (mp->m_resblks > request) {
|
|
lcounter = mp->m_resblks_avail - request;
|
|
if (lcounter > 0) { /* release unused blocks */
|
|
fdblks_delta = lcounter;
|
|
mp->m_resblks_avail -= lcounter;
|
|
}
|
|
mp->m_resblks = request;
|
|
if (fdblks_delta) {
|
|
spin_unlock(&mp->m_sb_lock);
|
|
error = xfs_mod_fdblocks(mp, fdblks_delta, 0);
|
|
spin_lock(&mp->m_sb_lock);
|
|
}
|
|
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* If the request is larger than the current reservation, reserve the
|
|
* blocks before we update the reserve counters. Sample m_fdblocks and
|
|
* perform a partial reservation if the request exceeds free space.
|
|
*/
|
|
error = -ENOSPC;
|
|
do {
|
|
free = percpu_counter_sum(&mp->m_fdblocks) -
|
|
mp->m_alloc_set_aside;
|
|
if (!free)
|
|
break;
|
|
|
|
delta = request - mp->m_resblks;
|
|
lcounter = free - delta;
|
|
if (lcounter < 0)
|
|
/* We can't satisfy the request, just get what we can */
|
|
fdblks_delta = free;
|
|
else
|
|
fdblks_delta = delta;
|
|
|
|
/*
|
|
* We'll either succeed in getting space from the free block
|
|
* count or we'll get an ENOSPC. If we get a ENOSPC, it means
|
|
* things changed while we were calculating fdblks_delta and so
|
|
* we should try again to see if there is anything left to
|
|
* reserve.
|
|
*
|
|
* Don't set the reserved flag here - we don't want to reserve
|
|
* the extra reserve blocks from the reserve.....
|
|
*/
|
|
spin_unlock(&mp->m_sb_lock);
|
|
error = xfs_mod_fdblocks(mp, -fdblks_delta, 0);
|
|
spin_lock(&mp->m_sb_lock);
|
|
} while (error == -ENOSPC);
|
|
|
|
/*
|
|
* Update the reserve counters if blocks have been successfully
|
|
* allocated.
|
|
*/
|
|
if (!error && fdblks_delta) {
|
|
mp->m_resblks += fdblks_delta;
|
|
mp->m_resblks_avail += fdblks_delta;
|
|
}
|
|
|
|
out:
|
|
if (outval) {
|
|
outval->resblks = mp->m_resblks;
|
|
outval->resblks_avail = mp->m_resblks_avail;
|
|
}
|
|
|
|
spin_unlock(&mp->m_sb_lock);
|
|
return error;
|
|
}
|
|
|
|
int
|
|
xfs_fs_goingdown(
|
|
xfs_mount_t *mp,
|
|
uint32_t inflags)
|
|
{
|
|
switch (inflags) {
|
|
case XFS_FSOP_GOING_FLAGS_DEFAULT: {
|
|
struct super_block *sb = freeze_bdev(mp->m_super->s_bdev);
|
|
|
|
if (sb && !IS_ERR(sb)) {
|
|
xfs_force_shutdown(mp, SHUTDOWN_FORCE_UMOUNT);
|
|
thaw_bdev(sb->s_bdev, sb);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case XFS_FSOP_GOING_FLAGS_LOGFLUSH:
|
|
xfs_force_shutdown(mp, SHUTDOWN_FORCE_UMOUNT);
|
|
break;
|
|
case XFS_FSOP_GOING_FLAGS_NOLOGFLUSH:
|
|
xfs_force_shutdown(mp,
|
|
SHUTDOWN_FORCE_UMOUNT | SHUTDOWN_LOG_IO_ERROR);
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Force a shutdown of the filesystem instantly while keeping the filesystem
|
|
* consistent. We don't do an unmount here; just shutdown the shop, make sure
|
|
* that absolutely nothing persistent happens to this filesystem after this
|
|
* point.
|
|
*/
|
|
void
|
|
xfs_do_force_shutdown(
|
|
xfs_mount_t *mp,
|
|
int flags,
|
|
char *fname,
|
|
int lnnum)
|
|
{
|
|
int logerror;
|
|
|
|
logerror = flags & SHUTDOWN_LOG_IO_ERROR;
|
|
|
|
if (!(flags & SHUTDOWN_FORCE_UMOUNT)) {
|
|
xfs_notice(mp,
|
|
"%s(0x%x) called from line %d of file %s. Return address = "PTR_FMT,
|
|
__func__, flags, lnnum, fname, __return_address);
|
|
}
|
|
/*
|
|
* No need to duplicate efforts.
|
|
*/
|
|
if (XFS_FORCED_SHUTDOWN(mp) && !logerror)
|
|
return;
|
|
|
|
/*
|
|
* This flags XFS_MOUNT_FS_SHUTDOWN, makes sure that we don't
|
|
* queue up anybody new on the log reservations, and wakes up
|
|
* everybody who's sleeping on log reservations to tell them
|
|
* the bad news.
|
|
*/
|
|
if (xfs_log_force_umount(mp, logerror))
|
|
return;
|
|
|
|
if (flags & SHUTDOWN_CORRUPT_INCORE) {
|
|
xfs_alert_tag(mp, XFS_PTAG_SHUTDOWN_CORRUPT,
|
|
"Corruption of in-memory data detected. Shutting down filesystem");
|
|
if (XFS_ERRLEVEL_HIGH <= xfs_error_level)
|
|
xfs_stack_trace();
|
|
} else if (!(flags & SHUTDOWN_FORCE_UMOUNT)) {
|
|
if (logerror) {
|
|
xfs_alert_tag(mp, XFS_PTAG_SHUTDOWN_LOGERROR,
|
|
"Log I/O Error Detected. Shutting down filesystem");
|
|
} else if (flags & SHUTDOWN_DEVICE_REQ) {
|
|
xfs_alert_tag(mp, XFS_PTAG_SHUTDOWN_IOERROR,
|
|
"All device paths lost. Shutting down filesystem");
|
|
} else if (!(flags & SHUTDOWN_REMOTE_REQ)) {
|
|
xfs_alert_tag(mp, XFS_PTAG_SHUTDOWN_IOERROR,
|
|
"I/O Error Detected. Shutting down filesystem");
|
|
}
|
|
}
|
|
if (!(flags & SHUTDOWN_FORCE_UMOUNT)) {
|
|
xfs_alert(mp,
|
|
"Please umount the filesystem and rectify the problem(s)");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Reserve free space for per-AG metadata.
|
|
*/
|
|
int
|
|
xfs_fs_reserve_ag_blocks(
|
|
struct xfs_mount *mp)
|
|
{
|
|
xfs_agnumber_t agno;
|
|
struct xfs_perag *pag;
|
|
int error = 0;
|
|
int err2;
|
|
|
|
for (agno = 0; agno < mp->m_sb.sb_agcount; agno++) {
|
|
pag = xfs_perag_get(mp, agno);
|
|
err2 = xfs_ag_resv_init(pag);
|
|
xfs_perag_put(pag);
|
|
if (err2 && !error)
|
|
error = err2;
|
|
}
|
|
|
|
if (error && error != -ENOSPC) {
|
|
xfs_warn(mp,
|
|
"Error %d reserving per-AG metadata reserve pool.", error);
|
|
xfs_force_shutdown(mp, SHUTDOWN_CORRUPT_INCORE);
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* Free space reserved for per-AG metadata.
|
|
*/
|
|
int
|
|
xfs_fs_unreserve_ag_blocks(
|
|
struct xfs_mount *mp)
|
|
{
|
|
xfs_agnumber_t agno;
|
|
struct xfs_perag *pag;
|
|
int error = 0;
|
|
int err2;
|
|
|
|
for (agno = 0; agno < mp->m_sb.sb_agcount; agno++) {
|
|
pag = xfs_perag_get(mp, agno);
|
|
err2 = xfs_ag_resv_free(pag);
|
|
xfs_perag_put(pag);
|
|
if (err2 && !error)
|
|
error = err2;
|
|
}
|
|
|
|
if (error)
|
|
xfs_warn(mp,
|
|
"Error %d freeing per-AG metadata reserve pool.", error);
|
|
|
|
return error;
|
|
}
|