-
Notifications
You must be signed in to change notification settings - Fork 256
Add CRR Cascaded capabilities #6179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development/9.4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ ENV PYTHON=python3 | |
| RUN npm install -g \ | ||
| node-gyp \ | ||
| typescript@4.9.5 | ||
| COPY package.json yarn.lock /usr/src/app/ | ||
| COPY package.json yarn.lock scality-cloudserverclient-v1.0.9.tgz /usr/src/app/ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Binary .tgz file checked into the repository. This should not be committed — use a proper npm registry reference or git-based dependency instead. |
||
|
|
||
| RUN yarn install --production --ignore-optional --frozen-lockfile --ignore-engines --network-concurrency 1 | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,13 @@ const joi = require('@hapi/joi'); | |
| const backbeatProxy = httpProxy.createProxyServer({ | ||
| ignorePath: true, | ||
| }); | ||
| const { auth, errors, errorInstances, s3middleware, s3routes, models, storage } = require('arsenal'); | ||
| const { auth, errors, errorInstances, s3middleware, s3routes, models, storage, versioning } = require('arsenal'); | ||
| const { decode, encode, compare: compareMicroVersionId, Ordering } = versioning.VersionID; | ||
| const { | ||
| VersionIdCollisionException, | ||
| StaleMicroVersionIdException, | ||
| CascadeLoopDetectedException, | ||
| } = require('@scality/cloudserverclient'); | ||
|
|
||
| const { responseJSONBody } = s3routes.routesUtils; | ||
| const { getSubPartIds } = s3middleware.azureHelper.mpuUtils; | ||
|
|
@@ -21,6 +27,7 @@ const locationStorageCheck = require('../api/apiUtils/object/locationStorageChec | |
| const { dataStore } = require('../api/apiUtils/object/storeObject'); | ||
| const prepareRequestContexts = require('../api/apiUtils/authorization/prepareRequestContexts'); | ||
| const { decodeVersionId } = require('../api/apiUtils/object/versioning'); | ||
| const getReplicationInfo = require('../api/apiUtils/object/getReplicationInfo'); | ||
| const locationKeysHaveChanged = require('../api/apiUtils/object/locationKeysHaveChanged'); | ||
| const { standardMetadataValidateBucketAndObj, metadataGetObject } = require('../metadata/metadataUtils'); | ||
| const { config } = require('../Config'); | ||
|
|
@@ -32,6 +39,7 @@ const { | |
| } = require('../api/apiUtils/integrity/validateChecksums'); | ||
| const { BackendInfo } = models; | ||
| const { pushReplicationMetric } = require('./utilities/pushReplicationMetric'); | ||
| const writeContinue = require('../utilities/writeContinue'); | ||
| const kms = require('../kms/wrapper'); | ||
| const { listLifecycleCurrents } = require('../api/backbeat/listLifecycleCurrents'); | ||
| const { listLifecycleNonCurrents } = require('../api/backbeat/listLifecycleNonCurrents'); | ||
|
|
@@ -93,7 +101,7 @@ function _isObjectRequest(req) { | |
| return ['data', 'metadata', 'multiplebackenddata', 'multiplebackendmetadata'].includes(req.resourceType); | ||
| } | ||
|
|
||
| function _respondWithHeaders(response, payload, extraHeaders, log, callback) { | ||
| function _respondWithHeaders(response, payload, extraHeaders, log, callback, statusCode = 200) { | ||
| let body = ''; | ||
| if (typeof payload === 'string') { | ||
| body = payload; | ||
|
|
@@ -115,10 +123,10 @@ function _respondWithHeaders(response, payload, extraHeaders, log, callback) { | |
| // eslint-disable-next-line no-param-reassign | ||
| response.serverAccessLog.endTurnAroundTime = process.hrtime.bigint(); | ||
| } | ||
| response.writeHead(200, httpHeaders); | ||
| response.writeHead(statusCode, httpHeaders); | ||
| response.end(body, 'utf8', () => { | ||
| log.end().info('responded with payload', { | ||
| httpCode: 200, | ||
| httpCode: statusCode, | ||
| contentLength: Buffer.byteLength(body), | ||
| }); | ||
| callback(); | ||
|
|
@@ -414,6 +422,32 @@ function putData(request, response, bucketInfo, objMd, log, callback) { | |
| log.error(errMessage); | ||
| return callback(errorInstances.BadRequest.customizeDescription(errMessage)); | ||
| } | ||
|
|
||
| const incomingVersionIdEncoded = request.headers['x-scal-version-id']; | ||
| const decoded = incomingVersionIdEncoded ? decode(incomingVersionIdEncoded) : null; | ||
|
SylvainSenechal marked this conversation as resolved.
|
||
| const incomingVersionIdDecoded = decoded instanceof Error ? null : decoded; | ||
| if (incomingVersionIdDecoded && objMd && objMd.versionId === incomingVersionIdDecoded) { | ||
| // Skip the write if data is already at destination for this version id | ||
| // Return 409 with the existing microVersionId so backbeat can | ||
| // decide if putMetadata is still needed | ||
| log.debug('crr cascade putData: version already at destination', { | ||
| method: 'putData', | ||
| bucketName: request.bucketName, | ||
| objectKey: request.objectKey, | ||
| hasMicroVersionId: !!objMd.microVersionId, | ||
| }); | ||
| request.resume(); | ||
| return _respondWithHeaders( | ||
| response, | ||
| { code: VersionIdCollisionException.name, message: 'version id already at destination' }, | ||
| { 'x-scal-micro-version-id': objMd.microVersionId ? encode(objMd.microVersionId) : '' }, | ||
| log, | ||
| callback, | ||
| 409, | ||
| ); | ||
| } | ||
|
|
||
| writeContinue(request, response); | ||
| const context = { | ||
| bucketName: request.bucketName, | ||
| owner: canonicalID, | ||
|
|
@@ -539,6 +573,47 @@ function getCanonicalIdsByAccountId(accountId, log, cb) { | |
| } | ||
|
|
||
| function putMetadata(request, response, bucketInfo, objMd, log, callback) { | ||
| const { bucketName, objectKey } = request; | ||
|
|
||
| const encodedMicroVersionId = request.headers['x-scal-micro-version-id']; | ||
| const decoded = encodedMicroVersionId ? decode(encodedMicroVersionId) : null; | ||
| const incomingMicroVersionId = decoded instanceof Error ? null : decoded; | ||
| if (incomingMicroVersionId) { | ||
| const cmp = compareMicroVersionId(incomingMicroVersionId, objMd?.microVersionId); | ||
| if (cmp === Ordering.EQUAL) { | ||
| log.debug('crr cascade putMetadata: loop detected, skipping write', { | ||
| method: 'putMetadata', | ||
| bucketName, | ||
| objectKey, | ||
| }); | ||
| request.resume(); | ||
| return _respondWithHeaders( | ||
| response, | ||
| { code: CascadeLoopDetectedException.name, message: 'incoming microVersionId already at destination' }, | ||
| {}, | ||
| log, | ||
| callback, | ||
| 409, | ||
| ); | ||
| } | ||
| if (cmp === Ordering.OLDER) { | ||
| log.debug('crr cascade putMetadata: stale event, rejecting', { | ||
| method: 'putMetadata', | ||
| bucketName, | ||
| objectKey, | ||
| }); | ||
| request.resume(); | ||
| return _respondWithHeaders( | ||
| response, | ||
| { code: StaleMicroVersionIdException.name, message: 'incoming revision is older than destination' }, | ||
| { 'x-scal-micro-version-id': objMd?.microVersionId ? encode(objMd.microVersionId) : '' }, | ||
| log, | ||
| callback, | ||
| 409, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
SylvainSenechal marked this conversation as resolved.
|
||
| return _getRequestPayload(request, (err, payload) => { | ||
| if (err) { | ||
| return callback(err); | ||
|
|
@@ -552,15 +627,15 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { | |
| return callback(errors.MalformedPOSTRequest); | ||
| } | ||
|
|
||
| const { headers, bucketName, objectKey } = request; | ||
| const { headers } = request; | ||
|
|
||
| // Destination-side delete-marker replication. | ||
| // We need the REPLICA status to distinguish from | ||
| // source-side replication status updates that also carry isDeleteMarker=true. | ||
| if ( | ||
| omVal.isDeleteMarker && | ||
| omVal.replicationInfo && | ||
| omVal.replicationInfo.status === 'REPLICA' && | ||
| (omVal.replicationInfo.isReplica || omVal.replicationInfo.status === 'REPLICA') && | ||
| request.serverAccessLog | ||
| ) { | ||
| // eslint-disable-next-line no-param-reassign | ||
|
|
@@ -576,7 +651,7 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { | |
| // The REPLICA status excludes source-side replication-status updates. | ||
| if ( | ||
| omVal.replicationInfo && | ||
| omVal.replicationInfo.status === 'REPLICA' && | ||
| (omVal.replicationInfo.isReplica === true || omVal.replicationInfo.status === 'REPLICA') && | ||
| (omVal.originOp === 's3:ObjectTagging:Put' || omVal.originOp === 's3:ObjectTagging:Delete') && | ||
| request.serverAccessLog | ||
| ) { | ||
|
|
@@ -593,7 +668,7 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { | |
| // The REPLICA status excludes source-side replication-status updates. | ||
| if ( | ||
| omVal.replicationInfo && | ||
| omVal.replicationInfo.status === 'REPLICA' && | ||
| (omVal.replicationInfo.isReplica === true || omVal.replicationInfo.status === 'REPLICA') && | ||
| omVal.originOp === 's3:ObjectAcl:Put' && | ||
| request.serverAccessLog | ||
| ) { | ||
|
|
@@ -672,7 +747,7 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { | |
| // then we want to create a version for the replica object even though | ||
| // none was provided in the object metadata value. | ||
| if (omVal.replicationInfo.isNFS) { | ||
| const isReplica = omVal.replicationInfo.status === 'REPLICA'; | ||
| const isReplica = omVal.replicationInfo.isReplica === true || omVal.replicationInfo.status === 'REPLICA'; | ||
| versioning = isReplica; | ||
| omVal.replicationInfo.isNFS = !isReplica; | ||
| } | ||
|
|
@@ -724,6 +799,48 @@ function putMetadata(request, response, bucketInfo, objMd, log, callback) { | |
| options.isNull = isNull; | ||
| } | ||
|
|
||
| // Cascade triggering | ||
| // If the bucket receiving this replica has its own CRR rules, set | ||
| // status to PENDING so the queue populator here picks it up for the | ||
| // next hop. If not, clear the source-side replicationInfo fields | ||
| // Always mark isReplica=true. | ||
| if (incomingMicroVersionId) { | ||
| const isMDOnly = headers['x-scal-replication-content'] === 'METADATA'; | ||
| const objSize = omVal['content-length'] || 0; | ||
|
|
||
| // These S3-compatible Scality locations are excluded | ||
| // as cascade targets because they use the MultiBackend S3 path which | ||
| // bypasses the putData/putMetadata routes, so loop detection cannot fire | ||
| // on those destinations. | ||
| const BLOCKED_LOCATION_TYPES = ['location-scality-ring-s3-v1', 'location-scality-artesca-s3-v1']; | ||
|
|
||
| const nextReplInfo = getReplicationInfo(config, objectKey, bucketInfo, isMDOnly, objSize, null, null, null); | ||
|
|
||
| if (nextReplInfo) { | ||
| nextReplInfo.backends = nextReplInfo.backends.filter(b => { | ||
| const loc = config.locationConstraints[b.site]; | ||
| return !loc || !BLOCKED_LOCATION_TYPES.includes(loc.type); | ||
| }); | ||
| } | ||
|
|
||
| if (nextReplInfo && nextReplInfo.backends.length > 0) { | ||
| omVal.replicationInfo = nextReplInfo; | ||
| } else { | ||
| omVal.replicationInfo = { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe this should be an arsenal function as its updating metadata edit: no, cannot as we are working with json |
||
| status: '', | ||
| backends: [], | ||
| content: [], | ||
| destination: '', | ||
| storageClass: '', | ||
| role: '', | ||
| storageType: '', | ||
| dataStoreVersionId: '', | ||
| }; | ||
| } | ||
|
|
||
| omVal.replicationInfo.isReplica = true; | ||
| } | ||
|
|
||
| return async.series( | ||
| [ | ||
| // Zenko's CRR delegates replacing the account | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget to revert, I guess it's for testing