mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-02-24 05:18:58 +00:00
(node) Nodes auto-configure on start-up instead of requiring manual configuration.
This commit is contained in:
parent
c98117f69d
commit
2df3e0f881
@ -17,29 +17,22 @@ public class NodeConfigurationService {
|
|||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NodeConfiguration create(String description, boolean acceptQueries) throws SQLException {
|
public NodeConfiguration create(int id, String description, boolean acceptQueries) throws SQLException {
|
||||||
try (var conn = dataSource.getConnection();
|
try (var conn = dataSource.getConnection();
|
||||||
var is = conn.prepareStatement("""
|
var is = conn.prepareStatement("""
|
||||||
INSERT INTO NODE_CONFIGURATION(DESCRIPTION, ACCEPT_QUERIES) VALUES(?, ?)
|
INSERT INTO NODE_CONFIGURATION(ID, DESCRIPTION, ACCEPT_QUERIES) VALUES(?, ?, ?)
|
||||||
""");
|
""")
|
||||||
var qs = conn.prepareStatement("""
|
)
|
||||||
SELECT LAST_INSERT_ID()
|
|
||||||
"""))
|
|
||||||
{
|
{
|
||||||
is.setString(1, description);
|
is.setInt(1, id);
|
||||||
is.setBoolean(2, acceptQueries);
|
is.setString(2, description);
|
||||||
|
is.setBoolean(3, acceptQueries);
|
||||||
|
|
||||||
if (is.executeUpdate() <= 0) {
|
if (is.executeUpdate() <= 0) {
|
||||||
throw new IllegalStateException("Failed to insert configuration");
|
throw new IllegalStateException("Failed to insert configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
var rs = qs.executeQuery();
|
return get(id);
|
||||||
|
|
||||||
if (rs.next()) {
|
|
||||||
return get(rs.getInt(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AssertionError("No LAST_INSERT_ID()");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +50,8 @@ public class NodeConfigurationServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() throws SQLException {
|
public void test() throws SQLException {
|
||||||
var a = nodeConfigurationService.create("Test", false);
|
var a = nodeConfigurationService.create(1, "Test", false);
|
||||||
var b = nodeConfigurationService.create("Foo", true);
|
var b = nodeConfigurationService.create(2, "Foo", true);
|
||||||
|
|
||||||
assertEquals(1, a.node());
|
assertEquals(1, a.node());
|
||||||
assertEquals("Test", a.description());
|
assertEquals("Test", a.description());
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
CREATE TABLE NODE_CONFIGURATION (
|
CREATE TABLE NODE_CONFIGURATION (
|
||||||
ID INT PRIMARY KEY AUTO_INCREMENT,
|
ID INT PRIMARY KEY,
|
||||||
DESCRIPTION VARCHAR(255),
|
DESCRIPTION VARCHAR(255),
|
||||||
ACCEPT_QUERIES BOOLEAN,
|
ACCEPT_QUERIES BOOLEAN,
|
||||||
DISABLED BOOLEAN DEFAULT FALSE
|
DISABLED BOOLEAN DEFAULT FALSE
|
||||||
|
@ -2,10 +2,14 @@ package nu.marginalia.service.server;
|
|||||||
|
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
import nu.marginalia.nodecfg.NodeConfigurationService;
|
import nu.marginalia.nodecfg.NodeConfigurationService;
|
||||||
|
import nu.marginalia.storage.FileStorageService;
|
||||||
|
import nu.marginalia.storage.model.FileStorageBaseType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -23,18 +27,22 @@ public class NodeStatusWatcher {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(NodeStatusWatcher.class);
|
private static final Logger logger = LoggerFactory.getLogger(NodeStatusWatcher.class);
|
||||||
|
|
||||||
private final NodeConfigurationService configurationService;
|
private final NodeConfigurationService configurationService;
|
||||||
|
private final FileStorageService fileStorageService;
|
||||||
private final int nodeId;
|
private final int nodeId;
|
||||||
|
|
||||||
private final Duration pollDuration = Duration.ofSeconds(15);
|
private final Duration pollDuration = Duration.ofSeconds(15);
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public NodeStatusWatcher(NodeConfigurationService configurationService,
|
public NodeStatusWatcher(NodeConfigurationService configurationService,
|
||||||
@Named("wmsa-system-node") Integer nodeId) throws InterruptedException {
|
FileStorageService fileStorageService, @Named("wmsa-system-node") Integer nodeId) {
|
||||||
this.configurationService = configurationService;
|
this.configurationService = configurationService;
|
||||||
|
this.fileStorageService = fileStorageService;
|
||||||
|
|
||||||
this.nodeId = nodeId;
|
this.nodeId = nodeId;
|
||||||
|
|
||||||
awaitConfiguration();
|
if (!isConfigured()) {
|
||||||
|
setupNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var watcherThread = new Thread(this::watcher, "node watcher");
|
var watcherThread = new Thread(this::watcher, "node watcher");
|
||||||
@ -42,29 +50,28 @@ public class NodeStatusWatcher {
|
|||||||
watcherThread.start();
|
watcherThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wait for the presence of an enabled NodeConfiguration before permitting the service to start */
|
private void setupNode() {
|
||||||
private void awaitConfiguration() throws InterruptedException {
|
try {
|
||||||
|
configurationService.create(nodeId, "Node " + nodeId, nodeId == 1);
|
||||||
boolean complained = false;
|
fileStorageService.createStorageBase("Index Data", Path.of("/idx"), nodeId, FileStorageBaseType.CURRENT);
|
||||||
|
fileStorageService.createStorageBase("Index Backups", Path.of("/backup"), nodeId, FileStorageBaseType.BACKUP);
|
||||||
for (;;) {
|
fileStorageService.createStorageBase("Crawl Data", Path.of("/storage"), nodeId, FileStorageBaseType.STORAGE);
|
||||||
try {
|
fileStorageService.createStorageBase("Work Area", Path.of("/work"), nodeId, FileStorageBaseType.WORK);
|
||||||
var config = configurationService.get(nodeId);
|
|
||||||
if (null != config && !config.disabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (!complained) {
|
|
||||||
logger.info("Waiting for node configuration, id = {}", nodeId);
|
|
||||||
complained = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (SQLException ex) {
|
|
||||||
logger.error("Error updating node status", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeUnit.SECONDS.sleep(pollDuration.toSeconds());
|
|
||||||
}
|
}
|
||||||
|
catch (IllegalStateException ex) {
|
||||||
|
// There is a slight chance of a race condition between the index and executor services both trying to run this,
|
||||||
|
// at the same time. Thanks to ACID, only one of them will succeed in creating the node, and the other will throw
|
||||||
|
// IllegalStateException. This is fine!
|
||||||
|
}
|
||||||
|
catch (SQLException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
private boolean isConfigured() {
|
||||||
|
var configuration = configurationService.get(nodeId);
|
||||||
|
return configuration != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Look for changes in the configuration and kill the service if the corresponding
|
/** Look for changes in the configuration and kill the service if the corresponding
|
||||||
|
@ -77,7 +77,6 @@ public class ControlNodeService {
|
|||||||
var newSpecsFormRenderer = rendererFactory.renderer("control/node/node-new-specs-form");
|
var newSpecsFormRenderer = rendererFactory.renderer("control/node/node-new-specs-form");
|
||||||
|
|
||||||
Spark.get("/public/nodes", this::nodeListModel, nodeListRenderer::render);
|
Spark.get("/public/nodes", this::nodeListModel, nodeListRenderer::render);
|
||||||
Spark.post("/public/nodes", this::createNode);
|
|
||||||
Spark.get("/public/nodes/:id", this::nodeOverviewModel, overviewRenderer::render);
|
Spark.get("/public/nodes/:id", this::nodeOverviewModel, overviewRenderer::render);
|
||||||
Spark.get("/public/nodes/:id/", this::nodeOverviewModel, overviewRenderer::render);
|
Spark.get("/public/nodes/:id/", this::nodeOverviewModel, overviewRenderer::render);
|
||||||
Spark.get("/public/nodes/:id/actors", this::nodeActorsModel, actorsRenderer::render);
|
Spark.get("/public/nodes/:id/actors", this::nodeActorsModel, actorsRenderer::render);
|
||||||
@ -106,18 +105,6 @@ public class ControlNodeService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object createNode(Request request, Response response) throws SQLException, FileNotFoundException {
|
|
||||||
var newConfig = nodeConfigurationService.create(request.queryParams("description"), "on".equalsIgnoreCase(request.queryParams("acceptQueries")));
|
|
||||||
int id = newConfig.node();
|
|
||||||
|
|
||||||
fileStorageService.createStorageBase("Index Data", Path.of("/idx"), id, FileStorageBaseType.CURRENT);
|
|
||||||
fileStorageService.createStorageBase("Index Backups", Path.of("/backup"), id, FileStorageBaseType.BACKUP);
|
|
||||||
fileStorageService.createStorageBase("Crawl Data", Path.of("/storage"), id, FileStorageBaseType.STORAGE);
|
|
||||||
fileStorageService.createStorageBase("Work Area", Path.of("/work"), id, FileStorageBaseType.WORK);
|
|
||||||
|
|
||||||
return redirectToOverview(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object nodeListModel(Request request, Response response) throws SQLException {
|
private Object nodeListModel(Request request, Response response) throws SQLException {
|
||||||
var configs = nodeConfigurationService.getAll();
|
var configs = nodeConfigurationService.getAll();
|
||||||
|
|
||||||
|
@ -27,34 +27,17 @@
|
|||||||
<td>{{acceptQueries}}</td>
|
<td>{{acceptQueries}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="m-5 p-5 border bg-light">
|
||||||
<div class="m-3 p-3 border">
|
<h2 class="my-3">Index Nodes</h2>
|
||||||
<h2>Add Node</h2>
|
<p>
|
||||||
<form method="post">
|
Index nodes are processing units. The search engine requires at least one, but more can be added
|
||||||
<div class="mb-3">
|
to spread the system load across multiple physical disks or even multiple servers.
|
||||||
<label for="name" class="form-label">ID</label>
|
</p>
|
||||||
<input class="form-control" type="text" name="id" id="id" value="{{nextNodeId}}" disabled aria-disabled="true" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="name" class="form-label">Description</label>
|
|
||||||
<input class="form-control" type="text" name="description" id="description" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
|
||||||
<input class="form-check-input" type="checkbox" role="switch" name="acceptQueries">
|
|
||||||
<label class="form-check-label" for="acceptQueries">Accept queries</label>
|
|
||||||
|
|
||||||
<div class="form-text">Sets whether queries will be routed to this node. This can be modified later.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Create</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</table>
|
{{/if}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
Loading…
Reference in New Issue
Block a user