Skip to content

Conversation

DarkIsDude
Copy link
Contributor

@DarkIsDude DarkIsDude commented Aug 25, 2025

Description

Reactivate some tests that were disable because some edge case were not covered.

Motivation and context

Manage 2 use case under the put metadata route:

  • Manage null version when an object is created before bucket versioning and put metadata called after bucket versioning enabled
/* eslint-disable no-console */
const util = require('util');

const { makeBackbeatRequest: makeBackbeatRequestBase } = require('./tests/functional/raw-node/utils/makeRequest');
const { models } = require('arsenal');
const { ObjectMD } = models;
const BucketUtility = require('./tests/functional/aws-node-sdk/lib/utility/bucket-util');

const makeBackbeatRequest = util.promisify(makeBackbeatRequestBase);

function objectMDFromRequestBody(data) {
    const bodyStr = JSON.parse(data.body).Body;
    return new ObjectMD(JSON.parse(bodyStr));
}

async function main() {
    const OBJECT_KEY = 'test-object';
    const BUCKET_NAME = 'test-bucket';
    const TEST_DATA = 'This is a test object for replication';

    const authCredentials = {
        accessKey: 'BJ8Q0L35PRJ92ABA2K0B',
        secretKey: 'kTgcfEaLjxvrLN5EKVcTnb4Ac046FU1m=33/baf1',
    };


    const bucketUtil = new BucketUtility('local-test-vault-s3', { signatureVersion: 'v4' });
    const s3 = bucketUtil.s3;

    console.info('Starting test for object metadata replication...');
    console.info('Checking if bucket exists...');
    await bucketUtil.emptyIfExists(BUCKET_NAME);

    if (await bucketUtil.bucketExists(BUCKET_NAME)) {
        console.info('Deleting bucket...');
        await s3.deleteBucket({ Bucket: BUCKET_NAME }).promise();
    }

    console.info('Creating bucket...');
    await s3.createBucket({ Bucket: BUCKET_NAME }).promise();
 
    console.info('Putting object with versioning disabled...');
    await s3.putObject({ Bucket: BUCKET_NAME, Key: OBJECT_KEY, Body: Buffer.from(TEST_DATA) }).promise();

    console.info('Enabling versioning on bucket...');
    await s3.putBucketVersioning({ Bucket: BUCKET_NAME, VersioningConfiguration: { Status: 'Enabled' } }).promise();
    const versionId = null;
    console.info('Retrieve metadata for the object with versioning enabled...');
    const data = await makeBackbeatRequest({
        method: 'GET',
        resourceType: 'metadata',
        bucket: BUCKET_NAME,
        objectKey: OBJECT_KEY,
        queryObj: {
            versionId,
        },
        authCredentials,
    });

    const objMD = objectMDFromRequestBody(data)
        .setContentLanguage('fr-FR')
        .getSerialized();

    console.info('Object metadata retrieved successfully:', objMD);
    console.info('Updating object metadata...');

    objMD.tags = {
        'fuck': 'test-value',
    };

    await makeBackbeatRequest({
        method: 'PUT',
        resourceType: 'metadata',
        bucket: BUCKET_NAME,
        objectKey: OBJECT_KEY,
        queryObj: {
            versionId,
        },
        authCredentials,
        requestBody: objMD,
    });

    console.info('Object metadata updated successfully.');

    const versions = await s3.listObjectVersions({
        Bucket: BUCKET_NAME,
        Prefix: OBJECT_KEY,
    }).promise();

    console.info({ versions });
}

main();
  • Add missing metadata
/* eslint-disable no-console */
const util = require('util');

const { makeBackbeatRequest: makeBackbeatRequestBase } = require('./tests/functional/raw-node/utils/makeRequest');
const { models } = require('arsenal');
const { ObjectMD } = models;
const BucketUtility = require('./tests/functional/aws-node-sdk/lib/utility/bucket-util');

const makeBackbeatRequest = util.promisify(makeBackbeatRequestBase);

/**
 * The final result should be to have two versions of the object:
 * 1. The original version created when versioning was disabled (null versionId)
 * 2. A new version created when versioning was re-enabled (with a VersionId)
 * The new version should have the updated metadata (ContentLanguage set to 'fr-FR')
 * while the original version should remain unchanged.
 **/

const OBJECT_KEY_ROOT = 'test-object-root';
const OBJECT_KEY = 'test-object';
const BUCKET_NAME = 'test-bucket';
const TEST_DATA = 'This is a test object for replication';

const authCredentials = {
    accessKey: 'BJ8Q0L35PRJ92ABA2K0B',
    secretKey: 'kTgcfEaLjxvrLN5EKVcTnb4Ac046FU1m=33/baf1',
};

function objectMDFromRequestBody(data) {
    const bodyStr = JSON.parse(data.body).Body;
    return new ObjectMD(JSON.parse(bodyStr));
}

async function main() {
    const bucketUtil = new BucketUtility('local-test-vault-s3', { signatureVersion: 'v4' });
    const s3 = bucketUtil.s3;

    // await initTest(s3, bucketUtil);
    await updateObjectMetadata(s3);
}

async function initTest(s3, bucketUtil) {
    if (await bucketUtil.bucketExists(BUCKET_NAME)) {
        await bucketUtil.emptyIfExists(BUCKET_NAME);
        await s3.deleteBucket({ Bucket: BUCKET_NAME }).promise();
    }

    await s3.createBucket({ Bucket: BUCKET_NAME }).promise();
    await s3.putBucketVersioning({ Bucket: BUCKET_NAME, VersioningConfiguration: { Status: 'Enabled' } }).promise();
    await s3.putObject({ Bucket: BUCKET_NAME, Key: OBJECT_KEY_ROOT, Body: Buffer.from(TEST_DATA) }).promise();
    await s3.putBucketVersioning({ Bucket: BUCKET_NAME, VersioningConfiguration: { Status: 'Suspended' } }).promise();
    await s3.putObject({ Bucket: BUCKET_NAME, Key: OBJECT_KEY, Body: Buffer.from(TEST_DATA) }).promise();
    await s3.putBucketVersioning({ Bucket: BUCKET_NAME, VersioningConfiguration: { Status: 'Enabled' } }).promise();
}

async function updateObjectMetadata(s3) {
        const data = await makeBackbeatRequest({
        method: 'GET',
        resourceType: 'metadata',
        bucket: BUCKET_NAME,
        objectKey: OBJECT_KEY,
        queryObj: {
            versionId: null,
        },
        authCredentials,
    });

    const objMD = objectMDFromRequestBody(data)
        .setContentLanguage('fr-FR')
        .getSerialized();

    await makeBackbeatRequest({
        method: 'PUT',
        resourceType: 'metadata',
        bucket: BUCKET_NAME,
        objectKey: OBJECT_KEY,
        queryObj: {
            versionId: '393832343336313437373734363139393939393952473030312020333061396264',
        },
        authCredentials,
        requestBody: objMD,
    });

    const versions = await s3.listObjectVersions({
        Bucket: BUCKET_NAME,
        Prefix: OBJECT_KEY,
    }).promise();

    console.info({ Versions: versions.Versions });
 }

async function run() {
    try {
        await main();
    } catch (e) {
        console.error('Error running test:', e);
        process.exit(1);
    }
}

run();

Related issues

https://scality.atlassian.net/browse/CLDSRV-632
scality/Arsenal#2490

@DarkIsDude DarkIsDude self-assigned this Aug 25, 2025
@bert-e
Copy link
Contributor

bert-e commented Aug 25, 2025

Hello darkisdude,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Contributor

bert-e commented Aug 25, 2025

Incorrect fix version

The Fix Version/s in issue CLDSRV-632 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 9.0.23

  • 9.1.0

Please check the Fix Version/s of CLDSRV-632, or the target
branch of this pull request.

@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch from 4e1523f to 300aaef Compare August 25, 2025 13:08
@DarkIsDude DarkIsDude changed the base branch from development/9.0 to development/9.1 August 25, 2025 13:08
@bert-e
Copy link
Contributor

bert-e commented Aug 25, 2025

Incorrect fix version

The Fix Version/s in issue CLDSRV-632 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 9.1.0

Please check the Fix Version/s of CLDSRV-632, or the target
branch of this pull request.

@DarkIsDude DarkIsDude marked this pull request as draft August 25, 2025 13:09
@DarkIsDude DarkIsDude changed the title Feature/cldsrv 632/put metadata edge cases CLDSRV-632 ✨ put metadata edge cases Aug 25, 2025
Copy link

codecov bot commented Aug 25, 2025

❌ 33 Tests Failed:

Tests completed Failed Passed Skipped
7397 33 7364 0
View the top 1 failed test(s) by shortest run time
"before each" hook for "should return metadata blob for a versionId"::backbeat routes GET Metadata route "before each" hook for "should return metadata blob for a versionId"
Stack Traces | 0.004s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"94f5f80ab245b4275db7","statusCode":404}
View the full list of 32 ❄️ flaky test(s)
PUT metadata with "x-scal-replication-content: METADATA"header should replicate metadata only::backbeat routes backbeat PUT routes PUT metadata with "x-scal-replication-content: METADATA"header should replicate metadata only

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.012s run time
ifError got unwanted exception: The specified key does not exist.
should create a new version when no versionId is passed in query string::backbeat routes backbeat PUT routes should create a new version when no versionId is passed in query string

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.007s run time
ifError got unwanted exception: The specified key does not exist.
should not remove data locations on replayed metadata PUT::backbeat routes backbeat PUT routes should not remove data locations on replayed metadata PUT

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.007s run time
ifError got unwanted exception: The specified key does not exist.
should remove old object data locations if version is overwritten with empty contents::backbeat routes backbeat PUT routes should remove old object data locations if version is overwritten with empty contents

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.007s run time
ifError got unwanted exception: The specified key does not exist.
should remove old object data locations if version is overwritten with same contents::backbeat routes backbeat PUT routes should remove old object data locations if version is overwritten with same contents

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.009s run time
ifError got unwanted exception: The specified key does not exist.
should successfully replicate a delete marker::backbeat routes for replication (cross account) should successfully replicate a delete marker

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.034s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"79ab31dd3c43825fe5d7","statusCode":404}
should successfully replicate a delete marker::backbeat routes for replication (same account) should successfully replicate a delete marker

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.038s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"82103ee09342ac8cd484","statusCode":404}
should successfully replicate a version and update account info::backbeat routes for replication (cross account) should successfully replicate a version and update account info

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.029s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"2518f3904e40bdf01d68","statusCode":404}
should successfully replicate a version and update account info::backbeat routes for replication (same account) should successfully replicate a version and update account info

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.033s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"03aa1da3364db020843a","statusCode":404}
should successfully replicate a version and update it::backbeat routes for replication (cross account) should successfully replicate a version and update it

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.027s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"7e41864420409a0bf631","statusCode":404}
should successfully replicate a version and update it::backbeat routes for replication (same account) should successfully replicate a version and update it

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.027s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"6e911431a544a1bf3fba","statusCode":404}
should successfully replicate a version::backbeat routes for replication (cross account) should successfully replicate a version

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.05s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"1e9db50ce443aad82e06","statusCode":404}
should successfully replicate a version::backbeat routes for replication (same account) should successfully replicate a version

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.029s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"67618395dc4aa2a5df5f","statusCode":404}
should successfully replicate multiple versions and keep original order::backbeat routes for replication (cross account) should successfully replicate multiple versions and keep original order

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.037s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"7afb94b08b4ab8d88d61","statusCode":404}
should successfully replicate multiple versions and keep original order::backbeat routes for replication (same account) should successfully replicate multiple versions and keep original order

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.05s run time
done() invoked with non-Error: {"code":"NoSuchKey","message":"The specified key does not exist.","resource":null,"requestId":"7c974894fb438cab95af","statusCode":404}
with UTF8 key::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with UTF8 key

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with ascii test key::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with ascii test key

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with encryption configuration and legacy API v1::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with encryption configuration and legacy API v1

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with encryption configuration::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with encryption configuration

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key Pâtisserie=中文-español-English::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key Pâtisserie=中文-español-English

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/spring/1.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/spring/1.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/spring/2.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/spring/2.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/spring/march/1.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/spring/march/1.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/summer/1.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/summer/1.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/summer/2.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/summer/2.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/summer/august/1.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/summer/august/1.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/year.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/year.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/yore.rs::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/yore.rs

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with key notes/zaphod/Beeblebrox.txt::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key notes/zaphod/Beeblebrox.txt

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with key 䆩鈁櫨㟔罳/䆩鈁櫨㟔罳/%42/mykey::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with key 䆩鈁櫨㟔罳/䆩鈁櫨㟔罳/%42/mykey

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.
with legacy API v1::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with legacy API v1

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.005s run time
ifError got unwanted exception: The specified key does not exist.
with percents and spaces encoded as '+' in key::backbeat routes backbeat PUT routes PUT data + metadata should create a new complete object with percents and spaces encoded as '+' in key

Flake rate in main: 25.39% (Passed 241 times, Failed 82 times)

Stack Traces | 0.006s run time
ifError got unwanted exception: The specified key does not exist.

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

});
},
async () => {
if (versioning && !objMd) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when does this !objMd case happen? Before this function is called, we already call standardMetadataValidateBucketAndObj (line 1655), which should return the object already...
(and looking at versioningPreprocessing, it seems that i knows how to handle objMd=nil as well as the case where object is/isn't the master?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@francoisferrand when you do a PUT on PutMD and the object does not exists. This case can happens when it's an empty object

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still not clear why we need the conditional, and not (more) systematically call versioningPreprocessing.
in particular, this function is responsible to handle version-suspended case (i.e. delete previous version). Or is this case objMD == nil a way to detect the insertion case (i.e. create a new version) instead of the udpate case (update -internal- metadata of an existing version, e.g. for replication status or transition)?

may help to have a comment explaining what use-case this "branch" handles.

This case can happens when it's an empty object

do you mean "empty" as in "no data"? I don't understand how it would/should affect the behavior here :-/

async () => {
if (versioning && !objMd) {
const masterObjectAndBucket =
await metadata.getBucketAndObjectMDPromised(bucketName, objectKey, {}, log);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is 2 extra I/Os: to get the bucket then get the object.
We already retrieved that in standardMetadataValidateBucketAndObj, so

  • we definitely already have the bucket -> no point retrieving it again
  • if we try to get the same object, we will get the same result i guess; if not did we actually need the first one? (i.e. could we fix that call instead of making another one here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I try to get the master / null version, so not the one that we want (objMD is undefined so we didn't find it). For the bucket I'll remove this extra call 🙏

const masterObjectAndBucket =
await metadata.getBucketAndObjectMDPromised(bucketName, objectKey, {}, log);
const masterObject = JSON.parse(masterObjectAndBucket.obj);
await versioningPreprocessingPromised(bucketName, bucketInfo, objectKey, masterObject, log);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

putMetadata can be used in 2 ways:

  • in CRR, it is most of the time used to "create" (or replace) object: it similar to a regular putObject, but dealing only with metadata (CRR replicates data and metadata separately)
    --> there it may need to just replace the document (non-versionned case), create a new one (no previous object or versionned case), or create a new one + delete previous one (version-suspended case)
  • in backbeat, it is used to "update" the metadata of an existing object: and it should never create a new document/revision
  • (in CRR, there may also be "real" metadata updates on the source, which get replicated the way: so the call from CRR may also be used to udpate the MD of an existing object, as backbeat does..... what may help though it that CRR can only ever happen in version-enabled buckets)

Looking at the code in putObject, we mostly handle the first case, though we have a corner case "putObjectVersion", which is more related to the second case (it is used to restore cold objects, and thus will write the data + simply update the metadata of existing document).

In that case, we actually must not (and do not 😁) call versioningPreprocessingPromised : so it seems we should have something similar here, and not call versioningPreprocessingPromised in the "update metadata" case...

Maybe that is the purpose of the !objMd condition, but it is a bit hard to see...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For larger context : I believe that we will eventually need to be able to differentiate "updates" from "puts", as there are some subbtle difference in handle race conditions/corner cases.

In particular, we may need to do this distinction to fix https://scality.atlassian.net/browse/ARTESCA-8449 (backbeat updates metadata by doing getMetadata...putMetadata, we multiple such sequences may happen in parallel). To fix this (which rarely happen), the idea is to introduce some some sort to counter and perform update with a condition on that counter: but it must be done only when the semantic is that of updating existing MD, not when the update is actually the replacement of a version.

Not sure it is really needed for this specific fix, but best to know this in case your change may go in similar direction.

.then(() => object)))
)
);
const listedObjects = await this.s3.listObjectVersions(param).promise();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how many objects are (typically) talking about in these tests?
could this affect the duration of the tests?

(or can we fix the backend so we can continue to delete in parallel?)

@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch 3 times, most recently from 0b5a099 to 9c143cb Compare September 5, 2025 07:57
@bert-e
Copy link
Contributor

bert-e commented Sep 5, 2025

Incorrect fix version

The Fix Version/s in issue CLDSRV-632 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 9.1.1

Please check the Fix Version/s of CLDSRV-632, or the target
branch of this pull request.

@bert-e
Copy link
Contributor

bert-e commented Sep 5, 2025

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch 7 times, most recently from b5c2131 to da6bfb6 Compare September 8, 2025 12:56
@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch from 14f2308 to e0ba601 Compare September 17, 2025 09:32
@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch from e0ba601 to 1b518c1 Compare September 22, 2025 09:31
@DarkIsDude DarkIsDude marked this pull request as ready for review September 22, 2025 11:52
@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch from 1b518c1 to dfc7d0d Compare September 22, 2025 11:55
@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch 2 times, most recently from 6acde44 to 2547220 Compare September 23, 2025 12:10
@DarkIsDude DarkIsDude force-pushed the feature/CLDSRV-632/put-metadata-edge-cases branch from 2547220 to f54f702 Compare September 23, 2025 12:42
Copy link
Contributor

@francoisferrand francoisferrand left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

somehow I am not sure this covers all cases - and not confident everything is fully tests: there are tests, but logic is quite intricate with the different versioning cases + different listings/... and all the flags: isNull, isNull2, ...

→ would be worth reviewing the tests to see if we miss some cases (e.g. all the combinations of versioning disabled/enabled/suspended, creating vs updating an object, with previous version/nullVersion/no previous version...). May help to look at putObject tests for inspiration on the test case?
→ since we have multiple backend, and the separation is sometimes not clear (or not clearly documented), we should make sure this test is run both with mongo and metadata backends: is it the case?

omVal.versionId = versionId;

if (isNull) {
omVal.isNull = isNull;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there maybe null2 field to handle as well ?
(somehow required for metadata backend, with the latest listing)

// To prevent this, the versionId field is only included in options when it is defined.
if (versionId !== undefined) {
options.versionId = versionId;
omVal.versionId = versionId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the semantics of putObject in metadata layer (either backends, i.e. metadata or mongodbClientInterface) are not clear or precisely defined : does setting this not interract (in some weird or unacceptable way?) the behavior of the versionId option?

});
},
async () => {
if (versioning && !objMd) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still not clear why we need the conditional, and not (more) systematically call versioningPreprocessing.
in particular, this function is responsible to handle version-suspended case (i.e. delete previous version). Or is this case objMD == nil a way to detect the insertion case (i.e. create a new version) instead of the udpate case (update -internal- metadata of an existing version, e.g. for replication status or transition)?

may help to have a comment explaining what use-case this "branch" handles.

This case can happens when it's an empty object

do you mean "empty" as in "no data"? I don't understand how it would/should affect the behavior here :-/

Comment on lines +677 to +679
if (versioningPreprocessingResult?.nullVersionId) {
omVal.nullVersionId = versioningPreprocessingResult.nullVersionId;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in other uses of versioningPreprocessing (in putObject / copyObject / putMpu) we set multiple (and different) fields:

			metadataStoreParams.versionId = options.versionId;
            metadataStoreParams.versioning = options.versioning;
            metadataStoreParams.isNull = options.isNull;
            metadataStoreParams.deleteNullKey = options.deleteNullKey;
            if (options.extraMD) {
                Object.assign(metadataStoreParams, options.extraMD);
            }

this gets translated (in metadataStoreObject) to this:

        if (versioning) {
            options.versioning = versioning;
        }
        if (versionId || versionId === '') {
            options.versionId = versionId;
        }

        if (deleteNullKey) {
            options.deleteNullKey = deleteNullKey;
        }

        const { isNull, nullVersionId, nullUploadId, isDeleteMarker } = params;
        md.setIsNull(isNull)
            .setNullVersionId(nullVersionId)
            .setNullUploadId(nullUploadId)
            .setIsDeleteMarker(isDeleteMarker);
        if (versionId && versionId !== 'null') {
            md.setVersionId(versionId);
        }
        if (isNull && !config.nullVersionCompatMode) {
            md.setIsNull2(true);
        }

→ so it seems many more fields may be set, to cover all cases?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants