From 0f80d9ca8fee9bc6076fb8cb79a5611e96147dcf Mon Sep 17 00:00:00 2001 From: Subodh Date: Sat, 19 Sep 2026 18:23:50 +0530 Subject: [PATCH] systemd node supervisor and makefile and small patches --- beedb-gateway/pom.xml | 14 ++ .../memcache/gateway/client/BeedbConfig.java | 19 +++ .../supervisor/ProcessNodeSupervisor.java | 24 ++- .../supervisor/SystemdNodeSupervisor.java | 97 ++++++++++++ .../src/main/resources/application.properties | 6 + beedb-server/Makefile | 148 ++++++++++++++++++ beedb-server/pom.xml | 43 +++++ .../src/main/java/com/memcache/Server.java | 84 ++++++++-- pom.xml | 18 ++- 9 files changed, 431 insertions(+), 22 deletions(-) create mode 100644 beedb-gateway/src/main/java/com/memcache/gateway/supervisor/SystemdNodeSupervisor.java create mode 100644 beedb-server/Makefile diff --git a/beedb-gateway/pom.xml b/beedb-gateway/pom.xml index c85a896..cd9ab7a 100644 --- a/beedb-gateway/pom.xml +++ b/beedb-gateway/pom.xml @@ -15,6 +15,20 @@ beedb-gateway beedb-gateway + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + pom + import + + + + diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/client/BeedbConfig.java b/beedb-gateway/src/main/java/com/memcache/gateway/client/BeedbConfig.java index 1a23815..ed73c95 100644 --- a/beedb-gateway/src/main/java/com/memcache/gateway/client/BeedbConfig.java +++ b/beedb-gateway/src/main/java/com/memcache/gateway/client/BeedbConfig.java @@ -13,6 +13,7 @@ import com.memcache.gateway.kv.WriteRateLimiter; import com.memcache.gateway.supervisor.NodeSupervisor; import com.memcache.gateway.supervisor.ProcessNodeSupervisor; +import com.memcache.gateway.supervisor.SystemdNodeSupervisor; import ch.qos.logback.core.pattern.parser.Node; @@ -76,6 +77,12 @@ public static class BeedbProperties { private int restartAfterSeconds = 15; + /** "process" on a laptop (nodes started by make), "systemd" on a server. */ + private String supervisor = "process"; + + /** Only read when supervisor=systemd. %s is the node id. */ + private String systemdUnitPattern = "beedb-node@%s.service"; + public int getKeyTtlSeconds() { return keyTtlSeconds; } public void setKeyTtlSeconds(int keyTtlSeconds) { this.keyTtlSeconds = keyTtlSeconds; } @@ -97,6 +104,10 @@ public static class BeedbProperties { public void setSupervisorServerDir(String supervisorServerDir) { this.supervisorServerDir = supervisorServerDir; } public int getRestartAfterSeconds() { return restartAfterSeconds; } public void setRestartAfterSeconds(int restartAfterSeconds) { this.restartAfterSeconds = restartAfterSeconds; } + public String getSupervisor() { return supervisor; } + public void setSupervisor(String supervisor) { this.supervisor = supervisor; } + public String getSystemdUnitPattern() { return systemdUnitPattern; } + public void setSystemdUnitPattern(String systemdUnitPattern) { this.systemdUnitPattern = systemdUnitPattern; } public Map getNodes() { return nodes; @@ -150,6 +161,14 @@ public NodeSupervisor nodeSupervisor(){ Set nodeIds = beedbProperties.getNodes().keySet(); String supervisorServerDir = beedbProperties.getSupervisorServerDir(); int restartAfterSeconds = beedbProperties.getRestartAfterSeconds(); + String supervisor = beedbProperties.getSupervisor(); + if ("systemd".equalsIgnoreCase(supervisor)) { + return new SystemdNodeSupervisor(Set.copyOf(nodeIds), + beedbProperties.getSystemdUnitPattern(), restartAfterSeconds); + } + if (!"process".equalsIgnoreCase(supervisor)) { + throw new IllegalStateException("beedb.supervisor must be 'process' or 'systemd', got: " + supervisor); + } return new ProcessNodeSupervisor(Set.copyOf(nodeIds), Path.of(supervisorServerDir), restartAfterSeconds); } diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/ProcessNodeSupervisor.java b/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/ProcessNodeSupervisor.java index 2d97782..09de759 100644 --- a/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/ProcessNodeSupervisor.java +++ b/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/ProcessNodeSupervisor.java @@ -1,6 +1,7 @@ package com.memcache.gateway.supervisor; import java.io.IOException; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -37,7 +38,7 @@ public ProcessNodeSupervisor(Set nodeIds, Path serverDirectory, int rest @Override public void kill(String nodeId) throws NodeCouldNotBeKilled { if(nodeId == null || !nodeIds.contains(nodeId)) throw new NodeCouldNotBeKilled("node "+nodeId+" is not in the set of nodes to kill"); - String pattern = "Dnode\\.id="+nodeId+" .*com\\.memcache\\.Server"; + String pattern = "Dnode\\.id="+nodeId+" -jar .*beedb-server.*\\.jar"; try { Process process = new ProcessBuilder(List.of("pkill", "-f", pattern)).redirectErrorStream(true).start(); if(!process.waitFor(PKILL_TIMEOUT_SECONDS, TimeUnit.SECONDS)){ @@ -68,7 +69,7 @@ public void kill(String nodeId) throws NodeCouldNotBeKilled { } private void chaosRestore(String nodeId) throws IOException{ - String cp = Files.readString(serverDirectory.resolve("target/classpath.txt")).trim(); + Path jar = serverJar(); // stdout is discarded: logback's CONSOLE appender writes every log line there, and // logs/.log already has them, rotated. stderr goes to a file instead it // stays empty unless the JVM dies of something only stderr sees, such as an @@ -77,12 +78,27 @@ private void chaosRestore(String nodeId) throws IOException{ Files.createDirectories(err.getParent()); new ProcessBuilder(List.of( "setsid", "java", "-Dnode.id=" + nodeId, - "-cp", "target/classes:" + cp, - "com.memcache.Server", nodeId + ".properties")) + "-jar", jar.toString(), nodeId + ".properties")) .directory(serverDirectory.toFile()) .redirectOutput(ProcessBuilder.Redirect.DISCARD) .redirectError(ProcessBuilder.Redirect.appendTo(err.toFile())) .start(); } + + /** + * the shaded jar in the server module's target/. found by glob rather than by a + * hard-coded version, so a version bump in the pom does not silently stop chaos + * from restoring nodes. + */ + private Path serverJar() throws IOException { + Path target = serverDirectory.resolve("target"); + try (DirectoryStream jars = Files.newDirectoryStream(target, "beedb-server-*.jar")) { + for (Path jar : jars) { + return jar; + } + } + throw new IOException("no beedb-server jar in " + target.toAbsolutePath() + + " -- run `make package` in the server module first"); + } } diff --git a/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/SystemdNodeSupervisor.java b/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/SystemdNodeSupervisor.java new file mode 100644 index 0000000..b74976c --- /dev/null +++ b/beedb-gateway/src/main/java/com/memcache/gateway/supervisor/SystemdNodeSupervisor.java @@ -0,0 +1,97 @@ +package com.memcache.gateway.supervisor; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.memcache.gateway.chaos.chaosexception.NodeCouldNotBeKilled; + +/** + * when we have nodes on real server there we need systemd to stop and start them + * we can't user {@link ProcessNodeSupervisor} because pkill sends SIGTERM, the + * JVM exits 143, and the unit declares 143 a clean stop , so systemd would + * consider the node deliberately stopped, while this process later started a + * replacement outside systemd, in the wrong working directory, that systemd + * knows nothing about. + * + * going through systemctl keeps one owner of the process. `stop` is an explicit + * request, so `Restart=on-failure` does not fire, the node stays down until we + * start it again, which is exactly what a chaos demo wants. + */ +public class SystemdNodeSupervisor implements NodeSupervisor { + + private static final Logger LOGGER = LoggerFactory.getLogger(SystemdNodeSupervisor.class); + private static final int SYSTEMCTL_TIMEOUT_SECONDS = 10; + + private final Set nodeIds; + private final String unitPattern; + private final int restartAfterSeconds; + + private final ThreadFactory chaosRestoreThreadFactory = runnable -> { + Thread thread = new Thread(runnable, "chaos-restore"); + thread.setDaemon(true); + return thread; + }; + private final ScheduledExecutorService chaosRestorer = + Executors.newScheduledThreadPool(1, chaosRestoreThreadFactory); + + public SystemdNodeSupervisor(Set nodeIds, String unitPattern, int restartAfterSeconds) { + this.nodeIds = nodeIds; + this.unitPattern = unitPattern; + this.restartAfterSeconds = restartAfterSeconds; + } + + @Override + public void kill(String nodeId) throws NodeCouldNotBeKilled { + // the node id becomes part of a command run as root, so it is checked + // against the configured set and never taken on trust. The sudoers rule + // names each unit in full as a second line of defence. + if (nodeId == null || !nodeIds.contains(nodeId)) { + throw new NodeCouldNotBeKilled("node " + nodeId + " is not in the set of nodes to kill"); + } + systemctl("stop", nodeId); + LOGGER.info("chaos stopped {}, restoring in {}s", nodeId, restartAfterSeconds); + + // the timer lives in this JVM: if the gateway restarts inside the + // window, the node stays stopped until someone starts it + chaosRestorer.schedule(() -> { + try { + systemctl("start", nodeId); + LOGGER.info("chaos restored {}", nodeId); + } catch (Exception e) { + LOGGER.error("failed to restore node {}", nodeId, e); + } + }, restartAfterSeconds, TimeUnit.SECONDS); + } + + private void systemctl(String verb, String nodeId) throws NodeCouldNotBeKilled { + String unit = String.format(unitPattern, nodeId); + List command = List.of("sudo", "-n", "systemctl", verb, unit); + try { + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + if (!process.waitFor(SYSTEMCTL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + process.destroyForcibly(); + throw new NodeCouldNotBeKilled("systemctl " + verb + " " + unit + " timed out"); + } + int exitCode = process.exitValue(); + if (exitCode != 0) { + // the sudoers rule is missing, or it does not + // cover this unit name. Both are silent until a kill is tried. + throw new NodeCouldNotBeKilled("systemctl " + verb + " " + unit + + " exited " + exitCode + ": " + new String(process.getInputStream().readAllBytes())); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new NodeCouldNotBeKilled("systemctl " + verb + " " + unit + " was interrupted", e); + } catch (IOException e) { + throw new NodeCouldNotBeKilled("systemctl " + verb + " " + unit + " failed: " + e.getMessage(), e); + } + } +} diff --git a/beedb-gateway/src/main/resources/application.properties b/beedb-gateway/src/main/resources/application.properties index 4113253..629eae3 100644 --- a/beedb-gateway/src/main/resources/application.properties +++ b/beedb-gateway/src/main/resources/application.properties @@ -42,3 +42,9 @@ beedb.chaos-startup-grace-seconds=60 beedb.supervisor-server-dir=../beedb-server beedb.restart-after-seconds=15 + +# process = pkill the JVM this gateway started (in case of laptop, `make up`) +# systemd = sudo systemctl stop/start the unit (in case of server) +# The server value belongs in /etc/beedb/gateway.properties +beedb.supervisor=process +beedb.systemd-unit-pattern=beedb-node@%s.service diff --git a/beedb-server/Makefile b/beedb-server/Makefile new file mode 100644 index 0000000..869cd78 --- /dev/null +++ b/beedb-server/Makefile @@ -0,0 +1,148 @@ +.PHONY: compile test classpath package run1 run2 run3 up down restart status tail logclean clean + +MVN := mvn -q +LOG_DIR := logs +CP_FILE := target/classpath.txt +MAIN := com.memcache.Server +# the same self-contained jar that gets deployed: running it here means local +# and server start the node exactly the same way, dependencies included. +JAR := target/beedb-server-1.0-SNAPSHOT.jar + +# logback.xml reads -Dnode.id to name its rolling file appender, so each node +# writes its own logs/.log with rotation and a totalSizeCap. +# stdout is sent to /dev/null: logback owns the file now, and a shell redirect +# writing to the same path would fight the rolling policy. +JAVA_RUN = java -Dnode.id=$(1) -jar $(JAR) $(1).properties + +# wipe one node's logs so every start writes into an empty file: the live file, +# the rotated .gz archives, and any .tmp left behind by an interrupted roll. +CLEAN_LOGS = rm -f $(LOG_DIR)/$(1).log $(LOG_DIR)/$(1)-metrics.log \ + $(LOG_DIR)/$(1).*.log.gz $(LOG_DIR)/$(1)-metrics.*.log.gz \ + $(LOG_DIR)/$(1).*.tmp $(LOG_DIR)/$(1)-metrics.*.tmp + +# find a node's live JVMs by command line rather than by pid file. The pid file +# is not authoritative: `up` overwrites it, so a node left running by an earlier +# up/run* becomes untrackable and survives every restart still holding its +# ports. Matching -Dnode.id= finds those orphans too, and cannot be fooled +# by a recycled pid the way a stale pid file can. +NODE_PIDS = pgrep -f "Dnode\.id=$(1) -jar .*beedb-server.*\.jar" + +# both ports a node binds, read from its .properties so this can never drift +# from the real config. +NODE_PORTS = grep -hE '^(clientPort|raftPort)=' $(1).properties | cut -d= -f2 | tr -d ' \r' + +# ports currently in LISTEN state, one per line, IPv4/IPv6 prefix stripped. +LISTENING = ss -ltnH 2>/dev/null | awk '{print $$4}' | sed 's/.*://' + +compile: + $(MVN) clean compile + +test: + $(MVN) test + +# resolve the dependency classpath once so `up`/`run*` don't pay Maven's +# per-invocation bootstrap cost for every node. +classpath: compile + $(MVN) dependency:build-classpath -Dmdep.outputFile=$(CP_FILE) + +# build the shaded jar. tests are skipped here on purpose: this is the inner +# loop, `mvn verify` is where tests belong. +package: + $(MVN) -DskipTests package + +run1: package + @mkdir -p $(LOG_DIR) + @$(call CLEAN_LOGS,node1) + $(call JAVA_RUN,node1) + +run2: package + @mkdir -p $(LOG_DIR) + @$(call CLEAN_LOGS,node2) + $(call JAVA_RUN,node2) + +run3: package + @mkdir -p $(LOG_DIR) + @$(call CLEAN_LOGS,node3) + $(call JAVA_RUN,node3) + +# start all 3 nodes as background processes. logs are written by logback +# (logs/node.log); only pids land here directly. +up: package + @mkdir -p $(LOG_DIR) + @busy=""; \ + listening=$$($(LISTENING)); \ + for n in 1 2 3; do \ + for p in $$($(call NODE_PORTS,node$$n)); do \ + if echo "$$listening" | grep -qx "$$p"; then \ + owner=$$(ss -lptnH "sport = :$$p" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1); \ + echo "node$$n: port $$p already bound ($$owner)"; \ + busy="$$busy node$$n"; \ + fi; \ + done; \ + done; \ + if [ -n "$$busy" ]; then \ + echo "refusing to start -- run 'make down' first"; \ + exit 1; \ + fi + @for n in 1 2 3; do \ + $(call CLEAN_LOGS,node$$n); \ + nohup java -Dnode.id=node$$n -jar $(JAR) node$$n.properties \ + > /dev/null 2>&1 & echo $$! > $(LOG_DIR)/node$$n.pid; \ + done + @sleep 1 + @echo "Cluster up. node1=11211 node2=11212 node3=11213 (client ports)" + @$(MAKE) --no-print-directory status + +down: + @for n in 1 2 3; do \ + rm -f $(LOG_DIR)/node$$n.pid; \ + pids=$$($(call NODE_PIDS,node$$n) || true); \ + if [ -z "$$pids" ]; then echo "node$$n: nothing running"; continue; fi; \ + for pid in $$pids; do kill $$pid 2>/dev/null || true; done; \ + i=0; \ + while [ $$i -lt 20 ]; do \ + alive=$$($(call NODE_PIDS,node$$n) || true); \ + [ -z "$$alive" ] && break; \ + sleep 0.5; i=$$((i+1)); \ + done; \ + for pid in $$($(call NODE_PIDS,node$$n) || true); do \ + echo "node$$n: pid $$pid ignored SIGTERM, sending SIGKILL"; \ + kill -9 $$pid 2>/dev/null || true; \ + done; \ + echo "stopped node$$n (pid $$(echo $$pids))"; \ + done + +restart: down up + +status: + @for n in 1 2 3; do \ + pids=$$($(call NODE_PIDS,node$$n) || true); \ + if [ -z "$$pids" ]; then echo "node$$n: DOWN"; else \ + echo "node$$n: RUNNING (pid $$(echo $$pids))"; \ + fi; \ + done + +# Wipe all three nodes' logs without starting anything. +logclean: + @mkdir -p $(LOG_DIR) + @for n in 1 2 3; do $(call CLEAN_LOGS,node$$n); done + @echo "logs cleared for node1 node2 node3" + +tail: + tail -f $(LOG_DIR)/node1.log $(LOG_DIR)/node2.log $(LOG_DIR)/node3.log + +# dropping rotated archives too, not just the live logs. +clean: down + $(MVN) clean + rm -rf $(LOG_DIR) + +# run the framing probe against a live cluster. +# +# the probe is a test class, so it still runs from target/test-classes with the +# dependency classpath -- only the nodes moved to the jar. +# +# make up && make probe +probe: + $(MVN) test-compile + $(MVN) dependency:build-classpath -Dmdep.outputFile=$(CP_FILE) + java -cp "target/classes:target/test-classes:$$(cat $(CP_FILE))" com.memcache.FramingProbe diff --git a/beedb-server/pom.xml b/beedb-server/pom.xml index 9376fbe..65d8532 100644 --- a/beedb-server/pom.xml +++ b/beedb-server/pom.xml @@ -49,6 +49,49 @@ + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + *:* + + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + module-info.class + + + + + + com.memcache.Server + + + + + + + + diff --git a/beedb-server/src/main/java/com/memcache/Server.java b/beedb-server/src/main/java/com/memcache/Server.java index 1c5a805..9bdf9d5 100644 --- a/beedb-server/src/main/java/com/memcache/Server.java +++ b/beedb-server/src/main/java/com/memcache/Server.java @@ -7,6 +7,7 @@ package com.memcache; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; @@ -58,6 +59,9 @@ public class Server implements AutoCloseable{ private static final long MAX_RELATIVE_EXPIRY_SECONDS = 30L * 24 * 60 * 60; public static void main(String[] args) throws IOException{ // read file name + if(args.length == 0){ + configError("usage: java -jar beedb-server.jar .properties"); + } String fileName = args[0]; // read properties file Properties properties = new Properties(); @@ -66,17 +70,18 @@ public static void main(String[] args) throws IOException{ Server server = new Server(); ){ properties.load(fileInputStream); - - ArrayList peers = new ArrayList<>(); - for(String peer : properties.getProperty("peers").split(",")){ - peers.add(peer); - } + + ArrayList peers = parsePeers(properties.getProperty("peers")); // if properties values are absent then we use default values Runtime.getRuntime().addShutdownHook(new Thread(server::close)); String nodeId = properties.getProperty("nodeId", "node1"); - int clientPort = Integer.parseInt(properties.getProperty("clientPort")); - int raftPort = Integer.parseInt(properties.getProperty("raftPort")); + int clientPort = requiredPort(properties, "clientPort"); + int raftPort = requiredPort(properties, "raftPort"); + if(clientPort == raftPort){ + throw new IllegalArgumentException("clientPort and raftPort are both " + clientPort + + "; a node cannot serve clients and raft on one port"); + } String stateDir = properties.getProperty("stateDir" , "state"); String snapshotDir = properties.getProperty("snapshotDir" , "snapshot"); String tmpDir = properties.getProperty("tmpDir" , "tmp"); @@ -94,10 +99,13 @@ public static void main(String[] args) throws IOException{ LOGGER.debug("clientPort is {} and raftPort is {} and peers is {} ", clientPort , raftPort, peers); LOGGER.debug("stateDir is {} and snapshotDir is {} and tmpDir is {} and walDir is {} ", stateDir , snapshotDir, tmpDir, walDir); LOGGER.debug("snapShotLimit is {} and snapShotThreshold is {} and minElectionTimeout is {} and maxElectionTimeout is {} and heartbeatInterval is {} ", snapShotLimit, snapShotThreshold, minElectionTimeout, maxElectionTimeout, heartbeatInterval); - if(clientPort == 0 || raftPort == 0 || peers.size() == 0){ - throw new IllegalArgumentException("clientPort and raftPort must be provided"); - } server.start(peers, nodeId , clientPort , raftPort, stateDir, snapshotDir, tmpDir, walDir, snapShotLimit, snapShotThreshold, minElectionTimeout, maxElectionTimeout, heartbeatInterval, peerRetryBackoffInitialMs, peerRetryBackoffMaxMs); + }catch(IllegalArgumentException e){ + // every config problem lands here: the node never starts, so it can + // never sit in the cluster listening but unable to take part. + configError(e.getMessage()); + }catch(FileNotFoundException e){ + configError("config file not found: " + fileName); }catch(Exception e){ LOGGER.error("something is wrong {}",e); System.exit(1); @@ -105,6 +113,62 @@ public static void main(String[] args) throws IOException{ } + /** + * peers is "host:port,host:port" + */ + private static ArrayList parsePeers(String raw) { + if (raw == null || raw.isBlank()) { + throw new IllegalArgumentException( + "peers is required, e.g. peers=127.0.0.1:12222,127.0.0.1:12223"); + } + ArrayList peers = new ArrayList<>(); + for (String entry : raw.split(",")) { + String peer = entry.trim(); + if (peer.isEmpty()) { + throw new IllegalArgumentException("peers has an empty entry: '" + raw + "'"); + } + // lastIndexOf, not indexOf: an IPv6 literal has colons of its own + int colon = peer.lastIndexOf(':'); + if (colon <= 0 || colon == peer.length() - 1) { + throw new IllegalArgumentException("peer '" + peer + "' must be host:port"); + } + parsePort(peer.substring(colon + 1), "port of peer '" + peer + "'"); + if (peers.contains(peer)) { + throw new IllegalArgumentException("peers lists '" + peer + "' twice"); + } + peers.add(peer); + } + return peers; + } + + /** A port this node binds. Must be present, numeric and a real port number. */ + private static int requiredPort(Properties properties, String key) { + String raw = properties.getProperty(key); + if (raw == null) { + throw new IllegalArgumentException("property '" + key + "' is required"); + } + return parsePort(raw, key); + } + + private static int parsePort(String raw, String what) { + int value; + try { + value = Integer.parseInt(raw.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(what + " is not a number: '" + raw.trim() + "'"); + } + if (value < 1 || value > 65535) { + throw new IllegalArgumentException(what + " is out of range: " + value); + } + return value; + } + + private static void configError(String message) { + LOGGER.error("bad configuration: {}", message); + System.err.println("bad configuration: " + message); + System.exit(2); + } + private static int intProperty(Properties properties, String key, int defaultValue) { String raw = properties.getProperty(key); if (raw == null) { diff --git a/pom.xml b/pom.xml index 3369f57..0d50dc4 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,9 @@ 2.17.2 2.0.13 - 1.5.6 + + 1.5.8 4.13.2 3.3.4 @@ -44,19 +46,19 @@ logback-classic ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + junit junit ${junit.version} test - - org.springframework.boot - spring-boot-starter-parent - ${spring.boot.version} - pom - import -