Skip to content

Commit

Permalink
Merge pull request #38 from Toparvion/choices-auto-reload
Browse files Browse the repository at this point in the history
#33 Auto-reload for choices properties
  • Loading branch information
a-polyudov committed Dec 31, 2020
2 parents 2389015 + b669d56 commit 8759d63
Show file tree
Hide file tree
Showing 23 changed files with 664 additions and 28 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ out/
memo.txt
log-samples/
work/
/dist/
/dist/
log/
15 changes: 11 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.bmuschko.gradle.docker.tasks.image.Dockerfile
buildscript {
ext {
springBootVersion = '2.2.5.RELEASE'
springCloudVersion = "Hoxton.RELEASE"
}
repositories {
mavenCentral()
Expand Down Expand Up @@ -52,10 +53,13 @@ repositories {
dependencies {
// Backend compile deps
// compile("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
implementation platform (group: 'org.springframework.boot', name: 'spring-boot-dependencies', version: springBootVersion)
implementation platform(group: 'org.springframework.boot', name: 'spring-boot-dependencies', version: springBootVersion)
implementation platform(group: 'org.springframework.cloud', name: 'spring-cloud-dependencies', version: springCloudVersion)

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator'
implementation group: 'org.springframework.boot' , name: 'spring-boot-starter-websocket'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-websocket'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-context'
implementation group: 'net.bull.javamelody', name: 'javamelody-spring-boot-starter', version: '1.82.0'

implementation group: 'org.springframework.integration', name: 'spring-integration-file'
Expand Down Expand Up @@ -91,7 +95,10 @@ dependencies {

test {
useJUnitPlatform()

testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
}
}

jacocoTestReport {
Expand Down Expand Up @@ -158,7 +165,7 @@ task createDockerfile(type: Dockerfile) {
entryPoint 'java'
// the command at whole will look like 'java -X... -jar analog.jar'
defaultCommand '-Xmx256M', '-XX:MaxMetaspaceSize=256M', '-XX:+HeapDumpOnOutOfMemoryError', '--enable-preview',
'-jar', "${bootJar.archiveFileName.get()}"
'-jar', "${bootJar.archiveFileName.get()}"
}

task syncImageFiles(type: Sync) {
Expand Down
4 changes: 4 additions & 0 deletions config/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ nodes:
name: myself
agentPort: 7801

choicesSource:
location: ./config/choices.yaml
autoReloadEnabled: true

choices:
- group: AnaLog
plainLogs:
Expand Down
8 changes: 4 additions & 4 deletions config/choices.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
choices:
- group: Примеры
localPlainLogsBase: E:\Issues\Testing\incubator\analog\log-samples
- group: Examples
localPlainLogsBase: ${user.dir}\log-samples
plainLogs:
- path: generated\core.log
title: $f [генерируемый]
title: $f [generated]
- path: generated\micro.log
- path: source\core-source.log
- path: source\micro-source.log
selected: true
compositeLogs:
- title: 'Пробный композит'
- title: 'Test Composite'
uriName: 'test-composite'
includes:
- path: k8s:https://deploy/restorun-test-deployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.toparvion.analog.model.api.LogChoice;
import tech.toparvion.analog.service.LogChoicesProvider;
import tech.toparvion.analog.service.choice.LogChoicesProvider;

import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.toparvion.analog.model.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import tech.toparvion.analog.util.config.ChoiceValidator;

Expand All @@ -11,9 +12,10 @@
/**
* @author Toparvion
*/
@SuppressWarnings({"unused"}) // setters presence are required by Spring Boot
@Component
@RefreshScope
@ConfigurationProperties
@SuppressWarnings({"unused"}) // setters are required by Spring Boot
public class ChoiceProperties {
private List<ChoiceGroup> choices = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package tech.toparvion.analog.model.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import tech.toparvion.analog.service.choice.ConditionalOnChoicesAutoReloadEnabled;
import tech.toparvion.analog.util.config.ChoicesCustomConfigurationLoader;

/**
* @author Polyudov
* @since v0.14
*/
@Component
@SuppressWarnings("unused") // setters are required by Spring Boot
@ConfigurationProperties("choices-source")
public class ChoicesAutoReloadProperties {
/**
* Path to custom {@linkplain ChoiceProperties choices} location
*
* @see ChoicesCustomConfigurationLoader
*/
private String location;

/**
* This field {@linkplain ConditionalOnChoicesAutoReloadEnabled used} for enable/disable auto reloading
*/
private boolean autoReloadEnabled = false;

public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}

public boolean isAutoReloadEnabled() {
return autoReloadEnabled;
}

public void setAutoReloadEnabled(boolean autoReloadEnabled) {
this.autoReloadEnabled = autoReloadEnabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Date;
import java.util.Objects;

import static java.lang.String.format;

Expand Down Expand Up @@ -57,11 +58,7 @@ public class ProcessTailMessageProducer extends FileTailingMessageProducerSuppor


public void setOptions(String options) {
if (options == null) {
this.options = "";
} else {
this.options = options;
}
this.options = Objects.requireNonNullElse(options, "");
}

public void setExecutable(String executable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package tech.toparvion.analog.service.choice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import tech.toparvion.analog.model.config.ChoicesAutoReloadProperties;

import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchService;
import java.util.Objects;
import java.util.concurrent.Executor;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.nio.file.Files.isDirectory;
import static java.nio.file.Files.isReadable;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.util.Objects.requireNonNullElse;
import static java.util.concurrent.Executors.newSingleThreadExecutor;

/**
* Configuration for choices list auto reloading
*
* @author Polyudov
* @since v0.14
*/
@Configuration
@ConditionalOnChoicesAutoReloadEnabled
class ChoicesAutoReloadConfiguration {
private static final Logger log = LoggerFactory.getLogger(ChoicesAutoReloadConfiguration.class);
static final String CHOICES_AUTO_RELOAD_EXECUTOR = "choicesAutoReloadExecutor";

@Bean(CHOICES_AUTO_RELOAD_EXECUTOR)
Executor choicesAutoReloadExecutor() {
return newSingleThreadExecutor(new CustomizableThreadFactory("auto-reload-"));
}

@Bean
@Nullable
FileWatcherProvider fileWatcherProvider(ChoicesAutoReloadProperties choiceProperties) {
String choicesPropertiesLocation = choiceProperties.getLocation();
if (isNullOrEmpty(choicesPropertiesLocation)) {
log.info("Custom path for choices list is not present in 'choices-source.location' property. Auto reload logic won't be applied.");
return null;
}

try {
Path choicesPropertiesPath = Paths.get(choicesPropertiesLocation); //check exception

if (!isReadable(choicesPropertiesPath)) {
log.warn("File '{}' does not exist or there is no read access to this file", choicesPropertiesLocation);
return null;
}
if (isDirectory(choicesPropertiesPath)) {
log.warn("'choices-source.location' ('{}') is a directory. Please specify a path to a regular file to enable choices auto reloading.", choicesPropertiesLocation);
return null;
}
return new FileWatcherProvider(choicesPropertiesPath);
} catch (Exception e) {
log.warn("Can`t create choices list watcher", e);
return null;
}
}

static class FileWatcherProvider {
private final Path watchDir;
private final Path choicesPropertiesPath;
private final WatchService watchService;

FileWatcherProvider(Path choicesPropertiesPath) throws IOException {
this.choicesPropertiesPath = choicesPropertiesPath;
this.watchDir = fetchParent(choicesPropertiesPath);
this.watchService = FileSystems.getDefault().newWatchService();

watchDir.register(watchService, ENTRY_MODIFY);
}

public WatchService getWatchService() {
return watchService;
}

public Path getChoicesPropertiesPath() {
return choicesPropertiesPath;
}

public boolean isChoicesPropertiesFileEvent(Path eventPath) {
Path eventResolvedPath = watchDir.resolve(eventPath);
return Objects.equals(choicesPropertiesPath, eventResolvedPath);
}

private Path fetchParent(Path path) {
return requireNonNullElse(path.getParent(), path);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package tech.toparvion.analog.service.choice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import tech.toparvion.analog.service.choice.ChoicesAutoReloadConfiguration.FileWatcherProvider;

import javax.annotation.Nullable;
import java.nio.file.*;

import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
import static tech.toparvion.analog.service.choice.ChoicesAutoReloadConfiguration.CHOICES_AUTO_RELOAD_EXECUTOR;

/**
* @author Polyudov
* @since v0.14
*/
@Service
@EnableAsync
@ConditionalOnChoicesAutoReloadEnabled
class ChoicesPropertiesChangesListener {
private static final Logger log = LoggerFactory.getLogger(ChoicesPropertiesChangesListener.class);

private final RefreshEndpoint refreshEndpoint;
private final FileWatcherProvider fileWatcherProvider;

@Autowired
ChoicesPropertiesChangesListener(RefreshEndpoint refreshEndpoint,
@Nullable FileWatcherProvider fileWatcherProvider) {
this.refreshEndpoint = refreshEndpoint;
this.fileWatcherProvider = fileWatcherProvider;
}

@Async(CHOICES_AUTO_RELOAD_EXECUTOR)
@EventListener(ApplicationReadyEvent.class)
public void watchFile() {
if (fileWatcherProvider == null) {
return;
}
try {
log.info("Start watching for the choices properties file: '{}'", fileWatcherProvider.getChoicesPropertiesPath().toAbsolutePath());
WatchService watchService = fileWatcherProvider.getWatchService();
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
//Filter some special events and repeated events
if (OVERFLOW.equals(event.kind()) || event.count() > 1) {
continue;
}
Path path = (Path) event.context();
//Check modified file
if (fileWatcherProvider.isChoicesPropertiesFileEvent(path)) {
refreshEndpoint.refresh();
log.info("Choices properties file was reloaded");
}
}
boolean reset = key.reset();
if (!reset) {
log.error("Choices properties changes watching is now invalid");
throw new ClosedWatchServiceException();
}
}
} catch (ClosedWatchServiceException ignored) { //this exception throws during a normal application shutdown
} catch (Exception e) {
log.error("Stop watching for the choices properties file because of error:", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tech.toparvion.analog.service.choice;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* Custom {@link Conditional @Conditional} that checks if choices <code>auto reload</code> enabled in <code>properties</code>.<br/>
* This is some kind of "syntax sugar" which is designed to add this condition to all required places.
*
* @author Polyudov
* @since v0.14
*/
@Documented
@Target(TYPE)
@Retention(RUNTIME)
@SuppressWarnings("DefaultAnnotationParam")
@ConditionalOnProperty(name = "choices-source.auto-reload-enabled", havingValue = "true", matchIfMissing = false)
public @interface ConditionalOnChoicesAutoReloadEnabled {}
Loading

0 comments on commit 8759d63

Please sign in to comment.