matrei commented on code in PR #15747:
URL: https://github.com/apache/grails-core/pull/15747#discussion_r3475393002
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
Review Comment:
```suggestion
LOG.info("Updated TTL of index [{}] on entity [{}] to {}s",
existingName, entity.getName(), expireAfterSeconds);
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
Review Comment:
```suggestion
LOG.error("Failed to create index for entity [{}] {}: {}",
entity.getName(), descriptor, e.getMessage(), e);
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
Review Comment:
```suggestion
LOG.error("Failed to create index for entity [{}] {}: {}",
entity.getName(), descriptor, original.getMessage(),
original);
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
+ return;
+ } catch (MongoCommandException collModError) {
+ // collMod can't make every change (e.g. add a TTL to a
non-TTL index on older
+ // servers) — fall through to recreate (if authorised) rather
than fail outright.
+ LOG.warn("collMod TTL update failed for index [" +
existingName + "] on entity [" + entity.getName() + "]: " +
+ collModError.getMessage() + (recreateOnConflict ? " —
recreating" : ""));
Review Comment:
```suggestion
LOG.warn("collMod TTL update failed for index [{}] on entity
[{}]: {}{}",
existingName, entity.getName(),
collModError.getMessage(), recreateOnConflict ? " — recreating" : "");
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
Review Comment:
```suggestion
LOG.error("Failed to create index for entity [{}] {} and could
not inspect existing indexes: {}",
entity.getName(), descriptor, listError.getMessage(),
original);
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
+ return;
+ } catch (MongoCommandException collModError) {
+ // collMod can't make every change (e.g. add a TTL to a
non-TTL index on older
+ // servers) — fall through to recreate (if authorised) rather
than fail outright.
+ LOG.warn("collMod TTL update failed for index [" +
existingName + "] on entity [" + entity.getName() + "]: " +
+ collModError.getMessage() + (recreateOnConflict ? " —
recreating" : ""));
+ }
+ }
+
+ if (recreateOnConflict) {
+ try {
+ collection.dropIndex(existingName);
+ collection.createIndex(keys, desired);
+ LOG.info("Recreated index [" + existingName + "] on entity ["
+ entity.getName() + "] " + descriptor);
+ } catch (MongoCommandException recreateError) {
+ LOG.error("Failed to recreate index [" + existingName + "] on
entity [" + entity.getName() + "] " + descriptor + ": " +
recreateError.getMessage(), recreateError);
+ }
+ return;
+ }
+
+ LOG.error("Index conflict for entity [" + entity.getName() + "] " +
descriptor + ": an index [" + existingName +
+ "] already exists on the same keys with different options.
Declare indexAttributes:[recreateOnConflict:true]" +
+ " to drop and recreate it. Original error: " +
original.getMessage());
+ }
+
+ /**
+ * Find an existing index whose key pattern matches the given keys, or
{@code null} if none.
+ * Directions/types are compared numerically (1 vs 1.0) so driver-returned
values match.
+ *
+ * <p>Text indexes are special-cased: a declared text index has key {@code
{field: 'text'}}, but
+ * MongoDB reports an existing one with a synthetic {@code {_fts: 'text',
_ftsx: 1}} key, so the
+ * two never match by pattern. Since MongoDB allows at most one text index
per collection, an
+ * existing text index is unambiguously the one a newly-declared text
index conflicts with —
+ * match it regardless of its key shape or name so {@code
recreateOnConflict} can absorb it.</p>
+ */
+ private static Document
findIndexByKeyPattern(com.mongodb.client.MongoCollection<Document> collection,
Document keys) {
+ boolean desiredIsText = isTextIndex(keys);
+ for (Document idx : collection.listIndexes()) {
+ Object key = idx.get("key");
+ if (!(key instanceof Document)) {
+ continue;
+ }
+ if (desiredIsText && isTextIndex((Document) key)) {
+ return idx;
+ }
+ if (sameKeyPattern((Document) key, keys)) {
+ return idx;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * True for a text index in either representation: a declaration ({@code
{field: 'text'}}) or the
+ * synthetic key MongoDB reports for an existing one ({@code {_fts:
'text', _ftsx: 1}}).
+ */
+ private static boolean isTextIndex(Document key) {
+ if (key.containsKey("_fts")) {
+ return true;
+ }
+ for (Object v : key.values()) {
+ if ("text".equals(v)) {
+ return true;
}
}
+ return false;
+ }
+ private static boolean sameKeyPattern(Document existingKey, Document
desiredKey) {
+ if (existingKey.size() != desiredKey.size()) {
+ return false;
+ }
+ for (Map.Entry<String, Object> entry : desiredKey.entrySet()) {
+ if (!existingKey.containsKey(entry.getKey())) {
+ return false;
+ }
+ Object a = existingKey.get(entry.getKey());
+ Object b = entry.getValue();
+ if (a instanceof Number && b instanceof Number) {
+ if (((Number) a).doubleValue() != ((Number) b).doubleValue()) {
+ return false;
+ }
+ } else if (a == null ? b != null : !a.equals(b)) {
Review Comment:
```suggestion
} else if (!Objects.equals(a, b)) {
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
+ return;
+ } catch (MongoCommandException collModError) {
+ // collMod can't make every change (e.g. add a TTL to a
non-TTL index on older
+ // servers) — fall through to recreate (if authorised) rather
than fail outright.
+ LOG.warn("collMod TTL update failed for index [" +
existingName + "] on entity [" + entity.getName() + "]: " +
+ collModError.getMessage() + (recreateOnConflict ? " —
recreating" : ""));
+ }
+ }
+
+ if (recreateOnConflict) {
+ try {
+ collection.dropIndex(existingName);
+ collection.createIndex(keys, desired);
+ LOG.info("Recreated index [" + existingName + "] on entity ["
+ entity.getName() + "] " + descriptor);
+ } catch (MongoCommandException recreateError) {
+ LOG.error("Failed to recreate index [" + existingName + "] on
entity [" + entity.getName() + "] " + descriptor + ": " +
recreateError.getMessage(), recreateError);
Review Comment:
```suggestion
LOG.error("Failed to recreate index [{}] on entity [{}] {}:
{}",
existingName, entity.getName(), descriptor,
recreateError.getMessage(), recreateError);
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
+ return;
+ } catch (MongoCommandException collModError) {
+ // collMod can't make every change (e.g. add a TTL to a
non-TTL index on older
+ // servers) — fall through to recreate (if authorised) rather
than fail outright.
+ LOG.warn("collMod TTL update failed for index [" +
existingName + "] on entity [" + entity.getName() + "]: " +
+ collModError.getMessage() + (recreateOnConflict ? " —
recreating" : ""));
+ }
+ }
+
+ if (recreateOnConflict) {
+ try {
+ collection.dropIndex(existingName);
+ collection.createIndex(keys, desired);
+ LOG.info("Recreated index [" + existingName + "] on entity ["
+ entity.getName() + "] " + descriptor);
+ } catch (MongoCommandException recreateError) {
+ LOG.error("Failed to recreate index [" + existingName + "] on
entity [" + entity.getName() + "] " + descriptor + ": " +
recreateError.getMessage(), recreateError);
+ }
+ return;
+ }
+
+ LOG.error("Index conflict for entity [" + entity.getName() + "] " +
descriptor + ": an index [" + existingName +
+ "] already exists on the same keys with different options.
Declare indexAttributes:[recreateOnConflict:true]" +
+ " to drop and recreate it. Original error: " +
original.getMessage());
Review Comment:
```suggestion
LOG.error(
"Index conflict for entity [{}] {}: an index [{}] already exists
on the same keys with different options. " +
"Declare indexAttributes:[recreateOnConflict:true] to drop
and recreate it. Original error: {}",
entity.getName(), descriptor, existingName,
original.getMessage());
```
##########
grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/MongoDatastore.java:
##########
@@ -897,20 +904,180 @@ protected void initializeIndices(final PersistentEntity
entity) {
options.putAll(attributes);
}
}
- // continue using deprecated method to support older versions
of MongoDB
- try {
- if (options.isEmpty()) {
- collection.createIndex(dbObject);
- } else {
- final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
- collection.createIndex(dbObject, indexOptions);
- }
- } catch (MongoCommandException e) {
- LOG.error("Failed to create index for entity [" +
entity.getName() + "] on property [" + property.getName() + "]: " +
e.getMessage(), e);
- }
+ createOrUpdateIndex(entity, collection, dbObject, options,
+ "on property [" + property.getName() + "]");
+ }
+ }
+
+ }
+
+ /**
+ * Create an index, reconciling option conflicts with any pre-existing
index on the same keys.
+ *
+ * <p>Two things this does beyond a raw {@code createIndex}:</p>
+ * <ol>
+ * <li>Applies {@code expireAfterSeconds} (TTL) — the one option {@link
MongoConstants#mapToObject}
+ * cannot set, because the driver only exposes the two-argument
{@link IndexOptions#expireAfter}.</li>
+ * <li>On {@code IndexOptionsConflict} (an index already exists on these
keys with different
+ * options), reconciles instead of only logging: a TTL change is
applied in place with
+ * {@code collMod} (no drop, no rebuild, no gap); any other conflict
is dropped and
+ * recreated only when {@code recreateOnConflict:true} was declared,
else logged with guidance.</li>
+ * </ol>
+ */
+ private void createOrUpdateIndex(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, Map<String, Object>
rawOptions, String descriptor) {
+ Map<String, Object> options = rawOptions != null ? new
HashMap<>(rawOptions) : new HashMap<>();
+
+ // Control flag — not a Mongo index option.
+ boolean recreateOnConflict =
Boolean.TRUE.equals(options.remove(INDEX_RECREATE_ON_CONFLICT));
+
+ Long expireAfterSeconds = null;
+ Object ttl = options.remove(INDEX_EXPIRE_AFTER_SECONDS);
+ if (ttl instanceof Number) {
+ expireAfterSeconds = ((Number) ttl).longValue();
+ }
+
+ final IndexOptions indexOptions =
MongoConstants.mapToObject(IndexOptions.class, options);
+ if (expireAfterSeconds != null) {
+ indexOptions.expireAfter(expireAfterSeconds, TimeUnit.SECONDS);
+ }
+
+ try {
+ collection.createIndex(keys, indexOptions);
+ } catch (MongoCommandException e) {
+ if (e.getErrorCode() == INDEX_OPTIONS_CONFLICT_CODE) {
+ reconcileIndexConflict(entity, collection, keys, indexOptions,
+ expireAfterSeconds, recreateOnConflict, descriptor, e);
+ } else {
+ LOG.error("Failed to create index for entity [" +
entity.getName() + "] " + descriptor + ": " + e.getMessage(), e);
+ }
+ }
+ }
+
+ /**
+ * Reconcile an {@code IndexOptionsConflict}: an index already exists on
the same keys with
+ * different options. A TTL difference is the common, safe case (e.g. a
configurable retention
+ * changed between restarts) and is updated in place via {@code collMod};
anything else needs an
+ * explicit {@code recreateOnConflict:true} to authorise the
drop-and-recreate.
+ */
+ private void reconcileIndexConflict(PersistentEntity entity,
+
com.mongodb.client.MongoCollection<Document> collection,
+ Document keys, IndexOptions desired,
Long expireAfterSeconds,
+ boolean recreateOnConflict, String
descriptor, MongoCommandException original) {
+ Document existing;
+ try {
+ existing = findIndexByKeyPattern(collection, keys);
+ } catch (RuntimeException listError) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor +
+ " and could not inspect existing indexes: " +
listError.getMessage(), original);
+ return;
+ }
+ if (existing == null) {
+ LOG.error("Failed to create index for entity [" + entity.getName()
+ "] " + descriptor + ": " + original.getMessage(), original);
+ return;
+ }
+
+ String existingName = existing.getString("name");
+ Object existingTtl = existing.get(INDEX_EXPIRE_AFTER_SECONDS);
+ Long existingTtlSeconds = existingTtl instanceof Number ? ((Number)
existingTtl).longValue() : null;
+
+ // TTL change on an existing index — update in place, no rebuild, no
gap.
+ boolean ttlChange = expireAfterSeconds != null &&
!expireAfterSeconds.equals(existingTtlSeconds);
+ if (ttlChange) {
+ try {
+ getMongoClient().getDatabase(getDatabaseName(entity))
+ .runCommand(new Document("collMod",
getCollectionName(entity))
+ .append("index", new Document("name",
existingName)
+ .append(INDEX_EXPIRE_AFTER_SECONDS,
expireAfterSeconds)));
+ LOG.info("Updated TTL of index [" + existingName + "] on
entity [" + entity.getName() + "] to " + expireAfterSeconds + "s");
+ return;
+ } catch (MongoCommandException collModError) {
+ // collMod can't make every change (e.g. add a TTL to a
non-TTL index on older
+ // servers) — fall through to recreate (if authorised) rather
than fail outright.
+ LOG.warn("collMod TTL update failed for index [" +
existingName + "] on entity [" + entity.getName() + "]: " +
+ collModError.getMessage() + (recreateOnConflict ? " —
recreating" : ""));
+ }
+ }
+
+ if (recreateOnConflict) {
+ try {
+ collection.dropIndex(existingName);
+ collection.createIndex(keys, desired);
+ LOG.info("Recreated index [" + existingName + "] on entity ["
+ entity.getName() + "] " + descriptor);
Review Comment:
```suggestion
LOG.info("Recreated index [{}] on entity [{}] {}",
existingName, entity.getName(), descriptor);
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]