Интерфейс на событиях
От: tyomchick Россия  
Дата: 16.06.15 09:23
Оценка: :)
История такая.
Сделал я класс, который реализует работу с пулом модемов.
  Интерфейс у него приблизительно такой:
public class ModemsPool : IEnumerable<Modem>, IDisposable
{
        /// <summary>
        /// Инициировать соединение через пул модемов
        /// </summary>
        /// <param name="route">Модемный маршрут</param>
        /// <returns>true - если возможна установка соединения, иначе false </returns>
    public bool InitConnect(ModemRoute route)
    {
    ...
    }  

    /// <summary>
    /// Событие установки соединения 
    /// </summary>    
    public event EventHandler<ModemsEventArgs> ModemConnected;

    /// <summary>
    /// Событие разрыва соединения 
    /// </summary>
    public event EventHandler<ModemsDisconnectedEventArgs> ModemDisconnected;

    /// <summary>
    /// Возникновение ошибки 
    /// </summary>
    public event EventHandler<ModemsExecutedErrorEventArgs> ModemExecutedError;

    /// <summary>
    /// Передача диагностической информации (журнала)
    /// </summary>
    public event EventHandler<ModemsMessageEventArgs> ModemMessage;  
}

Смысл его работы такой:
1. Потребитель вызывает InitConnect, который запускает асинхронный процесс установки соединения через свободный модем или ставит запрос в очередь на ожидание освобождения модема;
2. По факту установки/неудачи установки соединения, инициируется соответствующее событие, которое помимо прочего возвращает объект Modem, который содержит методы для работы с модемом (реализует Stream для прокачки данных через модем, содержит функцию разрыва соединения и прочее...).

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

Но вот так получилось, что один пул нужно пользовать из нескольких сборок в рамках одного процесса. Т.е. пул модемов один, а логика поверх разная.
Если конкретно, то ряд устройств реализуют хитрожопый протокол работы по GPRS. На них нужно позонить, они определяют номер, бросают трубу, а сами по номеру из своей таблицы выбирают адрес сервера и порт, и коннектятся на него.

Ну в общем я вынес класс в отдельную сборку, сделал из пула сингтон.
Но вот беда, в рамках одного процесса несколько сборок пользуют один объект, но при этом все получают все события. И свои и чужие.
А нужно бы сделать, что бы каждый клиент получал свои события.
Понятно, что изначально дизайн был косячный, но я чего то не сварю как его красиво переделать.

На уровне ModemsPool я могу отличить одних клиентов от других, т.к. фактический тип ModemRoute них будет разный и для меня вполне применимо допущение, что только один клиент будет использовать определенный тип ModemRoute. Но как без большого гемороя сделать селективную инициацию событий пока не понял.

Подскажите пожалуйста.
Можем от событий совсем стоит отказаться.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Отредактировано 16.06.2015 9:24 tyomchick . Предыдущая версия .
Re: Интерфейс на событиях
От: Sinix  
Дата: 16.06.15 09:41
Оценка: 1 (1)
Здравствуйте, tyomchick, Вы писали:

T>История такая.

Решение стандартное: берём реальные сценарии и пишем API так, чтобы с ним было удобно работать.

T>1. Потребитель вызывает InitConnect, который запускает асинхронный процесс установки соединения через свободный модем или ставит запрос в очередь на ожидание освобождения модема;

var modem = await InitConnectAsync(someParams);

// or

ModemPool.QueueTask(someParams, modem => SomeCallback(modem));



T>2. По факту установки/неудачи установки соединения, инициируется соответствующее событие, которое помимо прочего возвращает объект Modem, который содержит методы для работы с модемом (реализует Stream для прокачки данных через модем, содержит функцию разрыва соединения и прочее...).

А это уже ответственность самого модема, пул тут ни при чём. Посмотрите на реализацию SqlConnection — светится ли там пул (а он есть) и нужны ли там события вообще?


T>На уровне ModemsPool я могу отличить одних клиентов от других, т.к. фактический тип ModemRoute них будет разный и для меня вполне применимо допущение, что только один клиент будет использовать определенный тип ModemRoute. Но как без большого гемороя сделать селективную инициацию событий пока не понял.

Не выносить ответственность за работу с модемом в сам пул, по-другому никак.
Re[2]: Интерфейс на событиях
От: tyomchick Россия  
Дата: 17.06.15 14:41
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, tyomchick, Вы писали:


T>>История такая.

S>Решение стандартное: берём реальные сценарии и пишем API так, чтобы с ним было удобно работать.

Да, у меня к сожалению сценарий изменился.


T>>1. Потребитель вызывает InitConnect, который запускает асинхронный процесс установки соединения через свободный модем или ставит запрос в очередь на ожидание освобождения модема;

S>
S>var modem = await InitConnectAsync(someParams);
S>

Это мне вряд ли подойдет. Своего постоянного потока там внутри нет. Там автоматы, которые работают в потоках таймеров, так что для этого придется создавать отдельный ждущий поток.

S>
S>ModemPool.QueueTask(someParams, modem => SomeCallback(modem));
S>


Видимо так и придется на каждое соединение передавать отдельный калбек(и).

T>>2. По факту установки/неудачи установки соединения, инициируется соответствующее событие, которое помимо прочего возвращает объект Modem, который содержит методы для работы с модемом (реализует Stream для прокачки данных через модем, содержит функцию разрыва соединения и прочее...).

S>А это уже ответственность самого модема, пул тут ни при чём. Посмотрите на реализацию SqlConnection — светится ли там пул (а он есть) и нужны ли там события вообще?

T>>На уровне ModemsPool я могу отличить одних клиентов от других, т.к. фактический тип ModemRoute них будет разный и для меня вполне применимо допущение, что только один клиент будет использовать определенный тип ModemRoute. Но как без большого гемороя сделать селективную инициацию событий пока не понял.

S>Не выносить ответственность за работу с модемом в сам пул, по-другому никак.

Не совсем понял что вы имели ввиду.
За все модемные дела отвечает конечно класс модема (установка, разрыв соединения, обмен данными), просто пул владеет мадемами и контролирает состояние. Функции пула сводятся не просто к выборке свободного модема. Он может пнуть модем на соединение, в процессе которого может быть диагностировано его зависание (ну или отказ из-за отрицательного баланса), в этом случае пул выбирает другой модем. Идея была в том, что бы наружу выкидывать уже готовый поток ввода/вывода (Stream), через который прокачивать данные.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Re[3]: Интерфейс на событиях
От: Sinclair Россия https://github.com/evilguest/
Дата: 17.06.15 18:34
Оценка: +3
Здравствуйте, tyomchick, Вы писали:

T>Видимо так и придется на каждое соединение передавать отдельный калбек(и).

Да, и это гораздо лучше, чем изначальный вариант.
Независимо от количества сборок. Ведь у вас же нет никакой гарантии, что кому-то ещё не потребуется новое соединение ещё до того, как полностью отработает старое?
Так что по идее вам нужно просто передать в InitConnection() параметр типа Function<Stream>, в который и будет "выкинут" поток данных. А по Dispose этого стрима надо будет возвращать модем в пул.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[3]: Интерфейс на событиях
От: Sinix  
Дата: 18.06.15 06:01
Оценка: 2 (1)
Здравствуйте, tyomchick, Вы писали:


var modem = await InitConnectAsync(someParams);

T>Это мне вряд ли подойдет. Своего постоянного потока там внутри нет. Там автоматы, которые работают в потоках таймеров, так что для этого придется создавать отдельный ждущий поток.
Так ничего не мешает всю кухню спрятать за Task-ом. TaskCompletionSource и вперёд

T>Видимо так и придется на каждое соединение передавать отдельный калбек(и).

А это единственно правильное решение. Как его оформить для клиента: как таск или как ModemPool.QueueTask() — это уже нюансы, причём непринципиальные.


S>>А это уже ответственность самого модема, пул тут ни при чём. Посмотрите на реализацию SqlConnection — светится ли там пул (а он есть) и нужны ли там события вообще?


T>Не совсем понял что вы имели ввиду.

T>За все модемные дела отвечает конечно класс модема (установка, разрыв соединения, обмен данными), просто пул владеет мадемами и контролирает состояние. Функции пула сводятся не просто к выборке свободного модема. Он может пнуть модем на соединение, в процессе которого может быть диагностировано его зависание (ну или отказ из-за отрицательного баланса), в этом случае пул выбирает другой модем. Идея была в том, что бы наружу выкидывать уже готовый поток ввода/вывода (Stream), через который прокачивать данные.

Посмотрите как устроен пул соединений. Сама идея изложена например тут, см "How Does the Connection Pool Work?".

Под капотом живёт полноценный пул соединений со своей логикой, автопереподключением, поддержкой failover etc. Клиент работает с обёрткой, которая владеет одним из соединений из пула. Поэтому для клиента всё это выглядит так, как будто он просто каждый раз создаёт новое соединение. Причём сам пул реальной логикой коннекта не занимается, он только отдаёт команды реальному ресурсу, а тот уже сам действует по обстоятельствам.
Re[4]: Интерфейс на событиях
От: tyomchick Россия  
Дата: 18.06.15 06:33
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, tyomchick, Вы писали:


T>>Видимо так и придется на каждое соединение передавать отдельный калбек(и).

S>Да, и это гораздо лучше, чем изначальный вариант.
S>Независимо от количества сборок. Ведь у вас же нет никакой гарантии, что кому-то ещё не потребуется новое соединение ещё до того, как полностью отработает старое?
Да нет, с этим как раз проблем не было. Пул как раз был рассчитан на параллельную установку многих соединений и работу в них. Просто в аргументах инициируемых пулом событий передавался контекст задания. Проблема именно в нескольких потребителях пула. Они в такой архитектуре получают чужие события и это конечно треш.

S>Так, что по идее вам нужно просто передать в InitConnection() параметр типа Function<Stream>, в который и будет "выкинут" поток данных. А по Dispose этого стрима надо будет возвращать модем в пул.


Да, видимо как то так и придется делать. Хотя не все так просто. Нужно еще передавать диагностическую информацию в контексте задания.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Re[4]: Интерфейс на событиях
От: tyomchick Россия  
Дата: 18.06.15 06:40
Оценка:
Здравствуйте, Sinix, Вы писали:



S>Посмотрите как устроен пул соединений. Сама идея изложена например тут, см "How Does the Connection Pool Work?".


Спасибо, посмотрю, попробую сделать красиво.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Re[5]: Интерфейс на событиях
От: Sinclair Россия https://github.com/evilguest/
Дата: 18.06.15 07:11
Оценка: +1
Здравствуйте, tyomchick, Вы писали:
T>Да нет, с этим как раз проблем не было. Пул как раз был рассчитан на параллельную установку многих соединений и работу в них. Просто в аргументах инициируемых пулом событий передавался контекст задания. Проблема именно в нескольких потребителях пула. Они в такой архитектуре получают чужие события и это конечно треш.
Одно противоречит другому. Вы, судя по всему, описываете "потребителя", который "потребляет" одновременно несколько соединений.
Я пока не могу понять, зачем это может быть нужно.

T>Да, видимо как то так и придется делать. Хотя не все так просто. Нужно еще передавать диагностическую информацию в контексте задания.

Ну, вот к примеру в мире веба есть объект WebResponse. В нём есть и диагностика, и собственно Stream с приехавшими данными.
У вас, соответственно, должен возвращаться ModemConnection, с которым уже можно работать — писать в OutputStream, читать из InputStream, видеть диагностику, и т.п.
"Задание", по идее — это штука более высокого уровня, чем Connection.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: Интерфейс на событиях
От: tyomchick Россия  
Дата: 18.06.15 08:14
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, tyomchick, Вы писали:

T>>Да нет, с этим как раз проблем не было. Пул как раз был рассчитан на параллельную установку многих соединений и работу в них. Просто в аргументах инициируемых пулом событий передавался контекст задания. Проблема именно в нескольких потребителях пула. Они в такой архитектуре получают чужие события и это конечно треш.
S>Одно противоречит другому. Вы, судя по всему, описываете "потребителя", который "потребляет" одновременно несколько соединений.
S>Я пока не могу понять, зачем это может быть нужно.

Не совсем понял что чему противоречит. Я видимо не совсем ясно выражаюсь. Под потребителем я просто понимал код который использует синглтон пула.
Не уверен что смогу описать всю архитектуру и не уверен в необходимости этого.
Код просто делает запрос на каналы связи и по событиям от него асинхронно получает этот канал или получает отказ. Этих каналов параллельно существует множество (и не все они модемные), к ним строятся очереди запросов и ответы из каналов распихиваются по модулям-инициаторам запросов. Причем этих инициаторов для одного канала может быть множество, т.к. даже на одном модеме может висеть пачка разнотипных устройств с разными протоколами .... В общем пул это только нижний слой весьма многослойной системы.

T>>Да, видимо как то так и придется делать. Хотя не все так просто. Нужно еще передавать диагностическую информацию в контексте задания.

S>Ну, вот к примеру в мире веба есть объект WebResponse. В нём есть и диагностика, и собственно Stream с приехавшими данными.
S>У вас, соответственно, должен возвращаться ModemConnection, с которым уже можно работать — писать в OutputStream, читать из InputStream, видеть диагностику, и т.п.
Возможно как то так и сделаю,но пока плохо представляю как. Просто изначально была идея выдачи из пула уже готового соединения, а диагностическая инфа прет еще до момента коннекта. Возможно это не совсем дачная идея.

S>"Задание", по идее — это штука более высокого уровня, чем Connection.

Тут я под заданием имел ввиду только задачу установки коммутируемого соединения.
Даже самую простую задачу можно сделать невыполнимой, если провести достаточное количество совещаний
Re[7]: Интерфейс на событиях
От: Sinclair Россия https://github.com/evilguest/
Дата: 18.06.15 11:11
Оценка: 42 (1) +2
Здравствуйте, tyomchick, Вы писали:

T>Не совсем понял что чему противоречит. Я видимо не совсем ясно выражаюсь. Под потребителем я просто понимал код который использует синглтон пула.

Я под потребителем имел в виду "объект". Код — это "класс"; одновременно может существовать от 0 до бесконечности экземпляров конкретного класса.

T>Не уверен что смогу описать всю архитектуру и не уверен в необходимости этого.

Всю — не надо. Надо просто понять, что делает клиент, и как это вообще работает.

T>Код просто делает запрос на каналы связи и по событиям от него асинхронно получает этот канал или получает отказ. Этих каналов параллельно существует множество (и не все они модемные), к ним строятся очереди запросов и ответы из каналов распихиваются по модулям-инициаторам запросов. Причем этих инициаторов для одного канала может быть множество, т.к. даже на одном модеме может висеть пачка разнотипных устройств с разными протоколами .... В общем пул это только нижний слой весьма многослойной системы.

Не очень понятно, каким образом в одном модеме одновременно может быть много каналов.
У модема же ровно один stream для отправки/приёма данных. Если в него одновременно пишут два "инициатора", то получается каша.

Большинство "пулов" чего бы то ни было устроены именно так, как описал Sinix — они позволяют изолировать клиента от всего зоопарка.
В простом вымышленном примере, нам надо дозвониться до номера ХYZEWQR, установить сетевое соединение, и отправить туда текст "hello world". При этом нам всё равно, через какой модем мы это будем делать — важно, чтобы пока мы занимаемся отправкой этого текста, никто не вклинился со своим "ася, приноси кота на случку".
Поэтому мы не обращаемся к конкретному модему, а просим у пула дать нам какой-нибудь модем на время. Пул помечает этот модем как занятый и отдаёт его нам; мы инициализируем соединение, отправляем текст, вешаем трубу, и возвращаем модем в пул. Если одноврменно с этим кто-то ещё (например, в другом потоке) запрашивает модем, то пул ему не даст тот же самый модем — вплоть до момента, пока мы не вернём его в пул.
Если коду-инициатору нужен не обязательно модем, а некий абстрактный канал, то делаем пул каналов.
Типа
using(var channel = Channels.GetChannel(name:"канал1", type:ChannelType.ModemChannel, auxiliaryData:"num=+799912311232"))
{
   channel.StreamWriter.Write("hello, world!");
} // channel.Dispose() освобождает канал и занятые каналом ресурсы; в частности, разрешает использовать модем для другой работы
Console.WriteLine("Text sent!");


События (или таски) нам обычно интересны в тех случаях, когда мы не хотим замораживать поток на время длинной операции, а хотим отправить и пойти дальше.
В абстрактном виде всё сводится к переписыванию приведнного кода на
Channels.GetChannel(name:"канал1", type:ChannelType.ModemChannel, auxiliaryData:"num=+799912311232"), (channel)=>
{
   channel.StreamWriter.Write("hello, world!");
});
Console.WriteLine("Text send operation scheduled!");

И всё отличие — в том, что в консоль мы напишем текст, не дожидаясь повешения трубки модема. Ну и в том, что в зависимости от выбранной стратегии шедулинга, мы можем быть обязаны подготовиться к тому, что код работы с каналом будет вызван не в том же потоке, что и код вывода в консоль.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.