Skip to content

Graceful shutdown - defaults for io.ebean.Database to shutdown() last #820

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

Merged
merged 1 commit into from
May 22, 2025
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
15 changes: 15 additions & 0 deletions blackbox-test-inject/src/main/java/io/ebean/Database.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.ebean;

/**
* Simulate the Ebean io.ebean.Database interface with a shutdown() method.
* <p>
* For "Graceful Shutdown" we desire this to be shutdown last.
*/
public interface Database {

/** Shutdown including underlying DataSources. We want this to occur LAST. */
void shutdown();

/** Test only method */
boolean isShutdown();
}
16 changes: 16 additions & 0 deletions blackbox-test-inject/src/main/java/io/ebean/MyDatabase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.ebean;

public class MyDatabase implements Database {

public boolean shutdownCalled = false;

@Override
public void shutdown() {
shutdownCalled = true;
}

@Override
public boolean isShutdown() {
return shutdownCalled;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.example.myapp.config;

import io.avaje.inject.*;
import io.ebean.Database;
import io.ebean.MyDatabase;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.example.myapp.HelloData;
Expand All @@ -14,6 +16,12 @@ public class AppConfig {

public static AtomicBoolean BEAN_AUTO_CLOSED = new AtomicBoolean();

/** Because this is a io.ebean.Database, we know it should be shutdown() last */
@Bean // Effectively default to @Bean(destroyMethod = "shutdown", destroyPriority = Integer.MAX_VALUE)
Database database() {
return new MyDatabase();
}

@PreDestroy(priority = 999)
void close() {
MyDestroyOrder.add("AppConfig");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.example.myapp;

import io.avaje.inject.BeanScope;
import io.ebean.Database;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class DefaultDestroyTest {

@Test
void expect_ebeanDatabase_hasShutdownByDefault() {
try (BeanScope beanScope = BeanScope.builder().build()) {
Database database = beanScope.get(Database.class);

assertThat(database.isShutdown()).isFalse();
beanScope.close();

assertThat(database.isShutdown()).isTrue();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;

import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY;
import static io.avaje.inject.generator.ProcessingContext.asElement;
Expand All @@ -15,6 +12,13 @@ final class MethodReader {

private static final String CODE_COMMENT_BUILD_FACTORYBEAN = " /**\n * Create and register %s via factory bean method %s#%s().\n */";

private static final Map<String,String> DEFAULT_DESTROY_METHODS = Map.of(
"io.ebean.Database", "shutdown"
);
private static final Map<String,Integer> DEFAULT_DESTROY_PRIORITY = Map.of(
"io.ebean.Database", Integer.MAX_VALUE
);

private final TypeExtendsInjection factory;
private final ExecutableElement element;
private final String factoryType;
Expand Down Expand Up @@ -85,8 +89,8 @@ final class MethodReader {
this.factoryShortName = Util.shortName(factoryType);
this.isVoid = Util.isVoid(mainType);
String initMethod = bean == null ? null : bean.initMethod();
String destroyMethod = bean == null ? null : bean.destroyMethod();
this.destroyPriority = bean == null ? null : bean.destroyPriority();
String destroyMethod = bean == null ? null : defaultDestroyMethod(bean.destroyMethod());
this.destroyPriority = bean == null ? null : defaultDestroyPriority(bean.destroyPriority());
this.beanCloseable = bean != null && bean.autoCloseable();
// for multiRegister we desire a qualifier name such that builder.isBeanAbsent() uses it and allows
// other beans of the same type to also register, otherwise it defaults to slightly confusing behaviour
Expand All @@ -113,6 +117,20 @@ final class MethodReader {
}
}

private String defaultDestroyMethod(String destroyMethod) {
if (!destroyMethod.isEmpty()) {
return destroyMethod;
}
return DEFAULT_DESTROY_METHODS.get(returnTypeRaw);
}

private Integer defaultDestroyPriority(Integer destroyPriority) {
if (destroyPriority != null && destroyPriority != 1000) {
return destroyPriority;
}
return DEFAULT_DESTROY_PRIORITY.get(returnTypeRaw);
}

@Override
public String toString() {
return "MethodReader{" +
Expand Down