Я разработал и обслуживал несколько систем, которые управляют огромным количеством этих маленьких конечных автоматов. (Некоторые системы, до 100K / день, некоторые 100K / минута)
Я обнаружил, что чем больше вы указали явно возиться, тем больше вероятность, что оно где-то сломается. Или, другими словами, чем больше вы говорите вывод , тем более надежное решение.
Как говорится, вы должны поддерживать некоторое состояние. Но постарайтесь, чтобы он был минимальным.
Кроме того, логика конечного автомата в одном месте делает систему более надежной и простой в обслуживании. То есть не помещайте свою логику конечного автомата в код и базу данных. Я предпочитаю свою логику в коде.
Предпочтительное решение. (Простые картинки лучше).
Для вашего примера у меня будет очень простая таблица:
task_id, current_at, current_duration, is_done, is_deleted, description...
и выводят состояние, основанное на now
относительно current_at
и current_duration
. Это работает на удивление хорошо. Убедитесь, что вы проиндексировали / разбили таблицу на current_at
.
Логика обработки при изменении перехода
Вещи разные, когда вам нужно запустить событие при изменении перехода.
Измените таблицу так, чтобы она выглядела так:
task_id, current_at, current_duration, state, locked_by, locked_until, description...
Оставьте индекс на current_at
и добавьте индекс state
, если хотите. Сейчас вы работаете с состоянием, поэтому все становится немного более хрупким из-за параллелизма или сбоя, поэтому нам придется немного укрепить его, используя locked_by
и locked_until
для оптимистической блокировки, которую я опишу ниже.
Я полагаю, что ваша программа будет иногда зависать в середине обработки - даже если только для развертывания.
Вам нужен механизм для перевода задачи из одного состояния в другое. Чтобы упростить обсуждение, я буду заниматься переходом от БУДУЩЕГО к ТЕКУЩЕМУ, но логика одна и та же, независимо от перехода.
Если ваш набор данных достаточно велик, вы постоянно опрашиваете базу данных, чтобы обнаружить задачи, требующие перехода (конечно, с линейным или экспоненциальным откатом, когда нечего делать); в противном случае вы используете или ваш любимый планировщик, будь то cron или на основе ruby , или Quartz, если вы подписываетесь на Java / Scala / C #.
Выберите все записи, которые нужно переместить из БУДУЩЕГО в ТЕКУЩИЙ и в настоящее время не заблокированы.
( обновлен :)
-- move from pending to current
select task_id
from tasks
where now >= current_at
and (locked_until is null OR locked_until < now)
and state == 'PENDING'
and current_at >= (now - 3 days) -- optimization
limit :LIMIT -- optimization
Добавьте все эти task_id
в вашу надежную очередь. Или, если необходимо, просто обработайте их в своем сценарии.
Когда вы начинаете работать с элементом, вы должны сначала заблокировать его, используя нашу оптимистическую схему блокировки:
update tasks
set locked_by = :worker_id -- unique identifier for host + process + thread
, locked_until = now + 5 minutes -- however this looks in your SQL langage
where task_id = :task_id -- you can lock multiple tasks here if necessary
and (locked_until is null OR locked_until < now) -- only if it's not locked!
Теперь, если вы действительно обновили запись, у вас есть блокировка. Теперь вы можете использовать свою специальную логику при переходе. (Аплодисменты. Это то, что отличает вас от всех других менеджеров задач, верно?)
Когда это успешно , обновите состояние задачи, убедитесь, что вы все еще используете оптимистическую блокировку:
update tasks
set state = :new_state
, locked_until = null -- explicitly release the lock (an optimization, really)
where task_id = :task_id
and locked_by = :worker_id -- make sure we still own the lock
-- no-one really cares if we overstep our time-bounds
Многопоточность / оптимизация процесса
Делайте это только тогда, когда у вас есть несколько потоков или процессов, выполняющих задачи обновления в пакетном режиме (например, в задании cron или опросе базы данных)! Проблема в том, что каждый из них получит аналогичные результаты из базы данных, а затем будет бороться за блокировку каждой строки. Это неэффективно как из-за замедления работы базы данных, так и из-за того, что у вас есть потоки, которые в основном ничего не делают, кроме как замедляют другие.
Итак, добавьте ограничение на количество результатов, возвращаемых запросом, и следуйте этому алгоритму:
results = database.tasks_to_move_to_current_state :limit => BATCH_SIZE
while !results.empty
results.shuffle! # make sure we're not in lock step with another worker
contention_count = 0
results.each do |task_id|
if database.lock_task :task_id => task_id
on_transition_to_current task_id
else
contention_count += 1
end
break if contention_count > MAX_CONTENTION_COUNT # too much contention!
done
results = database.tasks_to_move_to_current_state :limit => BATCH_SIZE
end
Возьмите с собой BATCH_SIZE
и MAX_CONTENTION_COUNT
, пока программа не станет супербыстрой.
Обновление:
Оптимистическая блокировка позволяет использовать несколько процессоров параллельно.
Имея таймаут блокировки (через поле locked_until
), он допускает сбой при обработке перехода.Если процессор выходит из строя, другой процессор может выполнить задачу по истечении времени ожидания (5 минут в приведенном выше коде).В таком случае важно а) блокировать задачу только тогда, когда вы собираетесь над ней работать;и б) заблокировать задачу на то, сколько времени потребуется для выполнения задачи плюс щедрая свобода действий.
Поле locked_by
в основном для целей отладки (какой процесс / машина былаэто включено?) Достаточно иметь поле locked_until
, если драйвер базы данных возвращает количество обновленных строк, но только , если вы обновляете по одной строке за раз.