[personal profile] gdsfh
Меня давно волнует это тема, но в последнее время появилась явная практическая необходимость в ней, поэтому от личного и полупубличного обдумывания перейду к публичному.

Хочется мне построить одну систему в идеологии actors (или близкой к ней).

Если вкратце, это означает, что общение между компонентами системы осуществляется через message passing, и каждый компонент представляет собой обработчик сообщения, и на каждое сообщение запускается определённый, обычно один и тот же код. Это не общий случай message passing, так как нет явной функции "принять сообщение", и, тем более, нет selective receive. Но этим я приобретаю некоторые гарантии корректности.

Далее, терминологически, "компонент системы, получающий сообщение" = "процесс".

В чём тут польза? В простоте запуска кучи процессов, в неплохом управлении ресурсами (при запуске процесса ресурс берётся, при завершении финализируется). Например, один из компонентов системы будет заниматься общением с базой данных, при запуске будет открывать соединение к БД, при завершении будет это соединение закрывать. Можно запустить несколько таких процессов, можно даже с общей очередью сообщений и с указанием "кто первый прочитал сообщение -- тот его и обрабатывает". При завершении общего процесса он рассылает сообщения "завершайте работу" всем порождённым процессам, они завершают работу по мере выполнения текущих дел, закрывают соединение к БД. То есть, более-менее красиво.

Но от процессов бывает нужно обрабатывать различные запросы с различными ответами на них. В моём случае, например, будет запрос "выбрать из БД строку по её id" и будет операция "добавить строку в БД, возвращая её id" (кроме того, будут и другие операции, но для простоты их две).

То есть, select : id -> row и insert : row -> id.

Но эти операции должны работать с одним и тем же состоянием, содержащим соединение к БД. В простом случае можно оформить их в процесс, принимающий значение с типом rq и возвращающий значение с типом rs:
type rq = [ Select of id | Insert of row ];
type rs = [ Selected of row | Inserted of id ];


Но тогда код, работающий с этим процессом, приобретает уродливый вид:
value select id = call dbserver (Select id) >>= fun
[ Selected row -> работаем с row
| Inserted _ -> failwith "ошибка протокола"
];
value insert row = call dbserver (Insert row) >>= fun
[ Inserted new_id -> работаем с new_id
| Selected _ -> failwith "ошибка протокола"
];


Особенно печально в случаях, когда там не два варианта, а чуть больше.

Одно из решений -- разнести логику dbserver в несколько процессов:
dbserver_select : process id row;
dbserver_insert : process row id;


Но тогда, получится, либо нужно как-то протаскивать "общее состояние" по процессам, использующим одно и то же соединение к БД, либо использовать по одному соединению на каждую из операций, которая осуществляется с БД. А если делать пул, позволяющий одновременное выполнение N однотипных операций, а всего операций M, то получим N*M соединений к БД. А это -- криво.

В случае, когда осуществляется протаскивание состояния (например, если есть комбинатор, позволяющий строить процессы, использующие одно конкретное состояние, и блокирующий выполнение, пока состояние не отпустят), то всё получится, но криво: будут состояния s_i, которые используются соответствующими парами dbserver_select и dbserver_insert, и при выполнении dbserver_select на s_i, 1. нужно будет как-то обеспечивать то, что в этот момент при поступлении сообщения к dbserver_insert сообщение будет передано не тому процессу, который уже занял s_i, а свободному, 2. при какой-то фатальной ошибке нужно будет прибивать все процессы, использующие этот же s_i, 3. при завершении всех этих процессов нужно будет финализировать s_i. А это всё -- дополнительная логика, которую не знаю, как можно было бы прописать в общем случае, а в каждом конкретном случае прописывать руками -- замумукаюсь.

Задачу можно чуть обобщить, указав, что всё общение осуществляется через какой-то "протокол", определяемый как
type dest 'o = ...;
value dest_make : unit -> dest 'o;
value dest_put : dest 'o -> 'o -> unit;
value dest_get : dest 'o -> 'o;

type proto =
[ P_select of id and dest row
| P_insert of row and dest id
];


Но тогда надо было бы гарантировать, что все dest 'o будут заполнены при обработке запроса (а их в каждом конструкторе типа может быть больше, а может и вообще не быть, этого не проверить). Сейчас мне это не нужно гарантировать, потому что процесс строится из функции, берущей запрос и возвращающей ответ, ответ один, поэтому в случае успеха dest 'o будет заполнен. А в будущем -- гарантировать как-то пришлось бы, так как на случай расширения акторов на несколько разных хостов для каждого dest 'o, которое передаётся на другой хост, будет создаваться определённое значение, слот, куда придёт ответ при записи в этот dest 'o (там же надо хранить информацию о том, как десериализовывать поток байтов, идущий в слот, в зависимости от того, что предлагает данный тип 'o), и этот слот -- тоже ресурс, который нужно освобождать.

Можно поиграться с gadts, описать протокол так:
type proto 'resp =
[ Select of id and eq 'resp row
| Insert of row and eq 'resp id
];
value call_proto : proto 'resp -> 'resp;

Из проблем сходу -- разве что сериализация значений с типами eq 't1 't2, но это решаемо.
Кроме того, функция, пересылающая запрос серверу (например, Select 123 (Eq.refl ())), должна знать, как ей нужно десериализовывать ответ. Но на этот случай можно расширить тип eq 't1 't2 так, чтобы для одного из аргументов указывалось, как его десериализовывать.

Но, вообще, решение какое-то странное, придумалось только сейчас, и не может же быть, что всё так просто. Есть ли другие проблемы, которые я не вижу? Если рассматривать не детские случаи "работа в пределах одного рантайма", а сетевой случай и [де]сериализацию сообщений, в том числе в недоверенном окружении.

Profile

gdsfh

August 2013

S M T W T F S
    123
45678910
111213 14151617
18192021222324
25262728293031

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 27th, 2017 04:45 pm
Powered by Dreamwidth Studios