(control) Dialog for updating message state; clean up file view.

This commit is contained in:
Viktor Lofgren 2023-07-28 22:02:05 +02:00
parent 01476577b8
commit 866db6c63f
7 changed files with 134 additions and 84 deletions

View File

@ -12,6 +12,8 @@ import java.sql.SQLException;
import java.time.Duration; import java.time.Duration;
import java.util.*; import java.util.*;
import static nu.marginalia.mq.MqMessageState.NEW;
@Singleton @Singleton
public class MqPersistence { public class MqPersistence {
private final HikariDataSource dataSource; private final HikariDataSource dataSource;
@ -100,12 +102,18 @@ public class MqPersistence {
/** Modifies the state of a message by id */ /** Modifies the state of a message by id */
public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException { public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException {
if (NEW == mqMessageState) {
reinitializeMessage(id);
return;
}
try (var conn = dataSource.getConnection(); try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement(""" var stmt = conn.prepareStatement("""
UPDATE MESSAGE_QUEUE UPDATE MESSAGE_QUEUE
SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6) SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6)
WHERE ID=? WHERE ID=?
""")) { """)) {
stmt.setString(1, mqMessageState.name()); stmt.setString(1, mqMessageState.name());
stmt.setLong(2, id); stmt.setLong(2, id);
@ -115,6 +123,26 @@ public class MqPersistence {
} }
} }
/** Sets the message to 'NEW' state and removes any owner */
public void reinitializeMessage(long id) throws SQLException {
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement("""
UPDATE MESSAGE_QUEUE
SET STATE='NEW',
OWNER_INSTANCE=NULL,
OWNER_TICK=NULL,
UPDATED_TIME=CURRENT_TIMESTAMP(6)
WHERE ID=?
""")) {
stmt.setLong(1, id);
if (stmt.executeUpdate() != 1) {
throw new IllegalArgumentException("No rows updated");
}
}
}
/** Creates a new message in the queue referencing as a reply to an existing message /** Creates a new message in the queue referencing as a reply to an existing message
* This message will have it's RELATED_ID set to the original message's ID. * This message will have it's RELATED_ID set to the original message's ID.
*/ */
@ -207,7 +235,8 @@ public class MqPersistence {
AND RECIPIENT_INBOX=? AND RECIPIENT_INBOX=?
LIMIT ? LIMIT ?
""") """)
) { )
{
queryStmt.setString(1, inboxName); queryStmt.setString(1, inboxName);
queryStmt.setInt(2, n); queryStmt.setInt(2, n);
var rs = queryStmt.executeQuery(); var rs = queryStmt.executeQuery();
@ -232,7 +261,42 @@ public class MqPersistence {
return messages; return messages;
} }
} }
public MqMessage getMessage(long id) throws SQLException {
try (var conn = dataSource.getConnection();
var queryStmt = conn.prepareStatement("""
SELECT
ID,
RELATED_ID,
FUNCTION,
PAYLOAD,
STATE,
SENDER_INBOX IS NOT NULL AS EXPECTS_RESPONSE
FROM MESSAGE_QUEUE
WHERE ID=?
""")
)
{
queryStmt.setLong(1, id);
var rs = queryStmt.executeQuery();
if (rs.next()) {
long msgId = rs.getLong("ID");
long relatedId = rs.getLong("RELATED_ID");
String function = rs.getString("FUNCTION");
String payload = rs.getString("PAYLOAD");
MqMessageState state = MqMessageState.valueOf(rs.getString("STATE"));
boolean expectsResponse = rs.getBoolean("EXPECTS_RESPONSE");
return new MqMessage(msgId, relatedId, function, payload, state, expectsResponse);
}
}
throw new IllegalArgumentException("No message with id " + id);
}
/** Marks unclaimed messages addressed to this inbox with instanceUUID and tick, /** Marks unclaimed messages addressed to this inbox with instanceUUID and tick,
* then returns these messages. * then returns these messages.
*/ */
@ -378,4 +442,5 @@ public class MqPersistence {
} }
} }
} }

View File

@ -8,6 +8,8 @@ import nu.marginalia.control.svc.*;
import nu.marginalia.db.storage.model.FileStorageId; import nu.marginalia.db.storage.model.FileStorageId;
import nu.marginalia.db.storage.model.FileStorageType; import nu.marginalia.db.storage.model.FileStorageType;
import nu.marginalia.model.gson.GsonFactory; import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.mq.MqMessageState;
import nu.marginalia.mq.persistence.MqPersistence;
import nu.marginalia.renderer.RendererFactory; import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.service.server.*; import nu.marginalia.service.server.*;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -43,7 +45,8 @@ public class ControlService extends Service {
ControlActorService controlActorService, ControlActorService controlActorService,
StaticResources staticResources, StaticResources staticResources,
MessageQueueViewService messageQueueViewService, MessageQueueViewService messageQueueViewService,
ControlFileStorageService controlFileStorageService ControlFileStorageService controlFileStorageService,
MqPersistence persistence
) throws IOException { ) throws IOException {
super(params); super(params);
@ -60,7 +63,9 @@ public class ControlService extends Service {
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs"); var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls"); var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed"); var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
var storageDetailsRenderer = rendererFactory.renderer("control/storage-details"); var storageDetailsRenderer = rendererFactory.renderer("control/storage-details");
var updateMessageStateRenderer = rendererFactory.renderer("control/dialog-update-message-state");
this.controlActorService = controlActorService; this.controlActorService = controlActorService;
@ -102,6 +107,14 @@ public class ControlService extends Service {
Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, redirectToStorage); Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, redirectToStorage);
Spark.post("/public/storage/:fid/delete", controlFileStorageService::flagFileForDeletionRequest, redirectToStorage); Spark.post("/public/storage/:fid/delete", controlFileStorageService::flagFileForDeletionRequest, redirectToStorage);
Spark.get("/public/message/:id/state", (rq, rsp) -> persistence.getMessage(Long.parseLong(rq.params("id"))), updateMessageStateRenderer::render);
Spark.post("/public/message/:id/state", (rq, rsp) -> {
MqMessageState state = MqMessageState.valueOf(rq.queryParams("state"));
long id = Long.parseLong(rq.params("id"));
persistence.updateMessageState(id, state);
return "";
}, redirectToProcesses);
Spark.get("/public/:resource", this::serveStatic); Spark.get("/public/:resource", this::serveStatic);
monitors.subscribe(this::logMonitorStateChange); monitors.subscribe(this::logMonitorStateChange);

View File

@ -1,15 +1,7 @@
package nu.marginalia.control.model; package nu.marginalia.control.model;
import nu.marginalia.db.storage.model.FileStorage;
import java.util.List;
public record FileStorageFileModel(String filename, public record FileStorageFileModel(String filename,
String type, String mTime,
String size String size)
) { {
public boolean isDownloadable() {
return type.equals("file");
}
} }

View File

@ -116,11 +116,9 @@ public class ControlFileStorageService {
try (var filesStream = Files.list(storage.asPath())) { try (var filesStream = Files.list(storage.asPath())) {
filesStream filesStream
.filter(Files::isRegularFile)
.map(this::createFileModel) .map(this::createFileModel)
.sorted(Comparator .sorted(Comparator.comparing(FileStorageFileModel::filename))
.comparing(FileStorageFileModel::type)
.thenComparing(FileStorageFileModel::filename)
)
.forEach(files::add); .forEach(files::add);
} }
catch (IOException ex) { catch (IOException ex) {
@ -132,7 +130,7 @@ public class ControlFileStorageService {
private FileStorageFileModel createFileModel(Path p) { private FileStorageFileModel createFileModel(Path p) {
try { try {
String type = Files.isRegularFile(p) ? "file" : "directory"; String mTime = Files.getLastModifiedTime(p).toInstant().toString();
String size; String size;
if (Files.isDirectory(p)) { if (Files.isDirectory(p)) {
size = "-"; size = "-";
@ -146,7 +144,7 @@ public class ControlFileStorageService {
else size = sizeBytes / (1024 * 1024 * 1024) + " GB"; else size = sizeBytes / (1024 * 1024 * 1024) + " GB";
} }
return new FileStorageFileModel(p.toFile().getName(), type, size); return new FileStorageFileModel(p.toFile().getName(), mTime, size);
} }
catch (IOException ex) { catch (IOException ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);

View File

@ -0,0 +1,41 @@
<!doctype html>
<html>
<link rel="stylesheet" href="/style.css" />
<head><title>Update ID</title></head>
<body>
{{> control/partials/nav}}
<section>
<h1>Update Message State</h1>
<p>Update the of a message in the message queue. This may be useful to prevent an actor
from resuming an action when this is not desirable. Setting an old message to 'NEW' will
erase information about its owner, and inboxes will consider the message new again.</p>
<form method="post" action="/message/{{msgId}}/state">
<label for="msgId">msgId</label><br>
<input type="text" disabled id="msgId" name="msgId" value="{{msgId}}">
<br><br>
<label for="relatedId">relatedId</label><br>
<input type="text" disabled id="relatedId" name="relatedId" value="{{relatedId}}">
<br><br>
<label for="function">function</label><br>
<input type="text" disabled id="function" name="function" value="{{function}}">
<br><br>
<label for="payload">payload</label><br>
<input type="text" disabled id="payload" name="payload" value="{{payload}}">
<br><br>
<label for="oldState">current state</label><br>
<input type="text" disabled id="oldState" name="oldState" value="{{state}}">
<br>
<br>
<label for="state">new state</label><br>
<select id="state" name="state">
<option value="NEW">NEW</option>
<option value="ACK">ACK</option>
<option value="OK">OK</option>
<option value="ERR">ERR</option>
<option value="DEAD">DEAD</option>
</select>
<input type="submit" value="Update">
</form>
</section>
</html>

View File

@ -11,8 +11,8 @@
</tr> </tr>
{{#each messages}} {{#each messages}}
<tr> <tr>
<td>{{stateCode}}&nbsp;{{state}}</td> <td>{{stateCode}}&nbsp;<a onClick="updateMsgState({{id}})" href="/message/{{id}}/state">{{state}}</a></td>
<td><a onClick="editMessage({{id}})" href="#">{{id}}</a></td> <td>{{id}}</td>
<td>{{recipientInbox}}</td> <td>{{recipientInbox}}</td>
<td>{{function}}</td> <td>{{function}}</td>
<td title="{{ownerInstanceFull}}"> <td title="{{ownerInstanceFull}}">
@ -30,61 +30,3 @@
</tr> </tr>
{{/each}} {{/each}}
</table> </table>
<dialog id="edit-message">
<h1>Edit Message</h1>
<form method="post" action="/message/:id/edit">
<div class="form-grid">
<label for="id">ID</label> <input readonly type="text" name="id" id="id" pattern="\d+" value="12" />
<label for="relatedId">Related ID</label> <input type="text" name="relatedId" id="relatedId" pattern="\d+" />
<label for="state">State</label>
<select name="state" id="state">
<option value="NEW">NEW</option>
<option value="ACK">ACK</option>
<option value="OK">OK</option>
<option value="ERR">ERR</option>
<option value="DEAD">DEAD</option>
</select>
<label for="sender">Sender</label> <input type="text" name="sender" id="sender" />
<label for="recipient">Recipient</label> <input type="text" name="recipient" id="recipient" />
<label for="function">Function</label> <input type="text" name="function" id="function" />
<label for="payload">Payload</label> <input type="text" name="payload" id="payload" />
<label for="ttl">TTL</label> <input type="text" name="ttl" id="ttl" />
<label for="ownerInstance">Owner Instance</label> <input type="text" name="ownerInstance" id="ownerInstance" />
<label for="ownerTick" pattern="\d+">Owner Tick</label> <input type="text" name="ownerTick" id="ownerTick" />
<div>
<input type="submit" value="Save" style="float:left" />
</div>
<div>
<input type="button" value="Cancel" style="float:right" onClick="document.getElementById('edit-message').close()"/>
</div>
</div>
</form>
</dialog>
<script>
function editMessage(id) {
var message = document.getElementById('edit-message');
var xhr = new XMLHttpRequest();
xhr.open('GET', '/messages/' + id, true);
xhr.onload = function () {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
message.querySelector('#edit-message #id').value = data.id;
message.querySelector('#edit-message #relatedId').value = data.relatedId;
message.querySelector('#edit-message #state').value = data.state;
message.querySelector('#edit-message #sender').value = data.sender;
message.querySelector('#edit-message #recipient').value = data.recipient;
message.querySelector('#edit-message #function').value = data.function;
message.querySelector('#edit-message #payload').value = data.payload;
message.querySelector('#edit-message #ttl').value = data.ttl;
message.querySelector('#edit-message #ownerInstance').value = data.ownerInstance;
message.querySelector('#edit-message #ownerTick').value = data.ownerTick;
message.showModal();
}
};
xhr.send();
}
</script>

View File

@ -30,15 +30,14 @@
<table> <table>
<tr> <tr>
<th>File Name</th> <th>File Name</th>
<th>Type</th> <th>Last Mod</th>
<th>Size</th> <th>Size</th>
</tr> </tr>
{{#each storage.files}} {{#each storage.files}}
<tr> <tr>
<td> <td>
{{#if downloadable}}<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a></td> <a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a>
{{else}} {{filename}} {{/if}} <td>{{mTime}}</td>
<td>{{type}}</td>
<td>{{size}}</td> <td>{{size}}</td>
</tr> </tr>
{{/each}} {{/each}}