001package io.ebean.docker.commands; 002 003import io.ebean.docker.commands.process.ProcessResult; 004 005import java.io.File; 006import java.nio.file.Path; 007import java.sql.Connection; 008import java.sql.PreparedStatement; 009import java.sql.SQLException; 010import java.util.ArrayList; 011import java.util.List; 012import java.util.Properties; 013 014import static io.ebean.docker.commands.process.ProcessHandler.process; 015 016public class NuoDBContainer extends JdbcBaseDbContainer { 017 018 private static final String AD_RESET = "com.nuodb.nagent.AgentMain main Entering initializing for server"; 019 private static final String AD_RUNNING = "com.nuodb.nagent.AgentMain main NuoAdmin Server running"; 020 private static final String SM_RESET = "Starting Storage Manager"; 021 private static final String SM_RUNNING = "Database formed"; 022 private static final String SM_UNABLE_TO_CONNECT = "Unable to connect "; 023 private static final String TE_RESET = "Starting Transaction Engine"; 024 private static final String TE_RUNNING = "Database entered"; 025 026 /** 027 * Create NuoDB container with configuration from properties. 028 */ 029 public static NuoDBContainer create(String version, Properties properties) { 030 return new NuoDBContainer(new NuoDBConfig(version, properties)); 031 } 032 033 private final NuoDBConfig nuoConfig; 034 private final String network; 035 private final String adName; 036 private final String smName; 037 private final String teName; 038 039 public NuoDBContainer(NuoDBConfig config) { 040 super(config); 041 this.checkConnectivityUsingAdmin = true; 042 config.initDefaultSchema(); 043 this.nuoConfig = config; 044 this.network = config.getNetwork(); 045 this.adName = nuoConfig.containerName(); 046 this.smName = adName + "_" + nuoConfig.getSm1(); 047 this.teName = adName + "_" + nuoConfig.getTe1(); 048 } 049 050 @Override 051 public void stopRemove() { 052 if (stopDatabase()) { 053 commands.removeContainers(teName, smName, adName); 054 } 055 if (networkExists()) { 056 removeNetwork(); 057 } 058 } 059 060 private void removeNetwork() { 061 process(procNetworkRemove()); 062 } 063 064 @Override 065 public void stopOnly() { 066 stopDatabase(); 067 } 068 069 private boolean stopDatabase() { 070 071 // nuocmd shutdown database --db-name testdb 072 List<String> args = new ArrayList<>(); 073 args.add(config.docker); 074 args.add("exec"); 075 args.add("-i"); 076 args.add(adName); 077 args.add("nuocmd"); 078 args.add("shutdown"); 079 args.add("database"); 080 args.add("--db-name"); 081 args.add(dbConfig.getDbName()); 082 083 final ProcessResult result = process(createProcessBuilder(args)); 084 if (!result.success()) { 085 log.error("Error performing shutdown database " + result); 086 return false; 087 } 088 waitTime(100); 089 commands.stop(adName); 090 return true; 091 } 092 093 @Override 094 void runContainer() { 095 createNetwork(); 096 process(runAdminProcess()); 097 if (waitForAdminProcess()) { 098 process(runStorageManager()); 099 if (waitForStorageManager()) { 100 process(runTransactionManager()); 101 waitForTransactionManager(); 102 } 103 } 104 } 105 106 private boolean waitForTransactionManager() { 107 return waitForLogs(teName, TE_RUNNING, TE_RESET) && waitTime(100); 108 } 109 110 private boolean storageManagerUnableToConnect() { 111 112 boolean unableToConnect = false; 113 114 final List<String> logs = commands.logs(smName); 115 for (String log : logs) { 116 if (log.contains(SM_UNABLE_TO_CONNECT)) { 117 unableToConnect = true; 118 } else if (log.contains(SM_RUNNING)) { 119 unableToConnect = false; 120 } 121 } 122 return unableToConnect; 123 } 124 125 private boolean waitForStorageManager() { 126 return waitForLogs(smName, SM_RUNNING, SM_RESET); 127 } 128 129 private boolean waitForAdminProcess() { 130 return waitForLogs(config.containerName(), AD_RUNNING, AD_RESET); 131 } 132 133 private boolean waitTime(long millis) { 134 try { 135 Thread.sleep(millis); 136 } catch (InterruptedException e) { 137 Thread.currentThread().interrupt(); 138 e.printStackTrace(); 139 } 140 return true; 141 } 142 143 private boolean waitForLogs(String containerName, String match, String resetMatch) { 144 for (int i = 0; i < 150; i++) { 145 if (logsContain(containerName, match, resetMatch)) { 146 return true; 147 } 148 try { 149 int sleep = (i < 10) ? 10 : (i < 20) ? 20 : 100; 150 Thread.sleep(sleep); 151 } catch (InterruptedException e) { 152 Thread.currentThread().interrupt(); 153 return false; 154 } 155 } 156 return false; 157 } 158 159 @Override 160 void startContainer() { 161 if (!isArchivePopulated()) { 162 removeContainersAndRun(); 163 164 } else { 165 commands.start(adName); 166 if (!waitForAdminProcess() || !waitForDatabaseState()) { 167 throw new RuntimeException("Failed waiting for NuoDB admin container [" + smName + "] to start running"); 168 } else { 169 if (!startStorageManager(0)) { 170 throw new RuntimeException("Failed to start storage manager NuoDB [" + adName + "]"); 171 } else { 172 commands.start(teName); 173 if (!waitForTransactionManager()) { 174 throw new RuntimeException("Failed waiting for NuoDB transaction manager [" + smName + "] to start running"); 175 } 176 } 177 } 178 } 179 } 180 181 private void removeContainersAndRun() { 182 log.info("Archive directory is empty, remove containers and run"); 183 commands.removeContainers(teName, smName, adName); 184 runContainer(); 185 } 186 187 private boolean waitForDatabaseState() { 188 waitTime(100); 189 for (int i = 0; i < 20; i++) { 190 if (checkDbStateOk()) { 191 return true; 192 } else { 193 waitTime(100); 194 } 195 } 196 return false; 197 } 198 199 private boolean checkDbStateOk() { 200 //$ nuocmd show database --db-format 'dbState:{state}' --db-name testdb 201 List<String> args = new ArrayList<>(); 202 args.add(config.docker); 203 args.add("exec"); 204 args.add("-i"); 205 args.add(adName); 206 args.add("nuocmd"); 207 args.add("show"); 208 args.add("database"); 209 args.add("--db-format"); 210 args.add("dbState:{state}"); 211 args.add("--db-name"); 212 args.add(dbConfig.getDbName()); 213 214 try { 215 final ProcessResult result = process(createProcessBuilder(args)); 216 if (result.success()) { 217 for (String outLine : result.getOutLines()) { 218 final String trimmedOut = outLine.trim(); 219 if (trimmedOut.startsWith("dbState:")) { 220 return dbStateOk(trimmedOut); 221 } 222 } 223 } 224 } catch (CommandException e) { 225 return false; 226 } 227 return false; 228 } 229 230 private boolean dbStateOk(String trimmedOut) { 231 log.trace("checking dbStateOk [{}]", trimmedOut); 232 return trimmedOut.contains("NOT_RUNNING") || trimmedOut.contains("RUNNING"); 233 } 234 235 private boolean startStorageManager(int attempt) { 236 commands.start(smName); 237 if (!waitForStorageManager()) { 238 log.error("Failed waiting for NuoDB storage manager [" + adName + "] to start running"); 239 return false; 240 } 241 if (storageManagerUnableToConnect()) { 242 log.info("Retry NuoDB storage manager [" + adName + "] attempt:" + attempt); 243 return attempt <= 2 && startStorageManager(attempt + 1); 244 } 245 return true; 246 } 247 248 private void createNetwork() { 249 if (!networkExists()) { 250 process(procNetworkCreate()); 251 } 252 } 253 254 private boolean networkExists() { 255 return execute(network, procNetworkList()); 256 } 257 258 private ProcessBuilder procNetworkCreate() { 259 260 List<String> args = new ArrayList<>(); 261 args.add(config.docker); 262 args.add("network"); 263 args.add("create"); 264 args.add(network); 265 return createProcessBuilder(args); 266 } 267 268 private ProcessBuilder procNetworkRemove() { 269 270 List<String> args = new ArrayList<>(); 271 args.add(config.docker); 272 args.add("network"); 273 args.add("rm"); 274 args.add(network); 275 return createProcessBuilder(args); 276 } 277 278 private ProcessBuilder procNetworkList() { 279 280 List<String> args = new ArrayList<>(); 281 args.add(config.docker); 282 args.add("network"); 283 args.add("ls"); 284 args.add("-f"); 285 args.add("name=" + network); 286 return createProcessBuilder(args); 287 } 288 289 @Override 290 protected ProcessBuilder runProcess() { 291 throw new RuntimeException("Not used for NuoDB container"); 292 } 293 294 private ProcessBuilder runAdminProcess() { 295 296 List<String> args = new ArrayList<>(); 297 args.add(config.docker); 298 args.add("run"); 299 args.add("-d"); 300 args.add("--name"); 301 args.add(adName); 302 args.add("--hostname"); 303 args.add(adName); 304 args.add("--net"); 305 args.add(network); 306 args.add("-p"); 307 args.add(config.getPort() + ":" + config.getInternalPort()); 308 args.add("-p"); 309 args.add(nuoConfig.getPort2() + ":" + nuoConfig.getInternalPort2()); 310 args.add("-p"); 311 args.add(nuoConfig.getPort3() + ":" + nuoConfig.getInternalPort3()); 312 313 if (defined(dbConfig.getAdminPassword())) { 314 args.add("-e"); 315 args.add("NUODB_DOMAIN_ENTRYPOINT=" + adName); 316 } 317 args.add(config.getImage()); 318 args.add("nuoadmin"); 319 return createProcessBuilder(args); 320 } 321 322 private ProcessBuilder runStorageManager() { 323 324 // volumes for backup and archive not added yet 325 // as generally we are application testing with this 326 327 final Path archiveDir = archivePath(); 328 329 List<String> args = new ArrayList<>(); 330 args.add(config.docker); 331 args.add("run"); 332 args.add("-d"); 333 args.add("--name"); 334 args.add(smName); 335 args.add("--hostname"); 336 args.add(smName); 337 args.add("--volume"); 338 args.add(archiveDir.toAbsolutePath().toString() + ":/var/opt/nuodb/archive"); 339 args.add("--net"); 340 args.add(network); 341 args.add(config.getImage()); 342 args.add("nuodocker"); 343 args.add("--api-server"); 344 args.add(adName + ":" + config.getPort()); 345 args.add("start"); 346 args.add("sm"); 347 args.add("--db-name"); 348 args.add(dbConfig.getDbName()); 349 args.add("--server-id"); 350 args.add(adName); 351 args.add("--dba-user"); 352 args.add(dbConfig.getAdminUsername()); 353 args.add("--dba-password"); 354 args.add(dbConfig.getAdminPassword()); 355 args.add("--labels"); 356 args.add(nuoConfig.getLabels()); 357 args.add("--archive-dir"); 358 args.add("/var/opt/nuodb/archive"); 359 360 return createProcessBuilder(args); 361 } 362 363 boolean deleteDirectory(File dir) { 364 File[] allContents = dir.listFiles(); 365 if (allContents != null) { 366 for (File file : allContents) { 367 deleteDirectory(file); 368 } 369 } 370 return dir.delete(); 371 } 372 373 private Path archivePath() { 374 File nuoArchive = archiveFile(); 375 if (nuoArchive.exists()) { 376 log.info("delete " + nuoArchive.toPath()); 377 deleteDirectory(nuoArchive); 378 } else { 379 nuoArchive.setWritable(true, false); 380 if (!nuoArchive.mkdirs()) { 381 throw new RuntimeException("Failed to re-create " + nuoArchive.getAbsolutePath()); 382 } 383 } 384 return nuoArchive.toPath(); 385 } 386 387 private boolean isArchivePopulated() { 388 final File file = archiveFile(); 389 if (file.exists()) { 390 final File[] files = file.listFiles(); 391 return files != null && files.length > 0; 392 } 393 return false; 394 } 395 396 private File archiveFile() { 397 final File tmp = new File(System.getProperty("java.io.tmpdir")); 398 return new File(new File(tmp, "nuodb"), dbConfig.getDbName()); 399 } 400 401 private ProcessBuilder runTransactionManager() { 402 403 List<String> args = new ArrayList<>(); 404 args.add(config.docker); 405 args.add("run"); 406 args.add("-d"); 407 args.add("--name"); 408 args.add(teName); 409 args.add("--hostname"); 410 args.add(teName); 411 args.add("--net"); 412 args.add(network); 413 args.add(config.getImage()); 414 args.add("nuodocker"); 415 args.add("--api-server"); 416 args.add(adName + ":" + config.getPort()); 417 args.add("start"); 418 args.add("te"); 419 args.add("--db-name"); 420 args.add(dbConfig.getDbName()); 421 args.add("--server-id"); 422 args.add(adName); 423 424 return createProcessBuilder(args); 425 } 426 427 @Override 428 public boolean isDatabaseReady() { 429 return commands.logsContain(config.containerName(), "NuoAdmin Server running"); 430 } 431 432 @Override 433 protected boolean isDatabaseAdminReady() { 434 return true; 435 } 436 437 @Override 438 void createDatabase() { 439 createSchemaAndUser(false); 440 } 441 442 @Override 443 void dropCreateDatabase() { 444 createSchemaAndUser(true); 445 } 446 447 private void createSchemaAndUser(boolean withDrop) { 448 449 try (Connection connection = config.createAdminConnection()) { 450 451 if (withDrop) { 452 sqlDropSchema(connection, dbConfig.getSchema()); 453 } 454 455 final boolean schemaExists = sqlSchemaExists(connection, dbConfig.getSchema()); 456 if (!schemaExists) { 457 sqlCreateSchema(connection, dbConfig.getSchema()); 458 } 459 460 final boolean userExists = sqlUserExists(connection, dbConfig.getUsername()); 461 if (!userExists) { 462 sqlCreateUser(connection, dbConfig.getUsername(), dbConfig.getPassword()); 463 } 464 if (withDrop || !userExists) { 465 sqlUserGrants(connection, dbConfig.getSchema(), dbConfig.getUsername()); 466 } 467 connection.commit(); 468 469 } catch (SQLException e) { 470 throw new RuntimeException(e); 471 } 472 } 473 474 private void sqlDropSchema(Connection connection, String schema) throws SQLException { 475 exeSql(connection, "drop schema " + schema + " cascade if exists"); 476 } 477 478 private void sqlUserGrants(Connection connection, String schema, String username) throws SQLException { 479 exeSql(connection, "grant create on schema " + schema + " to " + username); 480 } 481 482 private void sqlCreateSchema(Connection connection, String schema) throws SQLException { 483 exeSql(connection, "create schema " + schema); 484 } 485 486 private void sqlCreateUser(Connection connection, String username, String password) throws SQLException { 487 exeSql(connection, "create user " + username + " password '" + password + "'"); 488 } 489 490 private boolean sqlSchemaExists(Connection connection, String schemaName) throws SQLException { 491 return sqlQueryMatch(connection, "select schema from system.schemas", schemaName); 492 } 493 494 private boolean sqlUserExists(Connection connection, String dbUser) throws SQLException { 495 return sqlQueryMatch(connection, "select username from system.users", dbUser); 496 } 497 498 private void exeSql(Connection connection, String sql) throws SQLException { 499 log.debug("exeSql {}", sql); 500 try (PreparedStatement st = connection.prepareStatement(sql)) { 501 st.execute(); 502 } 503 } 504}