-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
JdbcDatabaseContainer.java
231 lines (193 loc) · 8.06 KB
/
JdbcDatabaseContainer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package org.testcontainers.containers;
import lombok.NonNull;
import org.jetbrains.annotations.NotNull;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.utility.MountableFile;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Base class for containers that expose a JDBC connection
*
* @author richardnorth
*/
public abstract class JdbcDatabaseContainer<SELF extends JdbcDatabaseContainer<SELF>> extends GenericContainer<SELF> implements LinkableContainer {
private static final Object DRIVER_LOAD_MUTEX = new Object();
private Driver driver;
protected Map<String, String> parameters = new HashMap<>();
private static final RateLimiter DB_CONNECT_RATE_LIMIT = RateLimiterBuilder.newBuilder()
.withRate(10, TimeUnit.SECONDS)
.withConstantThroughput()
.build();
private int startupTimeoutSeconds = 120;
private int connectTimeoutSeconds = 120;
public JdbcDatabaseContainer(@NonNull final String dockerImageName) {
super(dockerImageName);
}
public JdbcDatabaseContainer(@NonNull final Future<String> image) {
super(image);
}
/**
* @return the name of the actual JDBC driver to use
*/
public abstract String getDriverClassName();
/**
* @return a JDBC URL that may be used to connect to the dockerized DB
*/
public abstract String getJdbcUrl();
/**
* @return the database name
*/
public String getDatabaseName() {
throw new UnsupportedOperationException();
}
/**
* @return the standard database username that should be used for connections
*/
public abstract String getUsername();
/**
* @return the standard password that should be used for connections
*/
public abstract String getPassword();
/**
* @return a test query string suitable for testing that this particular database type is alive
*/
protected abstract String getTestQueryString();
public SELF withUsername(String username) {
throw new UnsupportedOperationException();
}
public SELF withPassword(String password) {
throw new UnsupportedOperationException();
}
public SELF withDatabaseName(String dbName) {
throw new UnsupportedOperationException();
}
/**
* Set startup time to allow, including image pull time, in seconds.
*
* @param startupTimeoutSeconds startup time to allow, including image pull time, in seconds
* @return self
*/
public SELF withStartupTimeoutSeconds(int startupTimeoutSeconds) {
this.startupTimeoutSeconds = startupTimeoutSeconds;
return self();
}
/**
* Set time to allow for the database to start and establish an initial connection, in seconds.
*
* @param connectTimeoutSeconds time to allow for the database to start and establish an initial connection in seconds
* @return self
*/
public SELF withConnectTimeoutSeconds(int connectTimeoutSeconds) {
this.connectTimeoutSeconds = connectTimeoutSeconds;
return self();
}
@Override
protected void waitUntilContainerStarted() {
// Repeatedly try and open a connection to the DB and execute a test query
logger().info("Waiting for database connection to become available at {} using query '{}'", getJdbcUrl(), getTestQueryString());
Unreliables.retryUntilSuccess(getStartupTimeoutSeconds(), TimeUnit.SECONDS, () -> {
if (!isRunning()) {
throw new ContainerLaunchException("Container failed to start");
}
try (Connection connection = createConnection("")) {
boolean success = connection.createStatement().execute(JdbcDatabaseContainer.this.getTestQueryString());
if (success) {
logger().info("Obtained a connection to container ({})", JdbcDatabaseContainer.this.getJdbcUrl());
return null;
} else {
throw new SQLException("Failed to execute test query");
}
}
});
}
/**
* Obtain an instance of the correct JDBC driver for this particular database container type
*
* @return a JDBC Driver
*/
public Driver getJdbcDriverInstance() {
synchronized (DRIVER_LOAD_MUTEX) {
if (driver == null) {
try {
driver = (Driver) Class.forName(this.getDriverClassName()).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
throw new RuntimeException("Could not get Driver", e);
}
}
}
return driver;
}
/**
* Creates a connection to the underlying containerized database instance.
*
* @param queryString query string parameters that should be appended to the JDBC connection URL.
* The '?' character must be included
* @return a Connection
* @throws SQLException if there is a repeated failure to create the connection
*/
public Connection createConnection(String queryString) throws SQLException {
final Properties info = new Properties();
info.put("user", this.getUsername());
info.put("password", this.getPassword());
final String url = constructUrlForConnection(queryString);
final Driver jdbcDriverInstance = getJdbcDriverInstance();
try {
return Unreliables.retryUntilSuccess(getConnectTimeoutSeconds(), TimeUnit.SECONDS, () ->
DB_CONNECT_RATE_LIMIT.getWhenReady(() ->
jdbcDriverInstance.connect(url, info)));
} catch (Exception e) {
throw new SQLException("Could not create new connection", e);
}
}
/**
* Template method for constructing the JDBC URL to be used for creating {@link Connection}s.
* This should be overridden if the JDBC URL and query string concatenation or URL string
* construction needs to be different to normal.
*
* @param queryString query string parameters that should be appended to the JDBC connection URL.
* The '?' character must be included
* @return a full JDBC URL including queryString
*/
protected String constructUrlForConnection(String queryString) {
return getJdbcUrl() + queryString;
}
protected void optionallyMapResourceParameterAsVolume(@NotNull String paramName, @NotNull String pathNameInContainer, @NotNull String defaultResource) {
String resourceName = parameters.getOrDefault(paramName, defaultResource);
if (resourceName != null) {
final MountableFile mountableFile = MountableFile.forClasspathResource(resourceName);
addFileSystemBind(mountableFile.getResolvedPath(), pathNameInContainer, BindMode.READ_ONLY);
}
}
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
@SuppressWarnings("unused")
public void addParameter(String paramName, String value) {
this.parameters.put(paramName, value);
}
/**
* @return startup time to allow, including image pull time, in seconds
* @deprecated should not be overridden anymore, use {@link #withStartupTimeoutSeconds(int)} in constructor instead
*/
@Deprecated
protected int getStartupTimeoutSeconds() {
return startupTimeoutSeconds;
}
/**
* @return time to allow for the database to start and establish an initial connection, in seconds
* @deprecated should not be overridden anymore, use {@link #withConnectTimeoutSeconds(int)} in constructor instead
*/
@Deprecated
protected int getConnectTimeoutSeconds() {
return connectTimeoutSeconds;
}
}