Martin Charlesworth
Martin Charlesworth

Reputation: 617

Updating Dropwizard config at runtime

Is it possible to have my app update the config settings at runtime? I can easily expose the settings I want in my UI but is there a way to allow the user to update settings and make them permanent ie save them to the config.yaml file? The only way I can see it to update the file by hand then restart the server which seems a bit limiting.

Upvotes: 8

Views: 8650

Answers (5)

Scott G
Scott G

Reputation: 855

Although this feature isn't supported out of the box by dropwizard, you're able to accomplish this fairly easy with the tools they give you.

Before I get started, note that this isn't a complete solution for the question asked as it doesn't persist the updated config values to the config.yml. However, this would be easy enough to implement yourself simply by writing to the config file from the application. If anyone would like to write this implementation feel free to open a PR on the example project I've linked below.

Code

Start off with a minimal config:

config.yml

myConfigValue: "hello"

And it's corresponding configuration file:

ExampleConfiguration.java

public class ExampleConfiguration extends Configuration {
    private String myConfigValue;

    public String getMyConfigValue() {
        return myConfigValue;
    }

    public void setMyConfigValue(String value) {
        myConfigValue = value;
    }
}

Then create a task which updates the config:

UpdateConfigTask.java

public class UpdateConfigTask extends Task {
    ExampleConfiguration config;

    public UpdateConfigTask(ExampleConfiguration config) {
        super("updateconfig");
        this.config = config;
    }

    @Override
    public void execute(Map<String, List<String>> parameters, PrintWriter output) {
        config.setMyConfigValue("goodbye");
    }
}

Also for demonstration purposes, create a resource which allows you to get the config value:

ConfigResource.java

@Path("/config")
public class ConfigResource {
    private final ExampleConfiguration config;

    public ConfigResource(ExampleConfiguration config) {
        this.config = config;
    }

    @GET
    public Response handleGet() {
        return Response.ok().entity(config.getMyConfigValue()).build();
    }
}

Finally wire everything up in your application:

ExampleApplication.java (exerpt)

environment.jersey().register(new ConfigResource(configuration));
environment.admin().addTask(new UpdateConfigTask(configuration));

Usage

Start up the application then run:

$ curl 'http://localhost:8080/config'
hello
$ curl -X POST 'http://localhost:8081/tasks/updateconfig'
$ curl 'http://localhost:8080/config'
goodbye

How it works

This works simply by passing the same reference to the constructor of ConfigResource.java and UpdateConfigTask.java. If you aren't familiar with the concept see here: Is Java "pass-by-reference" or "pass-by-value"?

The linked classes above are to a project I've created which demonstrates this as a complete solution. Here's a link to the project:

scottg489/dropwizard-runtime-config-example

Footnote: I haven't verified this works with the built in configuration. However, the dropwizard Configuration class which you need to extend for your own configuration does have various "setters" for internal configuration, but it may not be safe to update those outside of run().



Disclaimer: The project I've linked here was created by me.

Upvotes: 1

Tatiana B
Tatiana B

Reputation: 101

I made a task that reloads the main yaml file (it would be useful if something in the file changes). However, it is not reloading the environment. After researching this, Dropwizard uses a lot of final variables and it's quite hard to reload these on the go, without restarting the app.

class ReloadYAMLTask extends Task {
    private String yamlFileName;

    ReloadYAMLTask(String yamlFileName) {
        super("reloadYaml");
        this.yamlFileName = yamlFileName;
    }
 @Override
    public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
        if (yamlFileName != null) {
            ConfigurationFactoryFactory configurationFactoryFactory = new DefaultConfigurationFactoryFactory<ReportingServiceConfiguration>();
            ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
            Validator validator = validatorFactory.getValidator();
            ObjectMapper objectMapper = Jackson.newObjectMapper();
            final ConfigurationFactory<ServiceConfiguration> configurationFactory = configurationFactoryFactory.create(ServiceConfiguration.class, validator, objectMapper, "dw");
            File confFile = new File(yamlFileName);
            configurationFactory.build(new File(confFile.toURI()));
        }
    }
}

Upvotes: 2

Jay Khatwani
Jay Khatwani

Reputation: 409

You can change the configuration in the YAML and read it while your application is running. This will not however restart the server or change any server configurations. You will be able to read any changed custom configurations and use them. For example, you can change the logging level at runtime or reload other custom settings.

My solution -

Define a custom server command. You should use this command to start your application instead of the "server" command.

ArgsServerCommand.java

public class ArgsServerCommand<WC extends WebConfiguration> extends EnvironmentCommand<WC> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ArgsServerCommand.class);

    private final Class<WC> configurationClass;

    private  Namespace _namespace;

    public static String COMMAND_NAME = "args-server";

    public ArgsServerCommand(Application<WC> application) {
        super(application, "args-server", "Runs the Dropwizard application as an HTTP server specific to my settings");
        this.configurationClass = application.getConfigurationClass();
    }

    /*
     * Since we don't subclass ServerCommand, we need a concrete reference to the configuration
     * class.
     */
    @Override
    protected Class<WC> getConfigurationClass() {
        return configurationClass;
    }

    public Namespace getNamespace() {
        return _namespace;
    }

    @Override
    protected void run(Environment environment, Namespace namespace, WC configuration) throws Exception {
        _namespace = namespace;
        final Server server = configuration.getServerFactory().build(environment);
        try {
            server.addLifeCycleListener(new LifeCycleListener());
            cleanupAsynchronously();
            server.start();
        } catch (Exception e) {
            LOGGER.error("Unable to start server, shutting down", e);
            server.stop();
            cleanup();
            throw e;
        }
    }

    private class LifeCycleListener extends AbstractLifeCycle.AbstractLifeCycleListener {
        @Override
        public void lifeCycleStopped(LifeCycle event) {
            cleanup();
        }
    }
}

Method to reload in your Application -

_ymlFilePath = null; //class variable 

  public static boolean reloadConfiguration() throws IOException, ConfigurationException {

        boolean reloaded = false;
        if (_ymlFilePath == null) {
            List<Command> commands = _configurationBootstrap.getCommands();
            for (Command command : commands) {
                String commandName = command.getName();
                if (commandName.equals(ArgsServerCommand.COMMAND_NAME)) {
                    Namespace namespace = ((ArgsServerCommand) command).getNamespace();
                    if (namespace != null) {
                        _ymlFilePath = namespace.getString("file");
                    }
                }
            }
        }

        ConfigurationFactoryFactory configurationFactoryFactory = _configurationBootstrap.getConfigurationFactoryFactory();
        ValidatorFactory validatorFactory = _configurationBootstrap.getValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        ObjectMapper objectMapper = _configurationBootstrap.getObjectMapper();
        ConfigurationSourceProvider provider = _configurationBootstrap.getConfigurationSourceProvider();

        final ConfigurationFactory<CustomWebConfiguration> configurationFactory = configurationFactoryFactory.create(CustomWebConfiguration.class, validator, objectMapper, "dw");

        if (_ymlFilePath != null) {

            // Refresh logging level.
            CustomWebConfiguration webConfiguration = configurationFactory.build(provider, _ymlFilePath);
            LoggingFactory loggingFactory = webConfiguration.getLoggingFactory();
            loggingFactory.configure(_configurationBootstrap.getMetricRegistry(), _configurationBootstrap.getApplication().getName());

            // Get my defined custom settings
            CustomSettings customSettings = webConfiguration.getCustomSettings();
            reloaded = true;
        }
        return reloaded;
    }

Upvotes: 1

Jay Khatwani
Jay Khatwani

Reputation: 409

Yes. It is possible to reload the service classes at runtime.

Dropwizard by itself does not have the way to reload the app, but jersey has.

Jersey uses a container object internally to maintain the running application. Dropwizard uses the ServletContainer class of Jersey to run the application.

How to reload the app without restarting it -

  1. Get a handle to the container used internally by jersey

    You can do this by registering a AbstractContainerLifeCycleListener in Dropwizard Environment before starting the app. and implement its onStartup method as below -

In your main method where you start the app -

//getting the container instance
        environment.jersey().register(new AbstractContainerLifecycleListener()  {
        @Override
        public void onStartup(Container container) {
            //initializing container - which will be used to reload the app
            _container = container;
        }

    });  
  1. Add a method to your app to reload the app. It will take in the list of string which are the names of the service classes you want to reload. This method will call the reload method of the container with the new custom DropWizardConfiguration instance.

In your Application class

 public static synchronized void reloadApp(List<String> reloadClasses) {
        DropwizardResourceConfig dropwizardResourceConfig = new DropwizardResourceConfig();

        for (String className : reloadClasses) {
           try {
                Class<?> serviceClass = Class.forName(className);
                dropwizardResourceConfig.registerClasses(serviceClass);
                System.out.printf(" + loaded class %s.\n", className);
            } catch (ClassNotFoundException ex) {
                System.out.printf(" ! class %s not found.\n", className);
            }
        }
        _container.reload(dropwizardResourceConfig);

    }  

For more details see the example documentation of jersey - jersey example for reload

Consider going through the code and documentation of following files in Dropwizard/Jersey for a better understanding -

Container.java

ContainerLifeCycleListener.java

ServletContainer.java

AbstractContainerLifeCycleListener.java

DropWizardResourceConfig.java

ResourceConfig.java

Upvotes: 9

Natan
Natan

Reputation: 2858

No.

Yaml file is parsed at startup and given to the application as Configuration object once and for all. I believe you can change the file after that but it wouldn't affect your application until you restart it.

Possible follow up question: Can one restart the service programmatically?

AFAIK, no. I've researched and read the code somewhat for that but couldn't find a way to do that yet. If there is, I'd love to hear that :).

Upvotes: 3

Related Questions