paint-brush
How to Change Parameters of a Server at Runtimeby@timurnav
167 reads

How to Change Parameters of a Server at Runtime

by Timur MukhitdinovMarch 13th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

An ability to change the parameters of a server in runtime, without recompiling and restarting the server is very useful. Different companies name these parameters differently, but I prefer to name them Business Configuration. There are many options for implementing the task of distributing the configuration, I want to share one of the options I implemented - using [MongoDB] and local caching.
featured image - How to Change Parameters of a Server at Runtime
Timur Mukhitdinov HackerNoon profile picture


An ability to change the parameters of a server in runtime, without recompiling and restarting the server is very useful. These parameters could be thresholds of various values, coefficients of formulas, queue size, and so on -- everything that must remain unchanged between releases, but, if necessary, be modified at any moment. Different companies name these parameters differently, but I prefer to name them Business Configuration.


There are many options for implementing the task of distributing the configuration, I want to share one of the options I implemented - using MongoDB and local caching.

Details of the system

There is an admin panel server, which we want to manage the business configuration.

The servers which require the configs will have a shared database with the admin panel, there are plenty of them.

Applications have 2 databases - Postgres and MongoDB.


We chose MongoDB to store the configuration, because of the dynamic document schema. We decided to store all the types of configuration in a single table, but with different keys, so each record has its own schema and fields.


Shared database between Admin Panel and API-servers


API servers require the configuration at the startup, so to ensure it's already provided we need to force-set an initial value if it's not there at the server's startup. We decided not to add any notifications on updates from Admin Panel, but let the API Servers poll the configs and cache it locally.


To edit the configuration we decided to keep it simple. So Admin panel will have CRUD operations on configs using JSON representation. And to keep it safe we need to validate the provided json against the existing schema of the configs.


Implementation

API

The first thing we need is the API of the provider, it should encapsulate the fetching, deserializing, and caching:

public interface ConfigProvider<T> {
    T getConfig();
}


In the implementation, in fact, we access the database and extract a field from the fetched entity. The field we consider to be an independent config.

Mapping the config-entity to the config object

We know the name of the field in the MongoDB document. It's mapped to a field of the entity in the java class, a getter of which we can call to get the config value.


It’s convenient to make the code generalized, so to access the getter I decided to use LambdaMetafactory.

    public static <E, T> Function<E, T> provideGetter(String fieldName, Class<T> targetType, Class<E> sourceType) {
        String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MethodType type = MethodType.methodType(targetType);
            MethodHandle virtual = lookup.findVirtual(sourceType, getterName, type);
            CallSite callSite = LambdaMetafactory.metafactory(lookup,
                    "apply",
                    MethodType.methodType(Function.class),
                    MethodType.methodType(Object.class, Object.class),
                    virtual, MethodType.methodType(targetType, sourceType)
            );
            return (Function<E, T>) callSite.getTarget().invokeExact();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }


In the method above, we convert the getter to a Function that will be used when fetching the config from the entity.


There are two advantages of LambdaMetaFactory:

  1. For the validation of existing methods, if you specify a non-existent fieldName - the application will not start, this is very easy to check in tests.
  2. The performance, it's way faster than Java Reflection and Method Handles.


Making sure that the config is ready to use

As already told above, we need to ensure that all the expected data is stored in MongoDB.

To do this, each of the ConfigProviders must set the default value of its parameter.


The generalized config provider class looks like this:

public class ConfigProviderImpl<T, E extends ConfigEntity>
        implements ConfigProvider<T> {

    private final ConfigService<E> configService;
    private final String fieldName;
    private final Function<E, T> getter;
    private final T defaultValue;

    @SuppressWarnings("unchecked")
    public ConfigProviderImpl(ConfigService<E> configService,
                                 String fieldName, T defaultValue) {
        this.configService = configService;
        this.fieldName = fieldName;
        this.defaultValue = defaultValue;
        this.getter = provideGetter(fieldName,
                (Class<T>) defaultValue.getClass(),
                configService.getType());
    }

    public T getConfig() {
        return Optional.ofNullable(configService.getConfig())
                .map(getter)
                .orElse(defaultValue);
    }

    void ensureCreated() {
        configService.ensureCreated(fieldName, defaultValue);
    }
}


If, for some reason, the expected field is not set, the getConfig returns the default value.

The ensureCreated should be called at the application startup, so we can use a BeanPostProcessor for it.

public Object postProcessAfterInitialization(Object bean, String beanName)
        throws BeansException {
    if (bean instanceof ConfigProviderImpl) {
        ((ConfigProviderImpl<?, ?>) bean).ensureCreated();
    }
    return bean;
}


The only thing left is the implementation of the ConfigService#.ensureCreated(fieldName, defaultValue), so let’s jump to the implementation first.

public class ConfigService<E extends ConfigEntity> {
    public static final String COLLECTION_NAME = "app_config";

    private final MongoOperations mongoOperations;
    private final E initial;
...

    public void ensureCreated(String fieldName, Object value) {
        mongoOperations.upsert(
                Query.query(Criteria.where("_id").is(initial.getId())),
                Update.update("_id", initial.getId()),
                getType(), COLLECTION_NAME);

        Query query = Query.query(Criteria
                .where("_id").is(initial.getId())
                .and(fieldName).isNull());
        Update update = new Update();
        update.set(fieldName, value);
        mongoOperations.findAndModify(query, update, getType(), COLLECTION_NAME);
    }

    @SuppressWarnings("unchecked")
    public Class<E> getType() {
        return (Class<E>) initial.getClass();
    }
}


As we need to have the type of the entity and its identifier for fetching and creating the configs, we just pass the empty entity with a hardcoded id in it - initial. The rest is trivial: upsert is used to ensure the entity exists and then we set the field if it’s null, so we don’t override existing values.

Reading configs

Configs are often read and rarely changed so it’s worth caching them. I prefer Guava’s memoizeWithExpiration here as a handy API and with no overhead costs.


This is what it looks like:

private final Supplier<E> configEntitySupplier =
            memoizeWithExpiration(this::fetchConfig, 60, TimeUnit.SECONDS);
...
public E getConfig() {
    return configEntitySupplier.get();
}

private E fetchConfig() {
    Query query = Query.query(Criteria.where("_id").is(initial.getId()));
    return mongoOperations.findOne(query, getType(), COLLECTION_NAME);
}


The entity should implement interface ConfigEntity, which provides getId for ConfigService and has a hardcoded id.


For instance:

@Data
public class AppConfigEntity implements ConfigEntity {

   private String id = "app_config";
...


Composing things together

The code is generic, so to bind providers and services up we need to create the configuration:

@Bean
public ConfigProvider<ConfigA> appConfigAProvider(ConfigService<AppConfigEntity> appConfigService) {
   ConfigA defaultValue = new ConfigA(20, .7);
   return new ConfigProviderImpl<>(appConfigService, "configA", defaultValue);
}

@Bean
public ConfigService<AppConfigEntity> appConfigService(MongoOperations mongoOperations) {
   return new ConfigService<>(mongoOperations, new AppConfigEntity());
}
... // and so on for each config


And not to forget about bean of the BeanPostProcessor.

Now you can inject ConfigProvider<ConfigA> appConfigAProvider and access the config!


Conclusion

Using the solution described above is simple and effective for managing the business configuration of applications. It allows you to change the setup of the servers without any frameworks and additional services safely and easily. And also extend the features by creating new provider instances.


The source code is available on github.