Skip to content

Commit 452e53d

Browse files
committed
NSFS | versioning | add direcory content versioning PUT action
Signed-off-by: nadav mizrahi <nadav.mizrahi16@gmail.com>
1 parent 7ad936c commit 452e53d

File tree

7 files changed

+259
-19
lines changed

7 files changed

+259
-19
lines changed

config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,7 @@ config.NSFS_RANDOM_DELAY_BASE = 70;
818818

819819
config.NSFS_VERSIONING_ENABLED = true;
820820
config.NSFS_UPDATE_ISSUES_REPORT_ENABLED = true;
821+
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = false;
821822

822823
config.NSFS_EXIT_EVENTS_TIME_FRAME_MIN = 24 * 60; // per day
823824
config.NSFS_MAX_EXIT_EVENTS_PER_TIME_FRAME = 10; // allow max 10 failed forks per day

src/sdk/namespace_fs.js

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ function to_fs_xattr(xattr) {
294294
return _.mapKeys(xattr, (val, key) => XATTR_USER_PREFIX + key);
295295
}
296296

297+
function filter_fs_xattr(xattr) {
298+
return _.pickBy(xattr, (val, key) => key?.startsWith(XATTR_NOOBAA_INTERNAL_PREFIX));
299+
}
300+
297301
/**
298302
* get_random_delay returns a random delay number between base + min and max
299303
* @param {number} base
@@ -1199,7 +1203,7 @@ class NamespaceFS {
11991203
try {
12001204
await this._check_path_in_bucket_boundaries(fs_context, file_path);
12011205

1202-
if (this.empty_dir_content_flow(file_path, params)) {
1206+
if (this.should_use_empty_content_dir_optimization() && this.empty_dir_content_flow(file_path, params)) {
12031207
const content_dir_info = await this._create_empty_dir_content(fs_context, params, file_path);
12041208
return content_dir_info;
12051209
}
@@ -1337,7 +1341,8 @@ class NamespaceFS {
13371341
const part_upload = file_path === upload_path;
13381342
const same_inode = params.copy_source && copy_res === COPY_STATUS_ENUM.SAME_INODE;
13391343
const should_replace_xattr = params.copy_source ? copy_res === COPY_STATUS_ENUM.FALLBACK : true;
1340-
const is_dir_content = this._is_directory_content(file_path, params.key);
1344+
const is_dir_content_unoptimized_flow = this._is_directory_content(file_path, params.key) &&
1345+
this.should_use_empty_content_dir_optimization();
13411346

13421347
const stat = await target_file.stat(fs_context);
13431348
this._verify_encryption(params.encryption, this._get_encryption_info(stat));
@@ -1380,18 +1385,21 @@ class NamespaceFS {
13801385
});
13811386
}
13821387
}
1383-
if (fs_xattr && !is_dir_content && should_replace_xattr) await target_file.replacexattr(fs_context, fs_xattr);
1388+
if (fs_xattr && !is_dir_content_unoptimized_flow && should_replace_xattr) {
1389+
await target_file.replacexattr(fs_context, fs_xattr);
1390+
}
13841391
// fsync
13851392
if (config.NSFS_TRIGGER_FSYNC) await target_file.fsync(fs_context);
13861393
dbg.log1('NamespaceFS._finish_upload:', open_mode, file_path, upload_path, fs_xattr);
13871394

13881395
if (!same_inode && !part_upload) {
1389-
await this._move_to_dest(fs_context, upload_path, file_path, target_file, open_mode, params.key, is_dir_content);
1396+
await this._move_to_dest(fs_context, upload_path, file_path, target_file, open_mode, params.key,
1397+
is_dir_content_unoptimized_flow);
13901398
}
13911399

13921400
// when object is a dir, xattr are set on the folder itself and the content is in .folder file
13931401
// we still should put the xattr if copy is link/same inode because we put the xattr on the directory
1394-
if (is_dir_content) {
1402+
if (is_dir_content_unoptimized_flow) {
13951403
await this._assign_dir_content_to_xattr(fs_context, fs_xattr, { ...params, size: stat.size }, copy_xattr);
13961404
}
13971405
stat.xattr = { ...stat.xattr, ...fs_xattr };
@@ -1419,16 +1427,14 @@ class NamespaceFS {
14191427
}
14201428

14211429
// move to dest GPFS (wt) / POSIX (w / undefined) - non part upload
1422-
async _move_to_dest(fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content) {
1423-
dbg.log2('_move_to_dest', fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content);
1430+
async _move_to_dest(fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content_unoptimized_flow) {
1431+
dbg.log2('_move_to_dest', fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content_unoptimized_flow);
14241432
let retries = config.NSFS_RENAME_RETRIES;
14251433
// will retry renaming a file in case of parallel deleting of the destination path
14261434
for (;;) {
14271435
try {
14281436
await native_fs_utils._make_path_dirs(dest_path, fs_context);
1429-
if (this._is_versioning_disabled() ||
1430-
(this._is_versioning_enabled() && is_dir_content)) {
1431-
// dir_content is not supported in versioning, hence we will treat it like versioning disabled
1437+
if (this._is_versioning_disabled() || is_dir_content_unoptimized_flow) {
14321438
if (open_mode === 'wt') {
14331439
await target_file.linkfileat(fs_context, dest_path);
14341440
} else {
@@ -1471,6 +1477,7 @@ class NamespaceFS {
14711477
dbg.log1('Namespace_fs._move_to_dest_version:', new_ver_tmp_path, latest_ver_path, upload_file);
14721478
let gpfs_options;
14731479
const is_gpfs = native_fs_utils._is_gpfs(fs_context);
1480+
const is_dir_content = this._is_directory_content(latest_ver_path, key);
14741481
let retries = config.NSFS_RENAME_RETRIES;
14751482
for (;;) {
14761483
try {
@@ -1490,7 +1497,7 @@ class NamespaceFS {
14901497
latest_ver_info = await this._get_version_info(fs_context, latest_ver_path);
14911498
}
14921499
const bucket_tmp_dir_path = this.get_bucket_tmpdir_full_path();
1493-
const versioned_path = latest_ver_info && this._get_version_path(key, latest_ver_info.version_id_str);
1500+
const versioned_path = latest_ver_info && this._get_version_path(key, latest_ver_info.version_id_str, is_dir_content);
14941501
dbg.log1('Namespace_fs._move_to_dest_version:', latest_ver_info, new_ver_info, gpfs_options);
14951502

14961503
if (this._is_versioning_suspended()) {
@@ -1513,6 +1520,9 @@ class NamespaceFS {
15131520
await native_fs_utils.safe_move(fs_context, latest_ver_path, versioned_path, latest_ver_info,
15141521
gpfs_options?.move_to_versions, bucket_tmp_dir_path);
15151522
}
1523+
if (is_dir_content) {
1524+
await this._handle_latest_disabled_dir_content_xattr(fs_context, key, versioned_path, latest_ver_path);
1525+
}
15161526
try {
15171527
// move new version to latest_ver_path (key path)
15181528
await native_fs_utils.safe_move(fs_context, new_ver_tmp_path, latest_ver_path, new_ver_info,
@@ -1536,6 +1546,37 @@ class NamespaceFS {
15361546
}
15371547
}
15381548

1549+
/** handle xattr of content dir when moving from disabled to enabled mode.
1550+
* in the disabled version of content dir, the xattr is on the directory itself. so need to move it seperatly from the obejct
1551+
* in case of enabled mode we need to move the xattr to the new object
1552+
* both for suspended and enabled mode we need to clear the user xattr from the directory
1553+
* @param {nb.NativeFSContext} fs_context
1554+
* @param {string} key
1555+
* @param {string} latest_ver_path
1556+
* @param {string} versioned_path
1557+
*/
1558+
async _handle_latest_disabled_dir_content_xattr(fs_context, key, versioned_path, latest_ver_path) {
1559+
const latest_version_dir_path = path.dirname(latest_ver_path);
1560+
const directory_stat = await native_fs_utils.stat_ignore_enoent(fs_context, latest_version_dir_path);
1561+
const is_disabled_dir_content = directory_stat && directory_stat.xattr && directory_stat.xattr[XATTR_DIR_CONTENT];
1562+
if (is_disabled_dir_content) {
1563+
if (this._is_versioning_enabled()) {
1564+
dbg.log1('NamespaceFS._move_to_dest_version latest object is a directory object with attributes on the directory. move the xattr to the new .version file');
1565+
if (versioned_path) {
1566+
await this.set_fs_xattr_op(fs_context, versioned_path, filter_fs_xattr(directory_stat.xattr), undefined);
1567+
} else {
1568+
//if no versioned_path, then we have empty content dir. need to create new .folder file
1569+
//this scenario happens only after moving from disabled to enabled mode or after upgrade. version-id is always null
1570+
versioned_path = this._get_version_path(key, NULL_VERSION_ID, true);
1571+
await native_fs_utils._make_path_dirs(versioned_path, fs_context);
1572+
//in case of empty directory object .folder of the latest doesn't exist. use 'w' to create it if its missing
1573+
await this.set_fs_xattr_op(fs_context, versioned_path, filter_fs_xattr(directory_stat.xattr), undefined, "w");
1574+
}
1575+
}
1576+
await this._clear_user_xattr(fs_context, latest_version_dir_path, XATTR_USER_PREFIX);
1577+
}
1578+
}
1579+
15391580
// Comparing both device and inode number (st_dev and st_ino returned by stat)
15401581
// will tell you whether two different file names refer to the same thing.
15411582
// If so, we will return the etag and encryption info of the file_path
@@ -2284,10 +2325,10 @@ class NamespaceFS {
22842325
* @param {*} clear - the xattr prefix to be cleared
22852326
* @returns {Promise<void>}
22862327
*/
2287-
async set_fs_xattr_op(fs_context, file_path, set, clear) {
2328+
async set_fs_xattr_op(fs_context, file_path, set, clear, mode = config.NSFS_OPEN_READ_MODE) {
22882329
let file;
22892330
try {
2290-
file = await nb_native().fs.open(fs_context, file_path, config.NSFS_OPEN_READ_MODE,
2331+
file = await nb_native().fs.open(fs_context, file_path, mode,
22912332
native_fs_utils.get_umasked_mode(config.BASE_MODE_FILE));
22922333
await file.replacexattr(fs_context, set, clear);
22932334
await file.close(fs_context);
@@ -2673,6 +2714,10 @@ class NamespaceFS {
26732714
const is_dir_content = this._is_directory_content(file_path, params.key);
26742715
return is_dir_content && params.size === 0;
26752716
}
2717+
2718+
should_use_empty_content_dir_optimization() {
2719+
return this._is_versioning_disabled() || !config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED;
2720+
}
26762721
/**
26772722
* returns if should force md5 calculation for the bucket/account.
26782723
* first check if defined for bucket / account, if not use global default
@@ -2730,9 +2775,12 @@ class NamespaceFS {
27302775
}
27312776

27322777
// returns version path of the form bucket_path/dir/.versions/{key}_{version_id}
2733-
_get_version_path(key, version_id) {
2734-
const key_version = path.basename(key) + (version_id ? '_' + version_id : '');
2735-
return path.normalize(path.join(this.bucket_path, path.dirname(key), HIDDEN_VERSIONS_PATH, key_version));
2778+
// in case of directory content, the path is bucket_path/dir/{key}/.versions/.folder_{version_id}
2779+
_get_version_path(key, version_id, is_dir_content = false) {
2780+
const key_name = is_dir_content ? config.NSFS_FOLDER_OBJECT_NAME : path.basename(key);
2781+
const dir_name = is_dir_content ? key : path.dirname(key);
2782+
const key_version = key_name + (version_id ? '_' + version_id : '');
2783+
return path.normalize(path.join(this.bucket_path, dir_name, HIDDEN_VERSIONS_PATH, key_version));
27362784
}
27372785

27382786
// this function returns the following version information -

src/test/unit_tests/coretest.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ config.test_mode = true;
2727
config.NODES_FREE_SPACE_RESERVE = 10 * 1024 * 1024;
2828
config.NSFS_VERSIONING_ENABLED = true;
2929
config.OBJECT_SDK_BUCKET_CACHE_EXPIRY_MS = 1;
30+
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = true;
3031

3132
config.ROOT_KEY_MOUNT = '/tmp/noobaa-server/root_keys';
3233

src/test/unit_tests/jest_tests/test_versioning_concurrency.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ const buffer_utils = require('../../../util/buffer_utils');
1010
const { TMP_PATH, IS_GPFS, TEST_TIMEOUT } = require('../../system_tests/test_utils');
1111
const { crypto_random_string } = require('../../../util/string_utils');
1212
const endpoint_stats_collector = require('../../../sdk/endpoint_stats_collector');
13+
const config = require('../../../../config');
14+
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = true;
1315

1416
function make_dummy_object_sdk(nsfs_config, uid, gid) {
1517
return {
@@ -554,6 +556,33 @@ describe('test versioning concurrency', () => {
554556
});
555557
expect(num_versions).toBe(num_copies);
556558
}, TEST_TIMEOUT);
559+
560+
it('content dir multiple puts of the same key - enabled', async () => {
561+
const bucket = 'bucket-directory';
562+
const key = 'key-s/';
563+
564+
//upload disabled mode empty content dir, to check the creation of new
565+
nsfs.versioning = 'DISABLED';
566+
await nsfs.upload_object({ bucket: bucket, key: key, size: 0 }, DUMMY_OBJECT_SDK);
567+
568+
nsfs.versioning = 'ENABLED';
569+
const failed_operations = [];
570+
const successful_operations = [];
571+
const num_of_concurrency = 10;
572+
for (let i = 0; i < num_of_concurrency; i++) {
573+
const random_data = Buffer.from(String(i));
574+
const body = buffer_utils.buffer_to_read_stream(random_data);
575+
nsfs.upload_object({ bucket: bucket, key: key, source_stream: body }, DUMMY_OBJECT_SDK)
576+
.catch(err => failed_operations.push(err))
577+
.then(res => successful_operations.push(res));
578+
}
579+
await P.delay(2000);
580+
expect(successful_operations).toHaveLength(num_of_concurrency);
581+
expect(failed_operations).toHaveLength(0);
582+
const versions = await nsfs.list_object_versions({ bucket: bucket }, DUMMY_OBJECT_SDK);
583+
//TODO should be num_of_concurrency + 1 (the null version). list-object-version currently ignores the latest .folder file
584+
expect(versions.objects.length).toBe(num_of_concurrency);
585+
}, TEST_TIMEOUT);
557586
});
558587

559588
/**

src/test/unit_tests/nc_coretest.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ async function config_dir_setup() {
204204
NC_RELOAD_CONFIG_INTERVAL: 1,
205205
// DO NOT CHANGE - setting VACCUM_ANALYZER_INTERVAL=1 needed for failing the tests
206206
// in case where vaccumAnalyzer is being called before setting process.env.NC_NSFS_NO_DB_ENV = 'true' on nsfs.js
207-
VACCUM_ANALYZER_INTERVAL: 1
207+
VACCUM_ANALYZER_INTERVAL: 1,
208+
NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED: true
208209
}));
209210
await fs.promises.mkdir(FIRST_BUCKET_PATH, { recursive: true });
210211
}

0 commit comments

Comments
 (0)