From 7d5bf69a9613f30109403dc1da0d0eb311f70b67 Mon Sep 17 00:00:00 2001 From: nadav mizrahi Date: Thu, 2 Jan 2025 11:15:18 +0200 Subject: [PATCH] NSFS | versioning | calculate multipart upload version by creation time Signed-off-by: nadav mizrahi --- src/api/common_api.js | 3 ++ src/native/fs/fs_napi.cpp | 45 ++++++++++++++++++ src/sdk/namespace_fs.js | 31 +++++++++++- src/sdk/nb.d.ts | 4 ++ .../unit_tests/test_bucketspace_versioning.js | 47 +++++++++++++++++++ src/test/unit_tests/test_nb_native_fs.js | 25 ++++++++++ 6 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/api/common_api.js b/src/api/common_api.js index 6a46718c8c..7c6b637f31 100644 --- a/src/api/common_api.js +++ b/src/api/common_api.js @@ -1201,6 +1201,9 @@ module.exports = { }, safeunlink: { $ref: 'common_api#/definitions/op_stats_val' + }, + utimensat: { + $ref: 'common_api#/definitions/op_stats_val' } } }, diff --git a/src/native/fs/fs_napi.cpp b/src/native/fs/fs_napi.cpp index af7bfcf137..e92850a99b 100644 --- a/src/native/fs/fs_napi.cpp +++ b/src/native/fs/fs_napi.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1351,6 +1352,47 @@ struct GetPwName : public FSWorker } }; +/** + * Utimensat is an fs op + * change file times with nanosecond precision (access and modification times) + * see https://man7.org/linux/man-pages/man3/utimensat.3p.html + */ +struct Utimensat : public FSWorker +{ + std::string _path; + struct timespec _times[2]; + Utimensat(const Napi::CallbackInfo& info) + : FSWorker(info) + { + _path = info[1].As(); + Napi::Object napi_times = info[2].As(); + std::string new_atime; + get_time_from_info(napi_times, "actime", _times[0], new_atime); + std::string new_mtime; + get_time_from_info(napi_times, "modtime", _times[1], new_mtime); + Begin(XSTR() << "utimensat " << DVAL(_path) << DVAL(new_atime) << DVAL(new_mtime)); + } + virtual void Work() + { + SYSCALL_OR_RETURN(utimensat(AT_FDCWD, _path.c_str(), _times, 0)); + } + + void get_time_from_info(const Napi::Object& napi_times, const std::string& time_type, struct timespec& timeval, std::string& log_str) { + if(napi_times.Get(time_type).IsBigInt()) { + const Napi::BigInt bigint = napi_times.Get(time_type).As(); + //TODO: handle lossless + bool lossless = true; + const int64_t time = bigint.Int64Value(&lossless); + timeval.tv_sec = time / int64_t(1e9); + timeval.tv_nsec = time % int64_t(1e9); + log_str = std::to_string(time); + } else { + timeval.tv_nsec = UTIME_OMIT; + log_str = "UTIME_OMIT"; + } + } +}; + struct FileWrap : public Napi::ObjectWrap { std::string _path; @@ -1374,6 +1416,7 @@ struct FileWrap : public Napi::ObjectWrap InstanceMethod<&FileWrap::flock>("flock"), InstanceMethod<&FileWrap::fcntllock>("fcntllock"), InstanceAccessor<&FileWrap::getfd>("fd"), + InstanceMethod<&FileWrap::utimensat>("utimensat"), })); constructor.SuppressDestruct(); } @@ -1403,6 +1446,7 @@ struct FileWrap : public Napi::ObjectWrap Napi::Value getfd(const Napi::CallbackInfo& info); Napi::Value flock(const Napi::CallbackInfo& info); Napi::Value fcntllock(const Napi::CallbackInfo& info); + Napi::Value utimensat(const Napi::CallbackInfo& info); }; Napi::FunctionReference FileWrap::constructor; @@ -2320,6 +2364,7 @@ fs_napi(Napi::Env env, Napi::Object exports) exports_fs["getsinglexattr"] = Napi::Function::New(env, api); exports_fs["getpwname"] = Napi::Function::New(env, api); exports_fs["symlink"] = Napi::Function::New(env, api); + exports_fs["utimensat"] = Napi::Function::New(env, api); FileWrap::init(env); exports_fs["open"] = Napi::Function::New(env, api); diff --git a/src/sdk/namespace_fs.js b/src/sdk/namespace_fs.js index fabe042ed3..f76169e252 100644 --- a/src/sdk/namespace_fs.js +++ b/src/sdk/namespace_fs.js @@ -1505,6 +1505,16 @@ class NamespaceFS { await this._delete_null_version_from_versions_directory(key, fs_context); } } + //in case new version is not the latest move straight to .versions dir. can happen for multipart upload + const prev_version_info = latest_ver_info || await this.find_max_version_past(fs_context, key); + if (this._is_versioning_enabled() && prev_version_info && + this._is_version_more_recent(prev_version_info.version_id_str, new_ver_info.version_id_str)) { + const new_versioned_path = this._get_version_path(key, new_ver_info.version_id_str); + dbg.log1('NamespaceFS._move_to_dest_version version ID of key is not the latest move to .versions '); + await native_fs_utils.safe_move(fs_context, new_ver_tmp_path, new_versioned_path, new_ver_info, + gpfs_options?.move_source_to_version, bucket_tmp_dir_path); + break; //since moving staight to .version, no need to change the latest version + } if (latest_ver_info && ((this._is_versioning_enabled()) || (this._is_versioning_suspended() && latest_ver_info.version_id_str !== NULL_VERSION_ID))) { @@ -1865,9 +1875,14 @@ class NamespaceFS { } if (!target_file) target_file = await native_fs_utils.open_file(fs_context, this.bucket_path, upload_path, open_mode); + const create_object_upload_path = path.join(params.mpu_path, 'create_object_upload'); + const create_object_upload_stat = await nb_native().fs.stat(fs_context, create_object_upload_path); + //according to aws, multipart upload version time should be calculated based on creation rather then completion time. + //change the files mtime to match the creation time + await nb_native().fs.utimensat(fs_context, upload_path, {modtime: create_object_upload_stat.mtimeNsBigint}); const { data: create_params_buffer } = await nb_native().fs.readFile( fs_context, - path.join(params.mpu_path, 'create_object_upload') + create_object_upload_path ); const upload_params = { fs_context, upload_path, open_mode, file_path, params, target_file }; @@ -2725,6 +2740,17 @@ class NamespaceFS { return { mtimeNsBigint: size_utils.string_to_bigint(arr[0], 36), ino: parseInt(arr[1], 36) }; } + /** + * compares two version strings, and returns if ver1 time is more recent then ver2 + * returns true if ver1 is more recent, otherwise return false + * if at least one of the versions is null return false + */ + _is_version_more_recent(ver1, ver2) { + const ver1_info = this._extract_version_info_from_xattr(ver1); + const ver2_info = this._extract_version_info_from_xattr(ver2); + return ver1_info && ver2_info && ver1_info.mtimeNsBigint > ver2_info.mtimeNsBigint; + } + _get_version_id_by_xattr(stat) { return (stat && stat.xattr[XATTR_VERSION_ID]) || 'null'; } @@ -3230,7 +3256,8 @@ class NamespaceFS { } return { move_to_versions: { src_file: dst_file, dir_file }, - move_to_dst: { src_file, dst_file, dir_file} + move_to_dst: { src_file, dst_file, dir_file}, + move_source_to_version: {src_file, dir_file} }; } catch (err) { dbg.warn('NamespaceFS._open_files_gpfs couldn\'t open files', err); diff --git a/src/sdk/nb.d.ts b/src/sdk/nb.d.ts index 33dad4f39d..d91d9d9787 100644 --- a/src/sdk/nb.d.ts +++ b/src/sdk/nb.d.ts @@ -953,6 +953,10 @@ interface NativeFS { checkAccess(fs_context: NativeFSContext, path: string): Promise; getsinglexattr(fs_context: NativeFSContext, path: string, key: string): Promise; getpwname(fs_context: NativeFSContext, user: string): Promise; + utimensat(fs_context: NativeFSContext, path: string, times: { + modtime?: bigint, + actime?: bigint + }): Promise; readFile( fs_context: NativeFSContext, diff --git a/src/test/unit_tests/test_bucketspace_versioning.js b/src/test/unit_tests/test_bucketspace_versioning.js index db0491ba1f..bb8428eac3 100644 --- a/src/test/unit_tests/test_bucketspace_versioning.js +++ b/src/test/unit_tests/test_bucketspace_versioning.js @@ -407,7 +407,54 @@ mocha.describe('bucketspace namespace_fs - versioning', function() { const exist = await version_file_exists(full_path, mpu_key1, '', prev_version_id); assert.ok(exist); }); + + mocha.it('mpu object - versioning enabled - put object before running mpu complete. mpu version should move to .versions dir and not latest', async function() { + const mpu_res = await s3_uid6.createMultipartUpload({ Bucket: bucket_name, Key: mpu_key1 }); + const upload_id = mpu_res.UploadId; + const part1 = await s3_uid6.uploadPart({ + Bucket: bucket_name, Key: mpu_key1, Body: body1, UploadId: upload_id, PartNumber: 1 }); + const res_put_object = await s3_uid6.putObject({Bucket: bucket_name, Key: mpu_key1, Body: body1}); + const res = await s3_uid6.completeMultipartUpload({ + Bucket: bucket_name, + Key: mpu_key1, + UploadId: upload_id, + MultipartUpload: { + Parts: [{ + ETag: part1.ETag, + PartNumber: 1 + }] + } + }); + const exist = await version_file_exists(full_path, mpu_key1, '', res.VersionId); + assert.ok(exist); + const comp_res = await compare_version_ids(full_path, mpu_key1, res_put_object.VersionId, res.VersionId); + assert.ok(comp_res); + }); + + mocha.it('mpu object - versioning enabled - create delete marker before running mpu complete. mpu should move to .versions dir and not latest', async function() { + const mpu_res = await s3_uid6.createMultipartUpload({ Bucket: bucket_name, Key: mpu_key1 }); + const upload_id = mpu_res.UploadId; + const part1 = await s3_uid6.uploadPart({ + Bucket: bucket_name, Key: mpu_key1, Body: body1, UploadId: upload_id, PartNumber: 1 }); + await s3_uid6.deleteObject({Bucket: bucket_name, Key: mpu_key1}); + const res = await s3_uid6.completeMultipartUpload({ + Bucket: bucket_name, + Key: mpu_key1, + UploadId: upload_id, + MultipartUpload: { + Parts: [{ + ETag: part1.ETag, + PartNumber: 1 + }] + } + }); + const exist = await version_file_exists(full_path, mpu_key1, '', res.VersionId); + assert.ok(exist); + const latest_version_dont_exist = fs_utils.file_not_exists(path.join(full_path, mpu_key1)); + assert.ok(latest_version_dont_exist); + }); }); + }); // The res of putBucketVersioning is different depends on the versioning state: diff --git a/src/test/unit_tests/test_nb_native_fs.js b/src/test/unit_tests/test_nb_native_fs.js index bb67ca3f62..98c004d47b 100644 --- a/src/test/unit_tests/test_nb_native_fs.js +++ b/src/test/unit_tests/test_nb_native_fs.js @@ -444,6 +444,31 @@ mocha.describe('nb_native fs', async function() { await fs_utils.file_delete(tmp_mv_path); } }); + + mocha.describe('Utimensat', async function() { + mocha.it('Utimensat - change both atime and mtime', async function() { + const { utimensat } = nb_native().fs; + const PATH1 = `/tmp/utimensat${Date.now()}_1`; + await create_file(PATH1); + const new_time = BigInt(Date.now()); + await utimensat(DEFAULT_FS_CONFIG, PATH1, {modtime: new_time, actime: new_time}); + const res = await nb_native().fs.stat(DEFAULT_FS_CONFIG, PATH1); + assert.equal(res.atimeNsBigint, new_time); + assert.equal(res.mtimeNsBigint, new_time); + }); + + mocha.it('Utimensat - change only mtime', async function() { + const { utimensat } = nb_native().fs; + const PATH1 = `/tmp/utimensat${Date.now()}_1`; + await create_file(PATH1); + const new_time = BigInt(Date.now()); + const res0 = await nb_native().fs.stat(DEFAULT_FS_CONFIG, PATH1); + await utimensat(DEFAULT_FS_CONFIG, PATH1, {modtime: new_time}); + const res1 = await nb_native().fs.stat(DEFAULT_FS_CONFIG, PATH1); + assert.equal(res1.atimeNsBigint, res0.atimeNsBigint); + assert.equal(res1.mtimeNsBigint, new_time); + }); + }); }); });