mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-15 02:50:45 +02:00
* storage: make vacuum/compaction commit crash-safe with a durable .cpc marker A crash mid-compaction-commit could lose or corrupt volume data. The two-rename commit (.cpd->.dat, .cpx->.idx) was not atomic, fsync results were discarded before renaming over a healthy .dat, a stale .ldb could poison the needle map, and a duplicate/late commit could delete the live .dat/.idx outright. Introduce a durable .cpc commit marker so the swap is atomic across a crash: - CommitCompact writes and fsyncs the .cpc marker after makeupDiff fsyncs the .cpd/.cpx, then runs applyCompactSwap: an existence-guarded rename of .cpd->.dat and .cpx->.idx, a directory fsync, removal of the stale .ldb/.rdb, and finally removal of the marker. - reconcileCompactState recovers an interrupted commit on load: roll forward (finish the renames) when the marker is present, roll back (delete the orphan .cpd/.cpx) when it is absent. It runs from a directory pre-pass keyed on .cpd/.cpc existence, since the per-volume loader is keyed on .idx/.vif and misses the marker-only and already-renamed-.idx states. - applyCompactSwap verifies BOTH .cpd and .cpx exist before touching the live files, so a stale-state commit (including the Windows RemoveAll-then-rename path) errors without deleting anything. - Error-check the fsyncs that gate the swap: the .cpd close-fsync and .cpx fsync in copyDataBasedOnIndexFile, the makeupDiff .idx fsync, and MemDb.SaveToIdx. - generateLevelDbFile rebuilds from offset 0 when the stored watermark sits past the end of the .idx, instead of replaying zero entries and poisoning the needle map. - removeVolumeFiles and cleanupCompact sweep the .cpc marker; cleanup refuses to unlink the temp files while a marker is present. Mirror the commit-marker, fsync-before-rename, guard, and load/reconcile logic in the Rust volume server. * storage: don't reconcile an already-loaded volume's compaction state on reload reconcileCompactStates runs in loadExistingVolumes, which is re-invoked at runtime on SIGHUP (Store.LoadNewVolumes). For a volume that is already loaded and mid-vacuum, its .cpd/.cpx are live temp files, not crash leftovers -- rolling them back would clobber the in-flight compaction (and remove a live .ldb out from under an open handle). Skip any vid already present in the volume map; genuine startup recovery runs before any volume is loaded, so the map is empty then. Mirrored in the Rust volume server. Also drop the .note keepVif change that crept into this branch; it belongs to the replica-copy/verify workstream and is restored to master's behavior here so the two changes don't collide. * storage: roll a compaction commit forward per-file, not all-or-nothing A crash after the .cpd->.dat rename but before .cpx->.idx leaves .cpd gone, .cpx and .cpc present, and a stale .idx. The roll-forward required BOTH temp files, so it skipped the swap and cleared the marker, pairing the fresh .dat with the stale .idx (index corruption). Finish whichever temp file remains: extract finishCompactSwap to rename .cpd->.dat and/or .cpx->.idx independently; applyCompactSwap keeps the both-present guard for the normal commit. Existence in the Rust mirror is checked robustly so a transient error never skips the swap. * seaweed-volume: propagate directory fsync failures on the compaction commit path fsync_dir dropped every sync_all error, so the commit could proceed with an undurable marker or rename and a later restart could recover the wrong generation. Return the error and check it at the commit call sites (marker write and the swap), matching the Go fsyncDir which already propagates. Directory fsync stays a no-op on Windows, where it is unsupported. * storage: overflow-safe stale-watermark check when rebuilding the leveldb index watermark*NeedleMapEntrySize can overflow uint64 for a corrupted watermark and wrap below the file size, defeating the stale-.ldb guard. Compare in entries (watermark > size/NeedleMapEntrySize) instead, which is equivalent and cannot overflow. LevelDb-backed needle map is Go-only; no Rust mirror. * storage: propagate idxFile.Close error when writing the compacted index SaveToIdx writes the .cpx that is renamed to .idx at commit; a discarded Close error (buffered data not flushed) could leave a partially-written index after a crash. Surface it in the same durability gate as the fsync.
491 lines
13 KiB
Go
491 lines
13 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/idx"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
. "github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
)
|
|
|
|
// mark it every watermarkBatchSize operations
|
|
const watermarkBatchSize = 10000
|
|
|
|
var watermarkKey = []byte("idx_entry_watermark")
|
|
|
|
type LevelDbNeedleMap struct {
|
|
baseNeedleMapper
|
|
dbFileName string
|
|
db *leveldb.DB
|
|
ldbOpts *opt.Options
|
|
ldbAccessLock sync.RWMutex
|
|
exitChan chan bool
|
|
// no need to use atomic
|
|
accessFlag int64
|
|
ldbTimeout int64
|
|
recordCount uint64
|
|
}
|
|
|
|
func NewLevelDbNeedleMap(dbFileName string, indexFile *os.File, opts *opt.Options, ldbTimeout int64, version needle.Version) (m *LevelDbNeedleMap, err error) {
|
|
m = &LevelDbNeedleMap{dbFileName: dbFileName}
|
|
m.indexFile = indexFile
|
|
if !isLevelDbFresh(dbFileName, indexFile) {
|
|
glog.V(1).Infof("Start to Generate %s from %s", dbFileName, indexFile.Name())
|
|
generateLevelDbFile(dbFileName, indexFile)
|
|
glog.V(1).Infof("Finished Generating %s from %s", dbFileName, indexFile.Name())
|
|
}
|
|
if stat, err := indexFile.Stat(); err != nil {
|
|
glog.Fatalf("stat file %s: %v", indexFile.Name(), err)
|
|
} else {
|
|
m.indexFileOffset = stat.Size()
|
|
}
|
|
glog.V(1).Infof("Opening %s...", dbFileName)
|
|
|
|
if m.ldbTimeout == 0 {
|
|
if m.db, err = leveldb.OpenFile(dbFileName, opts); err != nil {
|
|
if errors.IsCorrupted(err) {
|
|
m.db, err = leveldb.RecoverFile(dbFileName, opts)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
glog.V(1).Infof("Loading %s... , watermark: %d", dbFileName, getWatermark(m.db))
|
|
m.recordCount = uint64(m.indexFileOffset / NeedleMapEntrySize)
|
|
watermark := (m.recordCount / watermarkBatchSize) * watermarkBatchSize
|
|
err = setWatermark(m.db, watermark)
|
|
if err != nil {
|
|
glog.Fatalf("set watermark for %s error: %s\n", dbFileName, err)
|
|
return
|
|
}
|
|
}
|
|
mm, indexLoadError := newNeedleMapMetricFromIndexFile(indexFile, version)
|
|
if indexLoadError != nil {
|
|
return nil, indexLoadError
|
|
}
|
|
m.mapMetric = *mm
|
|
m.ldbTimeout = ldbTimeout
|
|
if m.ldbTimeout > 0 {
|
|
m.ldbOpts = opts
|
|
m.exitChan = make(chan bool, 1)
|
|
m.accessFlag = 0
|
|
go lazyLoadingRoutine(m)
|
|
}
|
|
return
|
|
}
|
|
|
|
func isLevelDbFresh(dbFileName string, indexFile *os.File) bool {
|
|
// normally we always write to index file first
|
|
dbLogFile, err := os.Open(filepath.Join(dbFileName, "LOG"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer dbLogFile.Close()
|
|
dbStat, dbStatErr := dbLogFile.Stat()
|
|
indexStat, indexStatErr := indexFile.Stat()
|
|
if dbStatErr != nil || indexStatErr != nil {
|
|
glog.V(0).Infof("Can not stat file: %v and %v", dbStatErr, indexStatErr)
|
|
return false
|
|
}
|
|
|
|
return dbStat.ModTime().After(indexStat.ModTime())
|
|
}
|
|
|
|
func generateLevelDbFile(dbFileName string, indexFile *os.File) error {
|
|
db, err := leveldb.OpenFile(dbFileName, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
watermark := getWatermark(db)
|
|
if stat, err := indexFile.Stat(); err != nil {
|
|
glog.Fatalf("stat file %s: %v", indexFile.Name(), err)
|
|
return err
|
|
} else {
|
|
// A watermark past the end of the .idx means the .ldb is stale relative
|
|
// to the index it must mirror (e.g. an interrupted compaction left the
|
|
// old .ldb beside a freshly swapped, shorter .idx). Trusting it would
|
|
// replay zero entries and silently poison the needle map, so rebuild
|
|
// from offset 0 instead.
|
|
// Compare in entries, not bytes: watermark*NeedleMapEntrySize can
|
|
// overflow uint64 for a corrupted watermark and wrap past the size check.
|
|
if watermark > uint64(stat.Size())/NeedleMapEntrySize {
|
|
glog.Warningf("stale watermark %d for %s (filesize %d); rebuilding leveldb from start", watermark, dbFileName, stat.Size())
|
|
watermark = 0
|
|
}
|
|
glog.V(1).Infof("generateLevelDbFile %s, watermark %d, num of entries:%d", dbFileName, watermark, (uint64(stat.Size())-watermark*NeedleMapEntrySize)/NeedleMapEntrySize)
|
|
}
|
|
return idx.WalkIndexFile(indexFile, watermark, func(key NeedleId, offset Offset, size Size) error {
|
|
if !offset.IsZero() && !size.IsDeleted() {
|
|
levelDbWrite(db, key, offset, size, false, 0)
|
|
} else {
|
|
levelDbDelete(db, key)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) Get(key NeedleId) (element *needle_map.NeedleValue, ok bool) {
|
|
if m.ldbTimeout > 0 {
|
|
if err := m.ensureLdbLoaded(); err != nil {
|
|
return nil, false
|
|
}
|
|
defer m.ldbAccessLock.RUnlock()
|
|
}
|
|
return m.getFromDb(key)
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) getFromDb(key NeedleId) (element *needle_map.NeedleValue, ok bool) {
|
|
bytes := make([]byte, NeedleIdSize)
|
|
NeedleIdToBytes(bytes[0:NeedleIdSize], key)
|
|
data, err := m.db.Get(bytes, nil)
|
|
if err != nil || len(data) != OffsetSize+SizeSize {
|
|
return nil, false
|
|
}
|
|
offset := BytesToOffset(data[0:OffsetSize])
|
|
size := BytesToSize(data[OffsetSize : OffsetSize+SizeSize])
|
|
return &needle_map.NeedleValue{Key: key, Offset: offset, Size: size}, true
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) Put(key NeedleId, offset Offset, size Size) error {
|
|
var oldSize Size
|
|
var watermark uint64
|
|
if m.ldbTimeout > 0 {
|
|
if err := m.ensureLdbLoaded(); err != nil {
|
|
return err
|
|
}
|
|
defer m.ldbAccessLock.RUnlock()
|
|
}
|
|
if oldNeedle, ok := m.getFromDb(key); ok {
|
|
oldSize = oldNeedle.Size
|
|
}
|
|
m.logPut(key, oldSize, size)
|
|
// write to index file first
|
|
if err := m.appendToIndexFile(key, offset, size); err != nil {
|
|
return fmt.Errorf("cannot write to indexfile %s: %v", m.indexFile.Name(), err)
|
|
}
|
|
m.recordCount++
|
|
if m.recordCount%watermarkBatchSize != 0 {
|
|
watermark = 0
|
|
} else {
|
|
watermark = (m.recordCount / watermarkBatchSize) * watermarkBatchSize
|
|
glog.V(1).Infof("put cnt:%d for %s,watermark: %d", m.recordCount, m.dbFileName, watermark)
|
|
}
|
|
return levelDbWrite(m.db, key, offset, size, watermark == 0, watermark)
|
|
}
|
|
|
|
func getWatermark(db *leveldb.DB) uint64 {
|
|
data, err := db.Get(watermarkKey, nil)
|
|
if err != nil || len(data) != 8 {
|
|
glog.V(1).Infof("read previous watermark from db: %v, %d", err, len(data))
|
|
return 0
|
|
}
|
|
return util.BytesToUint64(data)
|
|
}
|
|
|
|
func setWatermark(db *leveldb.DB, watermark uint64) error {
|
|
glog.V(3).Infof("set watermark %d", watermark)
|
|
var wmBytes = make([]byte, 8)
|
|
util.Uint64toBytes(wmBytes, watermark)
|
|
if err := db.Put(watermarkKey, wmBytes, nil); err != nil {
|
|
return fmt.Errorf("failed to setWatermark: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func levelDbWrite(db *leveldb.DB, key NeedleId, offset Offset, size Size, updateWatermark bool, watermark uint64) error {
|
|
|
|
bytes := needle_map.ToBytes(key, offset, size)
|
|
|
|
if err := db.Put(bytes[0:NeedleIdSize], bytes[NeedleIdSize:NeedleIdSize+OffsetSize+SizeSize], nil); err != nil {
|
|
return fmt.Errorf("failed to write leveldb: %w", err)
|
|
}
|
|
// set watermark
|
|
if updateWatermark {
|
|
return setWatermark(db, watermark)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func levelDbDelete(db *leveldb.DB, key NeedleId) error {
|
|
bytes := make([]byte, NeedleIdSize)
|
|
NeedleIdToBytes(bytes, key)
|
|
return db.Delete(bytes, nil)
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) Delete(key NeedleId, offset Offset) error {
|
|
var watermark uint64
|
|
if m.ldbTimeout > 0 {
|
|
if err := m.ensureLdbLoaded(); err != nil {
|
|
return err
|
|
}
|
|
defer m.ldbAccessLock.RUnlock()
|
|
}
|
|
oldNeedle, found := m.getFromDb(key)
|
|
if !found || oldNeedle.Size.IsDeleted() {
|
|
return nil
|
|
}
|
|
m.logDelete(oldNeedle.Size)
|
|
|
|
// write to index file first
|
|
if err := m.appendToIndexFile(key, offset, TombstoneFileSize); err != nil {
|
|
return err
|
|
}
|
|
m.recordCount++
|
|
if m.recordCount%watermarkBatchSize != 0 {
|
|
watermark = 0
|
|
} else {
|
|
watermark = (m.recordCount / watermarkBatchSize) * watermarkBatchSize
|
|
}
|
|
return levelDbWrite(m.db, key, oldNeedle.Offset, -oldNeedle.Size, watermark == 0, watermark)
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) Close() {
|
|
if m.indexFile != nil {
|
|
indexFileName := m.indexFile.Name()
|
|
if err := m.indexFile.Sync(); err != nil {
|
|
glog.Warningf("sync file %s failed: %v", indexFileName, err)
|
|
}
|
|
if err := m.indexFile.Close(); err != nil {
|
|
glog.Warningf("close index file %s failed: %v", indexFileName, err)
|
|
}
|
|
}
|
|
|
|
if m.db != nil {
|
|
if err := m.db.Close(); err != nil {
|
|
glog.Warningf("close levelDB failed: %v", err)
|
|
}
|
|
}
|
|
if m.ldbTimeout > 0 {
|
|
m.exitChan <- true
|
|
}
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) Destroy() error {
|
|
m.Close()
|
|
os.Remove(m.indexFile.Name())
|
|
return os.RemoveAll(m.dbFileName)
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) UpdateNeedleMap(v *Volume, indexFile *os.File, opts *opt.Options, ldbTimeout int64) error {
|
|
if v.nm != nil {
|
|
v.nm.Close()
|
|
v.nm = nil
|
|
}
|
|
defer func() {
|
|
if v.tmpNm != nil {
|
|
v.tmpNm.Close()
|
|
v.tmpNm = nil
|
|
}
|
|
}()
|
|
levelDbFile := v.FileName(".ldb")
|
|
m.indexFile = indexFile
|
|
err := os.RemoveAll(levelDbFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(v.FileName(".cpldb"), levelDbFile); err != nil {
|
|
return fmt.Errorf("rename %s: %v", levelDbFile, err)
|
|
}
|
|
|
|
db, err := leveldb.OpenFile(levelDbFile, opts)
|
|
if err != nil {
|
|
if errors.IsCorrupted(err) {
|
|
db, err = leveldb.RecoverFile(levelDbFile, opts)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
m.db = db
|
|
|
|
stat, e := indexFile.Stat()
|
|
if e != nil {
|
|
glog.Fatalf("stat file %s: %v", indexFile.Name(), e)
|
|
return e
|
|
}
|
|
m.indexFileOffset = stat.Size()
|
|
m.recordCount = uint64(stat.Size() / NeedleMapEntrySize)
|
|
|
|
//set watermark
|
|
watermark := (m.recordCount / watermarkBatchSize) * watermarkBatchSize
|
|
err = setWatermark(db, uint64(watermark))
|
|
if err != nil {
|
|
glog.Fatalf("setting watermark failed %s: %v", indexFile.Name(), err)
|
|
return err
|
|
}
|
|
v.nm = m
|
|
v.tmpNm = nil
|
|
m.ldbTimeout = ldbTimeout
|
|
if m.ldbTimeout > 0 {
|
|
m.ldbOpts = opts
|
|
m.exitChan = make(chan bool, 1)
|
|
m.accessFlag = 0
|
|
go lazyLoadingRoutine(m)
|
|
}
|
|
return e
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) DoOffsetLoading(v *Volume, indexFile *os.File, startFrom uint64) (err error) {
|
|
glog.V(0).Infof("loading idx to leveldb from offset %d for file: %s", startFrom, indexFile.Name())
|
|
version := needle.GetCurrentVersion()
|
|
if v != nil {
|
|
version = v.Version()
|
|
}
|
|
dbFileName := v.FileName(".cpldb")
|
|
db, dbErr := leveldb.OpenFile(dbFileName, nil)
|
|
defer func() {
|
|
if dbErr == nil {
|
|
db.Close()
|
|
}
|
|
if err != nil {
|
|
os.RemoveAll(dbFileName)
|
|
}
|
|
|
|
}()
|
|
if dbErr != nil {
|
|
if errors.IsCorrupted(err) {
|
|
db, dbErr = leveldb.RecoverFile(dbFileName, nil)
|
|
}
|
|
if dbErr != nil {
|
|
return dbErr
|
|
}
|
|
}
|
|
|
|
err = idx.WalkIndexFile(indexFile, startFrom, func(key NeedleId, offset Offset, size Size) (e error) {
|
|
m.mapMetric.FileCounter++
|
|
m.mapMetric.MaybeSetMaxNeedleEnd(offset, size, version)
|
|
bytes := make([]byte, NeedleIdSize)
|
|
NeedleIdToBytes(bytes[0:NeedleIdSize], key)
|
|
// fresh loading
|
|
if startFrom == 0 {
|
|
m.mapMetric.FileByteCounter += uint64(size)
|
|
e = levelDbWrite(db, key, offset, size, false, 0)
|
|
return e
|
|
}
|
|
// increment loading
|
|
data, err := db.Get(bytes, nil)
|
|
if err != nil {
|
|
if !strings.Contains(strings.ToLower(err.Error()), "not found") {
|
|
// unexpected error
|
|
return err
|
|
}
|
|
// new needle, unlikely happen
|
|
m.mapMetric.FileByteCounter += uint64(size)
|
|
e = levelDbWrite(db, key, offset, size, false, 0)
|
|
} else {
|
|
// needle is found
|
|
oldSize := BytesToSize(data[OffsetSize : OffsetSize+SizeSize])
|
|
oldOffset := BytesToOffset(data[0:OffsetSize])
|
|
if !offset.IsZero() && !size.IsDeleted() {
|
|
// updated needle
|
|
m.mapMetric.FileByteCounter += uint64(size)
|
|
if !oldOffset.IsZero() && !oldSize.IsDeleted() {
|
|
m.mapMetric.DeletionCounter++
|
|
m.mapMetric.DeletionByteCounter += uint64(oldSize)
|
|
}
|
|
e = levelDbWrite(db, key, offset, size, false, 0)
|
|
} else {
|
|
// deleted needle
|
|
m.mapMetric.DeletionCounter++
|
|
m.mapMetric.DeletionByteCounter += uint64(oldSize)
|
|
e = levelDbDelete(db, key)
|
|
}
|
|
}
|
|
return e
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (m *LevelDbNeedleMap) ensureLdbLoaded() error {
|
|
for {
|
|
m.ldbAccessLock.RLock()
|
|
if m.db != nil {
|
|
return nil
|
|
}
|
|
m.ldbAccessLock.RUnlock()
|
|
m.ldbAccessLock.Lock()
|
|
if m.db == nil {
|
|
if err := reloadLdb(m); err != nil {
|
|
m.ldbAccessLock.Unlock()
|
|
return err
|
|
}
|
|
}
|
|
m.ldbAccessLock.Unlock()
|
|
}
|
|
}
|
|
|
|
func reloadLdb(m *LevelDbNeedleMap) (err error) {
|
|
if m.db != nil {
|
|
return nil
|
|
}
|
|
glog.V(1).Infof("reloading leveldb %s", m.dbFileName)
|
|
m.accessFlag = 1
|
|
if m.db, err = leveldb.OpenFile(m.dbFileName, m.ldbOpts); err != nil {
|
|
if errors.IsCorrupted(err) {
|
|
m.db, err = leveldb.RecoverFile(m.dbFileName, m.ldbOpts)
|
|
}
|
|
if err != nil {
|
|
glog.Fatalf("RecoverFile %s failed:%v", m.dbFileName, err)
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func unloadLdb(m *LevelDbNeedleMap) (err error) {
|
|
m.ldbAccessLock.Lock()
|
|
defer m.ldbAccessLock.Unlock()
|
|
if m.db != nil {
|
|
glog.V(1).Infof("reached max idle count, unload leveldb, %s", m.dbFileName)
|
|
m.db.Close()
|
|
m.db = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lazyLoadingRoutine(m *LevelDbNeedleMap) (err error) {
|
|
glog.V(1).Infof("lazyLoadingRoutine %s", m.dbFileName)
|
|
var accessRecord int64
|
|
accessRecord = 1
|
|
for {
|
|
select {
|
|
case exit := <-m.exitChan:
|
|
if exit {
|
|
glog.V(1).Infof("exit from lazyLoadingRoutine")
|
|
return nil
|
|
}
|
|
case <-time.After(time.Hour * 1):
|
|
glog.V(1).Infof("timeout %s", m.dbFileName)
|
|
if m.accessFlag == 0 {
|
|
accessRecord++
|
|
glog.V(1).Infof("accessRecord++")
|
|
if accessRecord >= m.ldbTimeout {
|
|
unloadLdb(m)
|
|
}
|
|
} else {
|
|
glog.V(1).Infof("reset accessRecord %s", m.dbFileName)
|
|
// reset accessRecord
|
|
accessRecord = 0
|
|
m.accessFlag = 0
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|