-
Notifications
You must be signed in to change notification settings - Fork 23
Support per-backend role and destination in replication engine #2743
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 |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| 'use strict'; | ||
|
|
||
| const async = require('async'); | ||
| const { callbackify } = require('util'); | ||
| const { EventEmitter } = require('events'); | ||
| const Redis = require('ioredis'); | ||
| const schedule = require('node-schedule'); | ||
|
|
@@ -18,8 +19,6 @@ const QueueEntry = require('../../../lib/models/QueueEntry'); | |
| const TaskScheduler = require('../../../lib/tasks/TaskScheduler'); | ||
| const { getTaskSchedulerQueueKey, | ||
| getTaskSchedulerDedupeKey } = require('./taskSchedulerHelpers'); | ||
| const getLocationsFromStorageClass = | ||
| require('../utils/getLocationsFromStorageClass'); | ||
| const ReplicateObject = require('../tasks/ReplicateObject'); | ||
| const MultipleBackendTask = require('../tasks/MultipleBackendTask'); | ||
| const CopyLocationTask = require('../tasks/CopyLocationTask'); | ||
|
|
@@ -741,7 +740,7 @@ class QueueProcessor extends EventEmitter { | |
| } | ||
| this._consumer = this._createConsumer( | ||
| this.topic, | ||
| this.processReplicationEntry.bind(this), options); | ||
| callbackify(this.processReplicationEntry.bind(this)), options); | ||
| return this._consumer.once('canary', done); | ||
| }, | ||
| done => { | ||
|
|
@@ -864,41 +863,75 @@ class QueueProcessor extends EventEmitter { | |
| * @param {function} done - callback function | ||
| * @return {undefined} | ||
| */ | ||
| processReplicationEntry(kafkaEntry, done) { | ||
| async processReplicationEntry(kafkaEntry) { | ||
| const sourceEntry = QueueEntry.createFromKafkaEntry(kafkaEntry); | ||
|
|
||
| if (sourceEntry.error) { | ||
| this.logger.error('error processing replication entry', { error: sourceEntry.error }); | ||
| return process.nextTick(() => done(errors.InternalError)); | ||
| throw errors.InternalError; | ||
| } | ||
|
|
||
| if (sourceEntry.skip) { | ||
| // skip message, noop | ||
| return process.nextTick(done); | ||
| return; | ||
| } | ||
| let task; | ||
|
|
||
| const logSkip = () => { | ||
| this.logger.debug('skip replication entry', { entry: sourceEntry.getLogInfo() }); | ||
| }; | ||
|
|
||
| // Route Bucket Queue Entries | ||
| if (sourceEntry instanceof BucketQueueEntry) { | ||
| if (this.echoMode) { | ||
| task = new EchoBucket(this); | ||
| } | ||
| // ignore bucket entry if echo mode disabled | ||
| } else if (sourceEntry instanceof ObjectQueueEntry) { | ||
| const replicationStorageClass = | ||
| sourceEntry.getReplicationStorageClass(); | ||
| const sites = getLocationsFromStorageClass(replicationStorageClass); | ||
| if (sites.includes(this.site)) { | ||
| if (this.destConfig.replicationEndpoint && | ||
| replicationBackends.includes(this.destConfig.replicationEndpoint.type)) { | ||
| task = new MultipleBackendTask(this); | ||
| } else { | ||
| task = new ReplicateObject(this); | ||
| } | ||
| if (!this.echoMode) { | ||
| logSkip(); | ||
| return; | ||
| } | ||
| } | ||
| if (task) { | ||
|
|
||
| this.logger.debug('replication entry is being pushed', { entry: sourceEntry.getLogInfo() }); | ||
| return this.taskScheduler.push({ task, entry: sourceEntry, kafkaEntry }, done); | ||
| await new Promise((resolve, reject) => { | ||
| this.taskScheduler.push({ task: new EchoBucket(this), entry: sourceEntry, kafkaEntry }, | ||
| err => err ? reject(err) : resolve()); | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| // Route Object Queue Entries | ||
| if (!(sourceEntry instanceof ObjectQueueEntry)) { | ||
| logSkip(); | ||
| return; | ||
| } | ||
|
|
||
| const pendingBackends = sourceEntry.getReplicationBackends() | ||
| .filter(b => b.status === 'PENDING' && b.site === this.site); | ||
|
|
||
| if (pendingBackends.length === 0) { | ||
| logSkip(); | ||
| return; | ||
| } | ||
|
|
||
| const endpointType = this.destConfig.replicationEndpoint?.type; | ||
| const isCloud = endpointType && replicationBackends.includes(endpointType); | ||
|
|
||
| // Sequential loop over pending backends | ||
| for (const backend of pendingBackends) { | ||
| const task = new (isCloud ? MultipleBackendTask : ReplicateObject)(this); | ||
| const perBackendEntry = sourceEntry.clone() | ||
| .setSite(backend.site) | ||
| .setDestination(backend.destination) | ||
| .setRole(backend.role); | ||
|
|
||
| this.logger.debug('replication entry is being pushed', { | ||
| entry: perBackendEntry.getLogInfo(), | ||
| destination: backend.destination, | ||
| role: backend.role, | ||
| }); | ||
|
|
||
| await new Promise((resolve, reject) => { | ||
| this.taskScheduler.push( | ||
| { task, entry: perBackendEntry, kafkaEntry }, | ||
| err => err ? reject(err) : resolve(), | ||
| ); | ||
| }); | ||
| } | ||
|
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. The sequential |
||
| this.logger.debug('skip replication entry', { entry: sourceEntry.getLogInfo() }); | ||
| return process.nextTick(done); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,7 +85,8 @@ class MultipleBackendTask extends ReplicateObject { | |
| .then(data => { | ||
| const replicationEnabled = data.ReplicationConfiguration.Rules | ||
| .some(rule => rule.Status === 'Enabled' && | ||
| entry.getObjectKey().startsWith(rule.Prefix)); | ||
| entry.getObjectKey().startsWith( | ||
|
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.
|
||
| rule.Filter?.Prefix ?? rule.Prefix ?? '')); | ||
| if (!replicationEnabled) { | ||
| errMessage = 'replication disabled for object'; | ||
| log.debug(errMessage, { | ||
|
|
@@ -317,7 +318,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendAbortMPUCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
|
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. Several calls in |
||
| StorageClass: this.site, | ||
| UploadId: uploadId, | ||
| RequestUids: log.getSerializedUids(), | ||
|
|
@@ -353,7 +354,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendCompleteMPUCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| VersionId: sourceEntry.getEncodedVersionId() || 'null', | ||
| UserMetaData: sourceEntry.getUserMetadata(), | ||
|
|
@@ -445,7 +446,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendPutMPUPartCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| PartNumber: partNumber, | ||
| UploadId: uploadId, | ||
|
|
@@ -506,7 +507,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendInitiateMPUCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| VersionId: sourceEntry.getEncodedVersionId() || 'null', | ||
| UserMetaData: sourceEntry.getUserMetadata(), | ||
|
|
@@ -951,7 +952,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| Key: sourceEntry.getObjectKey(), | ||
| CanonicalID: sourceEntry.getOwnerId(), | ||
| ContentMD5: sourceEntry.getContentMd5(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| VersionId: sourceEntry.getEncodedVersionId() || 'null', | ||
| UserMetaData: sourceEntry.getUserMetadata(), | ||
|
|
@@ -1008,7 +1009,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendPutObjectTaggingCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| DataStoreVersionId: | ||
| sourceEntry.getReplicationSiteDataStoreVersionId(this.site), | ||
|
|
@@ -1065,7 +1066,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendDeleteObjectTaggingCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| DataStoreVersionId: | ||
| sourceEntry.getReplicationSiteDataStoreVersionId(this.site), | ||
|
|
@@ -1146,7 +1147,7 @@ class MultipleBackendTask extends ReplicateObject { | |
| const command = new MultipleBackendDeleteObjectCommand({ | ||
| Bucket: sourceEntry.getBucket(), | ||
| Key: sourceEntry.getObjectKey(), | ||
| StorageType: sourceEntry.getReplicationStorageType(), | ||
| StorageType: this._getReplicationEndpointType(), | ||
| StorageClass: this.site, | ||
| RequestUids: log.getSerializedUids(), | ||
| }); | ||
|
|
||
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.
The
_updateReplicationInfocall on line 349 setsrole: ReplicationConfiguration.resolveSourceRole(bucketRepInfo.role)but doesn't setdestinationorstorageClassin the replication info object. The old code set bothdestinationandstorageClass. If these fields are now handled insideReplicationConfiguration.resolveBackendsat the backend level, this is fine — but please confirm the downstream consumers (queue populator, status processor) don't rely on the top-leveldestination/storageClassfields being present.— Claude Code