/*
* Copyright 2013 David Tinker
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.qdb.server.controller;
import io.qdb.buffer.MessageBuffer;
import io.qdb.server.databind.DurationParser;
import io.qdb.server.model.*;
import io.qdb.server.model.Queue;
import io.qdb.server.queue.QueueManager;
import io.qdb.server.queue.QueueStatusMonitor;
import io.qdb.server.monitor.Status;
import io.qdb.server.repo.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
@Singleton
public class QueueController extends CrudController {
private final Repository repo;
private final MessageController messageController;
private final TimelineController timelineController;
private final OutputController outputController;
private final InputController inputController;
private final QueueManager queueManager;
private final QueueStatusMonitor queueStatusMonitor;
private static final Logger log = LoggerFactory.getLogger(QueueController.class);
public static class QueueDTO implements Comparable<QueueDTO> {
public String id;
public Integer version;
public String database;
public Long maxSize;
public Integer maxPayloadSize;
public String contentType;
public Object warnAfter;
public Object errorAfter;
public String status;
public Long size;
public Long messageCount;
public Object duration;
public Date oldestMessage;
public Date newestMessage;
public Object newestMessageReceived;
public Long oldestMessageId;
public Long nextMessageId;
@SuppressWarnings("UnusedDeclaration")
public QueueDTO() { }
public QueueDTO(String id, Queue queue, boolean borg) {
this.id = id;
this.version = queue.getVersion();
this.maxSize = queue.getMaxSize();
this.maxPayloadSize = queue.getMaxPayloadSize();
this.contentType = queue.getContentType();
if (borg) {
this.warnAfter = null0(queue.getWarnAfter());
this.errorAfter = null0(queue.getErrorAfter());
} else {
int secs = queue.getWarnAfter();
if (secs > 0) this.warnAfter = DurationParser.formatHumanMs(secs * 1000L);
secs = queue.getErrorAfter();
if (secs > 0) this.errorAfter = DurationParser.formatHumanMs(secs * 1000L);
}
}
private Integer null0(int x) {
return x == 0 ? null : x;
}
@Override
public int compareTo(QueueDTO o) {
return id.compareTo(o.id);
}
}
private static final Pattern VALID_QUEUE_ID = Pattern.compile("[0-9a-z\\-_]+", Pattern.CASE_INSENSITIVE);
@Inject
public QueueController(JsonService jsonService, Repository repo, MessageController messageController,
TimelineController timelineController, OutputController outputController,
InputController inputController, QueueManager queueManager, QueueStatusMonitor queueStatusMonitor) {
super(jsonService);
this.repo = repo;
this.messageController = messageController;
this.timelineController = timelineController;
this.outputController = outputController;
this.inputController = inputController;
this.queueManager = queueManager;
this.queueStatusMonitor = queueStatusMonitor;
}
@SuppressWarnings("unchecked")
@Override
protected void list(Call call, int offset, int limit) throws IOException {
List<QueueDTO> ans = new ArrayList<QueueDTO>();
Map<String, String> queues = call.getDatabase().getQueues();
if (queues != null) {
for (Map.Entry<String, String> e : queues.entrySet()) {
Queue queue = repo.findQueue(e.getValue());
if (queue != null) ans.add(createQueueDTO(call, e.getKey(), queue));
}
Collections.sort(ans);
int last = Math.min(offset + limit, ans.size());
if (offset > 0 || last < ans.size()) {
if (offset >= ans.size()) ans = Collections.EMPTY_LIST;
else ans = ans.subList(offset, last);
}
}
call.setJson(ans);
}
@Override
protected void count(Call call) throws IOException {
Map<String, String> queues = call.getDatabase().getQueues();
call.setJson(new Count(queues == null ? 0 : queues.size()));
}
@Override
protected void show(Call call, String id) throws IOException {
Map<String, String> queues = call.getDatabase().getQueues();
if (queues != null) {
String queueId = queues.get(id);
if (queueId != null) {
Queue queue = repo.findQueue(queueId);
if (queue != null) {
call.setJson(createQueueDTO(call, id, queue));
return;
}
}
}
call.setCode(404);
}
@SuppressWarnings("ConstantConditions")
protected QueueDTO createQueueDTO(Call call, String id, Queue queue) throws IOException {
boolean borg = call.getBoolean("borg");
QueueDTO dto = new QueueDTO(id, queue, borg);
try {
MessageBuffer mb = queueManager.getBuffer(queue);
if (mb != null) {
dto.size = mb.getSize();
dto.messageCount = mb.getMessageCount();
dto.oldestMessage = mb.getOldestTimestamp();
if (dto.oldestMessage != null) dto.oldestMessageId = mb.getOldestId();
dto.newestMessage = mb.getMostRecentTimestamp();
if (dto.newestMessage != null) {
long ms = System.currentTimeMillis() - dto.newestMessage.getTime();
dto.newestMessageReceived = borg ? ms : DurationParser.formatHumanMs(ms) + " ago";
ms = dto.newestMessage.getTime() - dto.oldestMessage.getTime();
dto.duration = borg ? ms : DurationParser.formatHumanMs(ms);
}
dto.nextMessageId = mb.getNextId();
}
Status status = queueStatusMonitor.getStatus(queue);
if (status != null) dto.status = status.toString();
} catch (IOException e) {
log.error("/db/" + queue.getDatabase() + "/q/" + id + ": " + e, e);
}
return dto;
}
@Override
protected void createOrUpdate(Call call, String id) throws IOException {
QueueDTO dto = getBodyObject(call, QueueDTO.class);
boolean create;
Queue q;
synchronized (repo) {
// re-lookup db inside sync block in case we need to update it
Database db = repo.findDatabase(call.getDatabase().getId());
if (db == null) { // this isn't likely but isn't impossible either
call.setCode(404);
return;
}
String qid = db.getQidForQueue(id);
if (create = qid == null) {
if (call.isPut()) {
call.setCode(404);
return;
}
if (!VALID_QUEUE_ID.matcher(id).matches()) {
call.setCode(422, "Queue id must contain only letters, numbers, hyphens and underscores");
return;
}
q = new Queue();
q.setDatabase(db.getId());
q.setMaxSize(100 * 1024 * 1024);
q.setMaxPayloadSize(1024 * 1024);
q.setContentType("application/octet-stream");
} else {
q = repo.findQueue(qid);
if (q == null) { // this shouldn't happen
String msg = "Queue /db/" + db.getId() + "/q/" + id + " qid [" + qid +
"] not found";
log.error(msg);
call.setCode(500, msg);
return;
}
if (dto.version != null && !dto.version.equals(q.getVersion())) {
call.setCode(409, createQueueDTO(call, id, q));
return;
}
q = q.deepCopy();
}
boolean changed = create;
if (dto.contentType != null && !dto.contentType.equals(q.getContentType())) {
q.setContentType(dto.contentType.length() > 0 ? dto.contentType : null);
changed = true;
}
if (dto.maxSize != null && dto.maxSize != q.getMaxSize()
|| dto.maxPayloadSize != null && dto.maxPayloadSize != q.getMaxPayloadSize()) {
long maxSize = q.getMaxSize();
int maxPayloadSize = q.getMaxPayloadSize();
if (dto.maxSize != null) {
maxSize = dto.maxSize;
if (maxSize < 1000000L) {
call.setCode(422, "maxSize must be at least 1000000 bytes");
return;
}
}
if (dto.maxPayloadSize != null) {
maxPayloadSize = dto.maxPayloadSize;
if (maxPayloadSize != 0) {
if (maxPayloadSize > maxSize / 3) {
call.setCode(422, "maxPayloadSize may not exceed 1/3 of maxSize (" + maxSize / 3 + ") bytes");
return;
}
if (maxPayloadSize < 1000) {
call.setCode(422, "maxPayloadSize must be at least 1000 bytes");
return;
}
}
} else if (maxPayloadSize != 0 && maxPayloadSize > maxSize / 3) {
maxPayloadSize = (int)(maxSize / 3);
}
q.setMaxSize(maxSize);
q.setMaxPayloadSize(maxPayloadSize);
changed = true;
}
if (dto.warnAfter != null) {
try {
int secs = convertDuration(dto.warnAfter);
if (secs != q.getWarnAfter()) {
q.setWarnAfter(secs);
changed = true;
}
} catch (IllegalArgumentException e) {
call.setCode(422, "Invalid warnAfter value, expected duration");
return;
}
}
if (dto.errorAfter != null) {
try {
int secs = convertDuration(dto.errorAfter);
if (secs != q.getErrorAfter()) {
q.setErrorAfter(secs);
changed = true;
}
} catch (IllegalArgumentException e) {
call.setCode(422, "Invalid errorAfter value, expected duration");
return;
}
}
if (create) {
for (int attempt = 0; ; ) {
q.setId(generateId());
if (repo.findQueue(q.getId()) == null) break;
if (++attempt == 20) throw new IOException("Got " + attempt + " dup id's attempting to create queue?");
}
}
if (create) {
db = db.deepCopy(); // make a copy before we modify it
Map<String, String> queues = db.getQueues();
if (queues == null) db.setQueues(queues = new HashMap<String, String>());
queues.put(id, q.getId());
repo.updateDatabase(db);
}
if (changed) repo.updateQueue(q);
}
call.setCode(create ? 201 : 200, createQueueDTO(call, id, q));
}
@Override
protected void delete(Call call, String id) throws IOException {
String qid = call.getDatabase().getQidForQueue(id);
if (qid == null) {
call.setCode(404);
return;
}
repo.deleteQueue(qid);
}
@Override
protected Controller getController(Call call, String id, String resource) throws IOException {
String qid = call.getDatabase().getQidForQueue(id);
Queue q = qid == null ? null : repo.findQueue(qid);
if (q != null) {
call.setQueue(q);
if ("messages".equals(resource)) return messageController;
if ("out".equals(resource)) return outputController;
if ("in".equals(resource)) return inputController;
if ("timeline".equals(resource)) return timelineController;
}
return StatusCodeController.SC_404;
}
}