- 222
- 5
- 28
Есть мод, в котором есть проблема с многопоточностью. Его автор очень сильно обосрался с её реализацией. Как я думаю уже понятно - мне выпала "честь" исправить это.
И так, допустим есть некий объект, который формируется из базы данных при надобности, и над которым выполняются несколько действий и что самое важное, завершающее действие должно выполниться в главном потоке.
К примеру приходит пакет на сервер, который говорит, что нужно сделать с объектом с id таким-то некое действие описанное с помощью Consumer.
Но перед тем как это дело делать, нужно получить и сформировать этот объект из базы данных.
Конечно же это делается в отдельном треде.
После получения и сформирования объекта мы передадим его на съедение Consumer, но сделаем это добавив задачу в очередь, которая будет выполнена при следующем тике сервера:
А теперь сама проблема. Представьте, что то самое получение пакета является нажатием на кнопку в GUI. И что нажатий может быть овер дохрена тем же кликером.
Т.е. что мы имеет - при многократном нажатии на кнопку отправляется кучу пакетов, эти кучу пакетов первым делом получают в async треде объект из базы базы данных, с которым им предстоит работать. И проблема в том, что действия начинают выполняться последовательно и если на первой задачи тот самый объект изменился, то следующая задача дубликат об этом ничего не знает и выполняет действие по новой. Конечно же нельзя сделать так, чтобы получение объекта всегда была "свежее", так как оно будет выполняться в главном потоке, а это уже лаги.
Автор попытался справится с данной проблемой временной "блокировкой" конекта(даже не вдумывайтесь, полнейшая параша гавнокода), это помогло, но в итоге это привело к тому, что игроки догадались делать это во время сохранения мира или лага сервера, тем самым забивая очередь одинаковыми задачами. Позже я узнал, что даже без лага получилось воспроизвести проблему.
Конечно, я мог бы сделать простейшую защиту от флуда(по типам действий и id объектов), но так как я считаю что это будет костыль и подобная система мне будет нужна 100% в другом моде\плагина я бы хотел сделать её сразу хорошо и правильно.
Собственно главный вопрос - как решить данную проблему максимально правильно и без костыльно? Как правильно спроектировать архитектуру в данном случае мода для подобной задачи?
И так, допустим есть некий объект, который формируется из базы данных при надобности, и над которым выполняются несколько действий и что самое важное, завершающее действие должно выполниться в главном потоке.
К примеру приходит пакет на сервер, который говорит, что нужно сделать с объектом с id таким-то некое действие описанное с помощью Consumer.
Java:
public static void receive(String id, PacketBuffer buffer, EntityPlayer player) {
switch (id) {
case "simple_action_1":
int objectId = buffer.readInt();
Messenger.getObjectById(objectId, object -> {
//simple actions with the object...
});
break;
case "simple_action_2":
//todo
break;
case "simple_action_3":
//todo
break;
case "simple_action_4":
//todo
break;
}
}
Но перед тем как это дело делать, нужно получить и сформировать этот объект из базы данных.
Конечно же это делается в отдельном треде.
Java:
public static void getObjectById(int id, Consumer<MessageContainer> consumer) {
CoreServer.listener().connect((connection, statement) -> {
try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM [ICODE]table[/ICODE] WHERE id = ?")) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
SimpleObject object = new SimpleObject(rs);
CoreServer.add(e -> {
try {
consumer.accept(object);
} catch (Throwable ex) {
CoreServer.listener().unlock();
throw ex;
}
});
}
}
} catch (Exception e) {
LOGGER.error("Exception while get object by id: '" + id + "'", e);
}
});
}
После получения и сформирования объекта мы передадим его на съедение Consumer, но сделаем это добавив задачу в очередь, которая будет выполнена при следующем тике сервера:
Java:
private static final Queue<ITickUpdate> tickUpdates = new ArrayDeque<>();
public static void add(ITickUpdate update) {
synchronized (tickUpdates) {
tickUpdates.add(update);
}
}
@SubscribeEvent
public void update(TickEvent.ServerTickEvent event) {
synchronized (tickUpdates) {
while (!tickUpdates.isEmpty()) {
tickUpdates.poll().tick(event);
}
}
}
@FunctionalInterface
public interface ITickUpdate {
void tick(TickEvent.ServerTickEvent e);
}
А теперь сама проблема. Представьте, что то самое получение пакета является нажатием на кнопку в GUI. И что нажатий может быть овер дохрена тем же кликером.
Т.е. что мы имеет - при многократном нажатии на кнопку отправляется кучу пакетов, эти кучу пакетов первым делом получают в async треде объект из базы базы данных, с которым им предстоит работать. И проблема в том, что действия начинают выполняться последовательно и если на первой задачи тот самый объект изменился, то следующая задача дубликат об этом ничего не знает и выполняет действие по новой. Конечно же нельзя сделать так, чтобы получение объекта всегда была "свежее", так как оно будет выполняться в главном потоке, а это уже лаги.
Автор попытался справится с данной проблемой временной "блокировкой" конекта(даже не вдумывайтесь, полнейшая параша гавнокода), это помогло, но в итоге это привело к тому, что игроки догадались делать это во время сохранения мира или лага сервера, тем самым забивая очередь одинаковыми задачами. Позже я узнал, что даже без лага получилось воспроизвести проблему.
Конечно, я мог бы сделать простейшую защиту от флуда(по типам действий и id объектов), но так как я считаю что это будет костыль и подобная система мне будет нужна 100% в другом моде\плагина я бы хотел сделать её сразу хорошо и правильно.
Собственно главный вопрос - как решить данную проблему максимально правильно и без костыльно? Как правильно спроектировать архитектуру в данном случае мода для подобной задачи?