ceph-sync
Version:
Sync tool between LOCAL file system and REMOTE object storage.
400 lines (331 loc) • 12.9 kB
JavaScript
;
const MODULE_REQUIRE = 1
/* built-in */
, events = require('events')
, fs = require('fs')
, path = require('path')
/* NPM */
, ceph = require('ceph')
, undertake = require('undertake')
, noda = require('noda')
, Progress = require('jinang/Progress')
, cloneObject = require('jinang/cloneObject')
, sleep = require('jinang/sleep')
/* in-package */
, Marker = noda.inRequire('class/Marker')
, lib = noda.inRequireDir('lib')
/* in-file */
;
/**
* @param {object} source
* @param {string} source.path path of directory in local file system to be synchronised
* @param {string} source alias of source.path
*
* @param {object} target connection configurations of remote CEPH storage
* @param {ceph.Connection} target instance of connection to remote CEPH storage
*
* @param {object} options reserved options
*
* @param {string[]} [options.names] object names to be synchronised
* @param {Function} [options.mapper] object names mapper
* @param {Function} [options.filter] object names filter
* @param {Function} [options.dualMetaFilter] object metainfos (both local and remote) filter
* @param {number} [options.maxCreated] maximum creation allowed (the progress will be terminated)
* @param {number} [options.maxCreating] maximum cocurrent creating operation allowed
* @param {number} [options.maxQueueing] maximum queue length allowed
* @param {number} [options.maxErrors] maximum exceptions allowed (the progress will be terminated)
* @param {number} [options.retry] maximum retry times on exception for each object
*
* @return EventEmitter
*/
function fs2ceph(source, target, options) {
// 指代整个同步过程。
let progress = new Progress();
// ---------------------------
// Uniform & validate arguments.
source = lib.parse_fs_argument(source);
target = lib.parse_ceph_argument(target);
let targetContainer = target.get('container');
target.on('error', (err) => {
progress.emit('error', err);
});
options = Object.assign({
maxCreating : 10,
maxCreated : Number.MAX_SAFE_INTEGER,
maxQueueing : 100000,
maxErrors : Number.MAX_SAFE_INTEGER,
retry : 3,
}, options);
if (typeof options.retry != 'number' || isNaN(options.retry)) {
options.retry = 0;
}
// ---------------------------
// Flags.
let
// 标记为真时,停止添加新的对象到待创建队列中。
stopRegister = false,
// 标记为真时,停止创建新的对象(已发起创建操作的对象不受影响),即使待创建队列不为空。
stopCreate = false,
// 在所有对象均已添加到待创建队列后(注册完毕),标记为真。
registerFinished = false;
// 收到 QUIT 信号时:
// * 将“停止注册”标记为 TRUE
progress.signal(Progress.SIGQUIT, () => {
stopRegister = true;
});
// 收到 ABORT 信号时:
// * 将“停止注册”标记为 TRUE
// * 将“停止创建”标记为 TRUE
progress.signal(Progress.SIGABRT, () => {
stopRegister = true;
stopCreate = true;
});
// 上次同步点。
let marker = new Marker(options.marker);
const STATUS_NAMES = [ 'waiting', 'creating', 'created', 'ignored', 'skipped' ];
// 队列。
let queue = {
// 等待同步的文件列表:[ [cephname, pathname] ]
waiting: [],
// 未归档的同步文件状态列表:[ [ cephname, 0 (waiting) | 1 (creating) | 2 (created) | 3 (ignored) | 4 (skipped) ] ]
// ignored 状态表示文件应某种故障未同步成功;
// skipped 状态表示文件被过滤器(filter | metaFilter | dualMetaFilter)禁止同步。
unarchived: [],
// { 对象名 : 重试次数 }
retry: {},
};
// 计数器。
let counter = {
// 在同步中的文件数目。
creating: 0,
// 已登记(包含不同同步状态)的文件数量。
registered: 0,
// 同步失败次数。
errors: 0,
// 创建(同步)成功的文件数目。
created: 0,
// 忽略(同步失败)的文件数目。
ignored: 0,
// 跳过(不执行同步)的文件数目。
skipped: 0,
};
// 触发 error / end 事件时,附带的统计数据。
let genReturnMeta = () => Object.assign(
{},
cloneObject(counter, [ 'errors', 'created', 'ignored' ])
);
// ---------------------------
// Main process.
// 执行创建操作。
// 在 CEPH 存储中创建对象。
let create = (cephname, pathname) => {
counter.creating++;
let realCephname;
if (options.mapper) {
realCephname = options.mapper(cephname);
}
else {
realCephname = cephname;
}
let callback = ex => {
if (ex) {
on_create_error(ex, cephname, pathname);
}
counter.creating--;
next();
};
if (options.filter && !options.filter(cephname)) {
archive(cephname, 4); // 4 means skipped
callback();
return;
}
let runCreate = function*() {
yield target.createObject(cephOptions, fs.createReadStream(pathname));
archive(cephname, 2); // 2 means created
};
let run;
let cephOptions = { name: realCephname, container: targetContainer, suppressNotFoundError: true };
if (options.dualMetaFilter) {
run = function*() {
let stat = yield callback => fs.stat(pathname, callback);
let meta = yield target.readObjectMeta(cephOptions);
if (options.dualMetaFilter(stat, meta)) {
yield runCreate();
}
else {
archive(cephname, 4); // 4 means skipped
}
};
}
else {
run = runCreate;
}
undertake(run , callback);
};
// 调度队列,尝试执行下一个创建操作。
let next = () => {
if (stopCreate) {
return false;
}
if (counter.creating >= options.maxCreating) {
return false;
}
else if (queue.waiting.length == 0) {
return false;
}
else {
let item = queue.waiting.shift();
let cephname = item[0], pathname = item[1];
// 更新同步状态。
let itemInUnarchived = queue.unarchived.find((q) => q[0] == cephname);
itemInUnarchived[1] = 1;
create(cephname, pathname);
return true;
}
};
let on_create_error = (err, cephname, pathname) => {
// 判断是否允许重试。
if (queue.retry[cephname]) {
// 如果已达最大重试次数,则忽略该对象并标记。
// 否则仅将重试次数累加。
if (queue.retry[cephname]++ >= options.retry) {
delete queue.retry[cephname];
}
}
else if (options.retry) {
queue.retry[cephname] = 1;
}
// 按重试处理。
if (queue.retry[cephname]) {
// 重置未归档队列中该对象的状态值。
queue.unarchived.find((q) => q[0] == cephname)[1] = 0; // 0 := waiting
// 放入等待队列队首,优先重试创建操作。
queue.waiting.unshift([cephname, pathname]);
// 触发警告。
progress.emit('warning', err, genReturnMeta());
}
else {
archive(cephname, 3); // 3 means ignored
// 触发错误。
progress.emit('error', err, genReturnMeta());
}
// 如果失败次数已达上限,则终止所有事务。
if (++counter.errors >= options.maxErrors) {
progress.abort();
return;
}
};
// 归档已创建对象。
let archive = (cephname, status) => {
let i = queue.unarchived.findIndex((q) => q[0] == cephname);
let statusName = STATUS_NAMES[status];
// 更新计数。
// statusName := created | ignored | skipped
counter[statusName]++;
// 触发事件。
progress.emit(statusName, { name: cephname });
// 如果在待归档队列中未排在首位,则更新其状态。
if (i > 0) {
queue.unarchived[i][1] = status;
}
// 否则,开始归档。
else {
let l = queue.unarchived.length;
while(i+1 < l && queue.unarchived[i+1][1] >= 2) {
// >= 2 means created OR ignored OR skipped
i++;
}
let markup = queue.unarchived[i][0];
queue.unarchived.splice(0, i+1);
// 触发游标前移事件。
progress.emit('moveon', markup);
try_end();
}
};
// 在队列中登记。
let register = (cephname, pathname) => {
// 如果当前登记项数已超过最大可创建对象数,则终止注册,并触发 QUIT 信号。
if (counter.registered >= options.maxCreated) {
progress.quit();
return false;
}
else {
queue.unarchived.push([ cephname, 0 ]);
queue.waiting.push([ cephname, pathname ]);
counter.registered++;
next();
return true;
}
};
let on_register_finished = () => {
registerFinished = true;
try_end();
};
let try_end = () => {
if (registerFinished && queue.unarchived.length == 0) {
progress.emit('end', genReturnMeta());
}
}
// 深度优先,遍历目录。
let started = false;
let run = (dirname, parentCephNamePieces) => {
return undertake(function*() {
// Why not fs.readdirSync() ?
// To avoid IO blocking.
let fsnames = yield undertake.calling(fs.readdir, fs, dirname, 'buffer');
fsnames.sort();
for (let i = 0; i < fsnames.length; i++) {
// 如果收到异常信号,则终止遍历。
if (stopRegister) return;
let fsname = fsnames[i].toString('utf8');
if (!Buffer.from(fsname).equals(fsnames[i])) {
progress.emit('no-utf8-filename', {
dirname: parentCephNamePieces.join('/'),
filenameBuffer: fsnames[i],
});
continue;
}
let cephnamePieces = parentCephNamePieces.concat(fsname);
let cephname = cephnamePieces.join('/');
if (marker.equal(cephname)) {
started = true;
continue;
}
// 如果尚未开始同步,则根据是否超越同步点,判断是否需要深入检查。
if (started || !marker.cover(cephname)) {
let realpath = path.join(dirname, fsname);
// Why not fs.statSync() ?
// To avoid IO blocking.
let stats = yield undertake.calling(fs.stat, fs, realpath);
// 遇目录则递归遍历。
if (stats.isDirectory()) {
yield run(realpath, cephnamePieces);
}
// 遇文件则直接同步(上载)。
else {
// 如果等候队列长度超过限度,则暂停排队。
while (queue.waiting.length >= options.maxQueueing) {
yield sleep.promise(1000);
}
register(cephname, realpath);
}
}
}
if (parentCephNamePieces.length == 0) on_register_finished();
});
};
process.nextTick(() => {
if (options.names) {
options.names.forEach(name => {
let realpath = path.resolve(source.path, name);
register(name, realpath);
});
on_register_finished();
}
else {
run(source.path, []);
}
});
return progress;
}
module.exports = fs2ceph;