(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.util.*;
import static nu.marginalia.mq.MqMessageState.NEW;
@Singleton
public class MqPersistence {
private final HikariDataSource dataSource;
@ -100,12 +102,18 @@ public class MqPersistence {
/** Modifies the state of a message by id */
public void updateMessageState(long id, MqMessageState mqMessageState) throws SQLException {
if (NEW == mqMessageState) {
reinitializeMessage(id);
return;
}
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement("""
UPDATE MESSAGE_QUEUE
SET STATE=?, UPDATED_TIME=CURRENT_TIMESTAMP(6)
WHERE ID=?
""")) {
stmt.setString(1, mqMessageState.name());
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
* 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=?
LIMIT ?
""")
) {
)
{
queryStmt.setString(1, inboxName);
queryStmt.setInt(2, n);
var rs = queryStmt.executeQuery();
@ -230,9 +259,44 @@ public class MqPersistence {
}
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,
* 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.FileStorageType;
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.service.server.*;
import org.slf4j.Logger;
@ -43,7 +45,8 @@ public class ControlService extends Service {
ControlActorService controlActorService,
StaticResources staticResources,
MessageQueueViewService messageQueueViewService,
ControlFileStorageService controlFileStorageService
ControlFileStorageService controlFileStorageService,
MqPersistence persistence
) throws IOException {
super(params);
@ -60,7 +63,9 @@ public class ControlService extends Service {
var storageSpecsRenderer = rendererFactory.renderer("control/storage-specs");
var storageCrawlsRenderer = rendererFactory.renderer("control/storage-crawls");
var storageProcessedRenderer = rendererFactory.renderer("control/storage-processed");
var storageDetailsRenderer = rendererFactory.renderer("control/storage-details");
var updateMessageStateRenderer = rendererFactory.renderer("control/dialog-update-message-state");
this.controlActorService = controlActorService;
@ -102,6 +107,14 @@ public class ControlService extends Service {
Spark.post("/public/storage/specs", controlActorService::createCrawlSpecification, 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);
monitors.subscribe(this::logMonitorStateChange);

View File

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

View File

@ -116,11 +116,9 @@ public class ControlFileStorageService {
try (var filesStream = Files.list(storage.asPath())) {
filesStream
.filter(Files::isRegularFile)
.map(this::createFileModel)
.sorted(Comparator
.comparing(FileStorageFileModel::type)
.thenComparing(FileStorageFileModel::filename)
)
.sorted(Comparator.comparing(FileStorageFileModel::filename))
.forEach(files::add);
}
catch (IOException ex) {
@ -132,7 +130,7 @@ public class ControlFileStorageService {
private FileStorageFileModel createFileModel(Path p) {
try {
String type = Files.isRegularFile(p) ? "file" : "directory";
String mTime = Files.getLastModifiedTime(p).toInstant().toString();
String size;
if (Files.isDirectory(p)) {
size = "-";
@ -146,7 +144,7 @@ public class ControlFileStorageService {
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) {
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>
{{#each messages}}
<tr>
<td>{{stateCode}}&nbsp;{{state}}</td>
<td><a onClick="editMessage({{id}})" href="#">{{id}}</a></td>
<td>{{stateCode}}&nbsp;<a onClick="updateMsgState({{id}})" href="/message/{{id}}/state">{{state}}</a></td>
<td>{{id}}</td>
<td>{{recipientInbox}}</td>
<td>{{function}}</td>
<td title="{{ownerInstanceFull}}">
@ -30,61 +30,3 @@
</tr>
{{/each}}
</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>
<tr>
<th>File Name</th>
<th>Type</th>
<th>Last Mod</th>
<th>Size</th>
</tr>
{{#each storage.files}}
<tr>
<td>
{{#if downloadable}}<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a></td>
{{else}} {{filename}} {{/if}}
<td>{{type}}</td>
<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a>
<td>{{mTime}}</td>
<td>{{size}}</td>
</tr>
{{/each}}