Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ class SbomPlugin implements Plugin<Project> {
'pkg:maven/jline/jline@2.14.6?type=jar' : 'BSD-2-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
'pkg:maven/opensymphony/sitemesh@2.6.0?type=jar' : 'OpenSymphony', // custom license approved by legal LEGAL-707
'pkg:maven/org.antlr/antlr4-runtime@4.7.2?type=jar' : 'BSD-3-Clause', // maps incorrectly because of https://github.com/CycloneDX/cyclonedx-core-java/issues/205
// mongo-java-server declares only "The BSD License" so it maps to BSD-4-Clause for the same
// reason as the org.jline group below. https://github.com/bwaldvogel/mongo-java-server/blob/main/LICENSE
// has three numbered clauses and no advertising clause, making it BSD-3-Clause.
'pkg:maven/de.bwaldvogel/mongo-java-server@1.47.0?type=jar' : 'BSD-3-Clause',
'pkg:maven/de.bwaldvogel/mongo-java-server-core@1.47.0?type=jar' : 'BSD-3-Clause',
'pkg:maven/de.bwaldvogel/mongo-java-server-memory-backend@1.47.0?type=jar': 'BSD-3-Clause',
// The whole org.jline group declares "The BSD License", which maps incorrectly because of
// https://github.com/CycloneDX/cyclonedx-core-java/issues/205 - the POMs point at BSD-3-Clause.
// jline.version tracks the JLine version Groovy ships, so every module resolves to one version.
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ githubSlug=apache/grails-core
apacheDsVersion=1.5.4
apacheMavenVersion=3.9.9
apacheMavenResolverVersion=1.9.22
flapdoodleVersion=4.33.0
mongoJavaServerVersion=1.47.0
commonsValidatorVersion=1.9.0
concurrentlinkedhashmapLruVersion=1.4.2
defaultElImplementationVersion=5.0.0
Expand Down
1 change: 1 addition & 0 deletions gradle/publish-root-config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def publishedProjects = [
'grails-data-mongodb',
'grails-data-mongodb-bson',
'grails-data-mongodb-core',
'grails-data-mongodb-embedded',
'grails-data-mongodb-ext',
'grails-data-mongodb-gson-templates',
'grails-data-mongodb-spring-boot',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.env.PropertyResolver;
import org.springframework.transaction.PlatformTransactionManager;
Expand Down Expand Up @@ -114,7 +115,7 @@
* @author Graeme Rocher
* @since 1.0
*/
public class MongoDatastore extends AbstractDatastore implements MappingContext.Listener, Closeable, StatelessDatastore, MultipleConnectionSourceCapableDatastore, MultiTenantCapableDatastore<MongoClient, MongoConnectionSourceSettings>, TransactionCapableDatastore {
public class MongoDatastore extends AbstractDatastore implements MappingContext.Listener, Closeable, StatelessDatastore, MultipleConnectionSourceCapableDatastore, MultiTenantCapableDatastore<MongoClient, MongoConnectionSourceSettings>, TransactionCapableDatastore, SmartLifecycle {

public static final String SETTING_DATABASE_NAME = MongoSettings.SETTING_DATABASE_NAME;
public static final String SETTING_CONNECTION_STRING = MongoSettings.SETTING_CONNECTION_STRING;
Expand Down Expand Up @@ -149,7 +150,12 @@ public class MongoDatastore extends AbstractDatastore implements MappingContext.
private static final int INDEX_OPTIONS_CONFLICT_CODE = 85;
public static final String CODEC_ENGINE = MongoConstants.CODEC_ENGINE;

protected final MongoClient mongo;
/**
* Not final because {@link #start()} replaces it after a CRaC restore. Everything other
* than construction reaches it through {@link #getMongoClient()}, so a replacement is
* picked up without anything else having to be told.
*/
protected volatile MongoClient mongo;
protected final String defaultDatabase;
protected final Map<PersistentEntity, String> mongoCollections = new ConcurrentHashMap<>();
protected final Map<PersistentEntity, String> mongoDatabases = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -1158,9 +1164,74 @@ public void persistentEntityAdded(PersistentEntity entity) {
initializeIndices(entity);
}

/**
* Below the web server's phase so the client outlives the requests using it, and above
* {@code EmbeddedMongoLifecycle.PHASE} so an embedded server outlives this client:
* Spring starts in ascending phase order and stops in descending.
*/
public static final int LIFECYCLE_PHASE = -1000;

private volatile boolean running = true;

/**
* Closes the {@link MongoClient} so the process can be checkpointed.
*
* <p>CRaC refuses to checkpoint a process holding open sockets, and a connected driver
* holds one per pooled connection plus its server monitors. Closing the client shuts the
* monitor threads down and releases every socket, which nothing else in the driver
* offers: draining the pool leaves the monitors connected.
*
* <p>A client the application supplied is left alone. Its lifecycle belongs to whoever
* created it, and this datastore stays {@link #isRunning() running} so that
* {@link #start()} does not later replace something it does not own.
*/
@Override
public void stop() {
if (!this.running || !ownsClient()) {
return;
}
this.mongo.close();
this.running = false;
}

/**
* Builds a replacement {@link MongoClient} after a restore, using the same factory and
* configuration the original was built from, so settings applied at startup still apply.
*/
@Override
public void start() {
if (this.running) {
return;
}
this.mongo = connectionSources.getFactory()
.create(ConnectionSource.DEFAULT, connectionSources.getBaseConfiguration())
.getSource();
this.running = true;
}

@Override
public boolean isRunning() {
return this.running;
}

@Override
public int getPhase() {
return LIFECYCLE_PHASE;
}

/**
* Whether GORM created the client and may close it, as opposed to it having been handed
* in by the application.
*/
private boolean ownsClient() {
ConnectionSource<MongoClient, MongoConnectionSourceSettings> source = connectionSources.getDefaultConnectionSource();
return !(source instanceof DefaultConnectionSource) || ((DefaultConnectionSource<?, ?>) source).isCloseable();
}

@Override
@PreDestroy
public void close() {
MongoClient current = this.mongo;
try {
super.destroy();
} catch (Exception e) {
Expand All @@ -1170,6 +1241,11 @@ public void close() {
if (connectionSources != null) {
connectionSources.close();
}
// connectionSources closes the client it was built with, which is no longer the
// one in use once a restore has replaced it.
if (current != null && ownsClient()) {
current.close();
}
} catch (IOException e) {
LOG.error("There was an error shutting down GORM for an entity: " + e.getMessage(), e);
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.grails.datastore.mapping.mongo

import java.util.concurrent.TimeUnit

import com.mongodb.MongoClientSettings
import com.mongodb.MongoTimeoutException
import com.mongodb.client.MongoClient

import org.grails.datastore.mapping.core.DatastoreUtils
import org.grails.datastore.mapping.mongo.config.MongoMappingContext

import spock.lang.Specification

/**
* Covers the {@code SmartLifecycle} contract the datastore takes part in.
*
* <p>CRaC refuses to checkpoint a process holding open sockets, and a connected driver holds
* one per pooled connection plus its server monitors. Spring stops lifecycle beans before the
* checkpoint and starts them again after the restore, so closing the client on stop is what
* lets an application using MongoDB be snapshotted at all -- and building a replacement on
* start is what leaves the restored process able to query anything.
*
* <p>No server is needed to tell an open client from a closed one: see {@link #closed}.
*/
class MongoDatastoreLifecycleSpec extends Specification {

void 'a datastore is running from the moment it is built'() {
given:
MongoDatastore datastore = ownedClientDatastore()

expect:
datastore.running

and: 'stopping after the web server and before the embedded MongoDB it may be talking to'
datastore.phase == MongoDatastore.LIFECYCLE_PHASE
datastore.phase < 0

cleanup:
datastore.close()
}

void 'stopping closes the client GORM owns, which is what releases its sockets'() {
given:
MongoDatastore datastore = ownedClientDatastore()
MongoClient client = datastore.mongoClient

expect: 'the client is usable to begin with'
!closed(client)

when: 'the checkpoint stops it'
datastore.stop()

then: 'draining the pool would leave the monitors connected, so the client itself is closed'
!datastore.running
closed(client)

cleanup:
datastore.close()
}

void 'starting after a stop builds a replacement from the same configuration'() {
given:
MongoDatastore datastore = ownedClientDatastore()
MongoClient original = datastore.mongoClient
datastore.stop()

when: 'the restore starts it again'
datastore.start()

then:
datastore.running

and: 'a closed client cannot be reopened, so the restored process gets a new one'
!datastore.mongoClient.is(original)
!closed(datastore.mongoClient)

cleanup:
datastore.close()
}

void 'closing after a restore closes the replacement rather than only the client it replaced'() {
given: 'a datastore that has been through a checkpoint and a restore'
MongoDatastore datastore = ownedClientDatastore()
datastore.stop()
datastore.start()
MongoClient restored = datastore.mongoClient

when: 'the application shuts down for real'
datastore.close()

then: 'the connection sources only know the client they were built with, so the one ' +
'actually in use has to be closed as well rather than left holding sockets'
closed(restored)
}

void 'stopping an already stopped datastore leaves it alone'() {
given:
MongoDatastore datastore = ownedClientDatastore()

when:
datastore.stop()
datastore.stop()

then:
!datastore.running

when: 'and a running datastore is started again, which would otherwise leak a client'
datastore.start()
MongoClient restored = datastore.mongoClient
datastore.start()

then:
datastore.running
datastore.mongoClient.is(restored)

cleanup:
datastore.close()
}

void 'a client the application supplied is neither closed nor replaced'() {
given: 'a datastore built around an externally managed MongoClient'
MongoClient supplied = Mock(MongoClient)
MongoDatastore datastore = new MongoDatastore(supplied)

when: 'the checkpoint stops it'
datastore.stop()

then: 'whoever created the client owns closing it, checkpoint or not'
0 * supplied.close()

and: 'so it stays running, and a later start does not replace something it does not own'
datastore.running

when:
datastore.start()

then:
datastore.mongoClient.is(supplied)

cleanup:
datastore.close()
}

/**
* Whether the driver has been closed, which needs no MongoDB to answer: selecting a server
* from a closed cluster is rejected outright, while an open client with nothing to connect
* to waits for the server selection timeout and gives up.
*/
private static boolean closed(MongoClient client) {
try {
client.listDatabaseNames().first()
false
}
catch (IllegalStateException ignored) {
true
}
catch (MongoTimeoutException ignored) {
false
}
}

private static MongoDatastore ownedClientDatastore() {
MongoClientSettings.Builder clientOptions = MongoClientSettings.builder()
.applyToClusterSettings { it.serverSelectionTimeout(50, TimeUnit.MILLISECONDS) }
new MongoDatastore(clientOptions,
DatastoreUtils.createPropertyResolver([:]),
new MongoMappingContext('test'))
}
}
Loading
Loading