System.Threading.Channels — высокопроизводительный производитель-потребитель и асинхронность без аллокаций и стэк дайва

И снова здравствуй. Какое-то время назад я писал о другом малоизвестном инструменте для любителей высокой производительности — System.IO.Pipelines. По своей сути, рассматриваемый System.Threading.Channels (в дальнейшем «каналы») построен по похожим принципам, что и Пайплайны, решает ту же задачу — Производитель-Потребитель. Однако имеет в разы более простое апи, которое изящно вольется в любого рода enterprise-код. При этом использует асинхронность без аллокаций и без stack-dive даже в асинхронном случае! (Не всегда, но часто).


Оглавление

Введение

Задача Производитель/Потребитель встречается на пути программистов довольно часто и уже не первый десяток лет. Сам Эдсгер Дейкстра приложил руку к решению данной задачи — ему принадлежит идея использования семафоров для синхронизации потоков при организации работы по принципу производитель/потребитель. И хотя ее решение в простейшем виде известно и довольно тривиально, в реальном мире данный шаблон (Производитель/Потребитель) может встречаться в гораздо более усложненном виде. Также современные стандарты программирования накладывают свои отпечатки, код пишется более упрощенно и разбивается для дальнейшего переиспользования. Все делается для понижения порога написания качественного кода и упрощения данного процесса. И рассматриваемое пространство имен — System.Threading.Channels — очередной шаг на пути к этой цели.

Какое-то время назад я рассматривал System.IO.Pipelines. Там требовалось более внимательная работа и глубокое осознание дела, в ход шли Span и Memory, а для эффективной работы требовалось не вызывать очевидных методов (чтобы избежать лишних выделений памяти) и постоянно думать в байтах. Из-за этого программный интерфейс Пайплайнов был нетривиален и не интуитивно понятен.

В System.Threading.Channels пользователю представляется гораздо более простое api для работы. Стоит упомянуть, что несмотря на простоту api, данный инструмент является весьма оптимизированным и на протяжении своей работы вполне вероятно не выделит память. Возможно это благодаря тому, что под капотом повсеместно используется ValueTask, а даже в случае реальной асинхронности используется IValueTaskSource, который переиспользуется для дальнейших операций. Именно в этом заключается весь интерес реализации Каналов.

Каналы являются обобщенными, тип обобщения, как несложно догадаться — тип, экземпляры которого будут производиться и потребляться. Интересно то, что реализация класса Channel, которая помещается в 1 строку (источник github):

namespace System.Threading.Channels {     public abstract class Channel<T> : Channel<T, T> { } } 

Таким образом основной класс каналов параметризован 2 типами — отдельно под канал производитель и канал потребитель. Но для реализованых каналов это не используется.
Для тех, кто знаком с Пайплайнами, общий подход для начала работы покажется знакомым. А именно. Мы создаем 1 центральный класс, из которого вытаскиваем отдельно производителей(ChannelWriter) и потребителей(ChannelReader). Несмотря на названия, стоит помнить, что это именно производитель/потребитель, а не читатель/писатель из еще одной классической одноименной задачи на многопоточность. ChannelReader изменяет состояние общего channel (вытаскивает значение), которое более становится недоступно. А значит он скорее не читает, а потребляет. Но с реализацией мы ознакомимся позже.

Начало работы. Channel

Начало работы с каналами начинается с абстрактного класса Channel<T> и статического класса Channel, который создает наиболее подходящую реализацию. Далее из этого общего Channel можно получать ChannelWriter для записи в канал и ChannelReader для потребления из канала. Канал является хранилищем общей информации для ChannelWriter и ChannelReader, так, именно в нем хранятся все данные. А уже логика их записи или потребления рассредоточена в ChannelWriter и ChannelReader, Условно каналы можно разделить на 2 группы — безграничные и ограниченные. Первые более простые по реализации, в них можно писать безгранично (пока память позволяет). Вторые же ограничены неким максимальным значением количества записей.

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

Поведения читателей по большей части одинаково — если в канале есть что-то, то читатель просто читает это и завершается синхронно. Если ничего нет, то ожидает пока кто-то что-то запишет.

Статический класс Channel содержит 4 метода для создания вышеперечисленных каналов:

Channel<T> CreateUnbounded<T>(); Channel<T> CreateUnbounded<T>(UnboundedChannelOptions options); Channel<T> CreateBounded<T>(int capacity); Channel<T> CreateBounded<T>(BoundedChannelOptions options); 

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

UnboundedChannelOptions содержит 3 свойства, значение которых по умолчанию false:

  1. AllowSynchronousContinuations — просто сумасводящая опция, которая позволяет выполнить продолжение асинхронной операции тому, кто ее разблокирует. А теперь по-простому. Допустим, мы писали в заполненный канал. Соответственно, операция прерывается, поток освобождается, а продолжение будет выполнено по завершению на новом потоке из пула. Но если включить эту опцию, продолжение выполнит тот, кто разблокирует операцию, то есть в нашем случае читатель. Это серьезно меняет внутреннее поведение и позволяет более экономно и производительно распоряжаться ресурсами, ведь зачем нам слать какие-то продолжения в какие-то потоки, если мы можем сами его выполнить;
  2. SingleReader — указывает, что будет использоваться один потребитель. Опять же, это позволяет избавиться от некоторой лишней синхронизации;
  3. SingleWriter — то же самое, только для писателя;

BoundedChannelOptions содержит те же 3 свойства и еще 2 сверху

  1. AllowSynchronousContinuations — то же;
  2. SingleReader — то же;
  3. SingleWriter — то же;
  4. Capacity — количество вмещаемых в канал записей. Данный параметр также является параметром конструктора;
  5. FullMode — перечисление BoundedChannelFullMode, которое имеет 4 опции, определяет поведение при попытке записи в заполненный канал:
    • Wait — ожидает освобождения места для завершения асинхронной операции
    • DropNewest — записываемый элемент перезаписывает самый новый из существующих, завершается синхронно
    • DropOldest — записываемый элемент перезаписывает самый старый из существующих завершается синхронно
    • DropWrite — записываемый элемент не записывается, завершается синхронно

В зависимости от переданных параметров и вызванного метода будет создана одна из 3 реализаций: SingleConsumerUnboundedChannel, UnboundedChannel, BoundedChannel. Но это не столь важно, ведь пользоваться каналом мы будем через базовый класс Channel<TWrite, TRead>.

У него есть 2 свойства:

  • ChannelReader<TRead> Reader { get; protected set; }
  • ChannelWriter<TWrite> Writer { get; protected set; }

А также, 2 оператора неявного приведения типа к ChannelReader<TRead> и ChannelWriter<TWrite>.

Пример начала работы с каналами:

Channel<int> channel = Channel.CreateUnbounded<int>(); //Можно делать так ChannelWriter<int> writer = channel.Writer; ChannelReader<int> reader = channel.Reader;  //Или так ChannelWriter<int> writer = channel; ChannelReader<int> reader = channel; 

Данные хранятся в очереди. Для 3 типов используются 3 разные очереди — ConcurrentQueue<T>, Deque<T> и SingleProducerSingleConsumerQueue<T>. На этом моменте мне показалось, что я устарел и пропустил кучу новых простейших коллекций. Но спешу огорчить — они не для всех. Помечены internal, так что использовать их не получится. Но если вдруг они понадобятся на проде — их можно найти здесь (SingleProducerConsumerQueue) и здесь (Deque). Реализация последней весьма проста. Советую ознакомится, ее очень быстро можно изучить.

Итак, приступим к изучению непосредственно ChannelReader и ChannelWriter, а также интересных деталей реализации. Они все сводятся к асинхронности без выделений памяти с помощью IValueTaskSource.

ChannelReader — потребитель

При запросе объекта потребителя возвращается одна из реализаций абстрактного класса ChannelReader<T>. Опять же в отличие от Пайплайнов АПИ несложное и методов немного. Достаточно просто знать список методов, чтобы понять, как использовать это на практике.

Методы:

  1. Виртуальное get-only свойство Task Completion { get; }
    Обьект типа Task, который завершается, когда закрывается канал;
  2. Виртуальное get-only свойство int Count { get; }
    Тут следует заострить внимание, что возвращается текущее количество доступных для чтения объектов;
  3. Виртуальное get-only свойство bool CanCount { get; }
    Показывает, доступно ли свойство Count;
  4. Абстрактный метод bool TryRead(out T item)
    Пытается потребить объект из канала. Возвращает bool, показывающий, получилось ли у него прочитать. Результат помещается в out параметр (или null, если не получилось);
  5. Абстрактный ValueTask<bool> WaitToReadAsync(CancellationToken cancellationToken = default)
    Возвращается ValueTask со значением true, когда в канале появятся доступные для чтения данные, до тех пор задача не завершается. Возвращает ValueTask со значением false, когда канал закрывается(данных для чтения больше не будет);
  6. Виртуальный метод ValueTask<T> ReadAsync(CancellationToken cancellationToken = default)
    Потребляет значение из канала. Если значение есть, возвращается синхронно. В противном случае асинхронно ждет появления доступных для чтения данных и возвращает их.

    У данного метода в абстрактном классе есть реализация, которая основана на методах TryRead и WaitToReadAsync. Если опустить все инфраструктурные нюансы (исключения и cancelation tokens), то логика примерно такая — попытаться прочитать объект с помощью TryRead. Если не удалось, то в цикле while(true) проверять результат метода WaitToReadAsync. Если true, то есть данные есть, вызвать TryRead. Если TryRead получается прочитать, то вернуть результат, в противном случае цикл по новой. Цикл нужен для неудачных попыток чтения — в результате гонки потоков, сразу много потоков могут получить завершение WaitToReadAsync, но объект будет только один, соответственно только один поток сможет прочитать, а остальные уйдут на повторный круг.
    Однако данная реализация, как правило, переопределена на что-то более завязанное на внутреннем устройстве.

ChannelWriter — производитель

Все аналогично потребителю, так что сразу смотрим методы:

  1. Виртуальный метод bool TryComplete(Exception? error = null)
    Пытается пометить канал как завершенный, т.е. показать, что в него больше не будет записано данных. В качестве необязательного параметра можно передать исключение, которое вызвало завершение канала. Возвращает true, если удалось завершить, в противном случае false (если канал уже был завершен или не поддерживает завршение);
  2. Абстрактный метод bool TryWrite(T item)
    Пытается записать в канал значение. Возвращает true, если удалось и false, если нет
  3. Абстрактный метод ValueTask<bool> WaitToWriteAsync(CancellationToken cancellationToken = default)
    Возвращает ValueTask со значением true, который завершится, когда в канале появится место для записи. Значение false будет в том случае, если записи в канал более не будут разрешены;
  4. Виртуальный метод ValueTask WriteAsync(T item, CancellationToken cancellationToken = default)
    Асинхронно пишет в канал. Например, в случае, если канал заполнен, операция будет реально асинхронной и завершится только после освобождения места под данную запись;
  5. Метод void Complete(Exception? error = null)
    Просто пытается пометить канал как завершенный с помощью TryComplete, а в случае неудачи кидает исключение.

Небольшой пример вышеописанного (для легкого начала ваших собственных экспериментов):

Channel<int> unboundedChannel = Channel.CreateUnbounded<int>();  //Объекты ниже можно отправить в разные потоки, которые будут использовать их независимо в своих целях ChannelWriter<int> writer = unboundedChannel; ChannelReader<int> reader = unboundedChannel;  //Первый поток может писать в канал int objectToWriteInChannel = 555; await writer.WriteAsync(objectToWriteInChannel); //И завершить его, при исключении или в случае, когда записал все, что хотел writer.Complete();  //Второй может читать данные из канала по мере их доступности int valueFromChannel = await reader.ReadAsync(); 

А теперь перейдем к самой интересной части.

Асинхронность без алллокаций

В процессе написания и изучения кода, я осознал, что почти ничего интересного в реализации всех этих операций нет. В общем можно описать так — избегание лишних блокировок с помощью конкурентных коллекций и обильное использование ValueTask, который является структурой, что экономит память. Однако спешу напомнить, что не стоит быстрой заменой проходиться по всем файлам на вашей ПЭВМ и заменять все Task на ValueTask. Он имеет смысл только в случаях, когда операция в большинстве случаев завершается синхронно. Ведь, как мы помним, при асинхронности вполне вероятна смена потока, а значит и стек уже будет не тот, что прежде. Да и вообще, истинный профессионал в области производительности знает — не оптимизируй до возникновения проблем.

Радует одно, в профессионалы я себя записывать не буду, а поэтому самое время разобраться, в чем же секрет написания асинхронного кода без выделений памяти, что на первый взгляд звучит слишком хорошо для правды. Но бывает и такое.

Интерфейс IValueTaskSource

Начнем наш путь с истоков — структуры ValueTask, которая была добавлена в .net core 2.0 и дополнена в 2.1. Внутри этой структуры скрывается хитрое поле object _obj. Несложно догадаться, опираясь на говорящее название, что в этом поле может скрываться одна из 3 вещей — null, Task/Task<T> или IValueTaskSource. На самом деле, это вытекает из способов создания ValueTask.

Как заверяет производитель, данную структуру следует использовать лишь очевидно — с ключевым словом await. То есть не следует применять await много раз к одному и тому же ValueTask, использовать комбинаторы, добавлять несколько продолжений и тп. Также не следует получать результат из ValueTask более одного раза. А связано это как раз с тем, что мы пытаемся понять — переиспользованием всего этого добра без выделения памяти.

Я уже упомянул интерфейс IValueTaskSource. Именно он помогает сэкономить память. Делается это с помощью переиспользования самого IValueTaskSource несколько раз для множества задач. Но именно из-за этого переиспользования и нет возможности баловаться с ValueTask.

Итак, IValueTaskSource. Данный интерфейс имеет 3 метода, реализовав которые вы будете успешно экономить память и время на выделении тех заветных байт.

  1. GetResult – Вызывается единожды, когда в стейт машине, образованной на рантайме для асинхронных методов, понадобится результат. В ValueTask есть метод GetResult, который и вызывает одноименный метод интерфейса, который, как мы помним, может хранится в поле _obj.
  2. GetStatus – Вызывается стейт машиной для определения состояния операции. Также через ValueTask.
  3. OnCompleted – Опять же, вызывается стейт машиной для добавления продолжения к невыполненной на тот момент задаче.

Но несмотря на простой интерфейс, реализация потребует определенной сноровки. И тут можно вспомнить про то, с чего мы начали — Channels. В данной реализации используется класс AsyncOperation, который является реализацией IValueTaskSource. Данный класс скрыт за модификатором доступа internal. Но это не мешает разобраться, в основных механизмах. Напрашивается вопрос, почему не дать реализацию IValueTaskSource в массы? Первая причина (хохмы ради) — когда в руках молоток, повсюду гвозди, когда в руках реализация IValueTaskSource, повсюду неграмотная работа с памятью. Вторая причина (более правдоподобная) — в то время, как интерфейс прост и универсален, реальная реализация оптимальна при использований определенных нюансов применения. И вероятно именно по этой причине можно найти реализации в самых разных частях великого и могучего .net, как то AsyncOperation под капотом каналов, AsyncIOOperation внутри нового API сокетов и тд.
Однако, справедливости ради, есть все же одна общая реализация — ManualResetValueTaskSourceCore. Но это уже слишком далеко от темы статьи.

CompareExchange

Довольно популярный метод популярного класса, позволяющий избежать накладных расходов на классические примитивы синхронизации. Думаю, большинство знакомы с ним, но все же стоит описать в 3 словах, ведь данная конструкция используется довольно часто в AsyncOperation.
В массовой литературе данную функцию называют compare and swap (CAS). В .net она доступна в классе Interlocked.

Сигнатура следующая:

public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class; 

Имеются также перегрузи с int, long, float, double, IntPtr, object.

Сам метод атомарный, то бишь выполняется без прерываний. Сравнивает 2 значения и, если они равны, выполняет присваивание нового значения в переменную. Решают проблему, когда нужно проверить значение переменной и в зависимости от него изменить переменную.

Допустим, вы хотите инкрементировать переменную, если ее значение меньше 10.

Далее идут 2 потока.

Поток 1 Поток 2
Проверяет значение переменной на некоторое условие (то есть меньше ли оно 10), которое срабатывает
Между проверкой и изменением значения Присваивает переменной значение, не удовлетворяющее условию (например, 15)
Изменяет значение, хотя не должен, ведь условие уже не соблюдается

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

location1 — переменная, значение которой мы хотим поменять. Оно сравнивается с comparand, в случае равенства в location1 записывается value. Если операция удалась, то метод вернет прошлое значение переменной location1. Если же нет, то будет возращено актуальное значение location1.
Если говорить чуть глубже, то существует инструкция языка ассемблера cmpxchg, которая выполняет эти действия. Именно она и используется под капотом.

Stack dive

Рассматривая весь этот код я не раз наткнулся на упоминания «Stack Dive». Это очень крутая и интересная штука, которая на самом деле очень нежелательна. Суть в том, что при синхронном выполнении продолжений мы можем исчерпать ресурсы стека.

Допустим, мы имеем 10000 задач, в стиле

//code1 await ... //code2

Допустим, первая задача завершает выполнение и этим освобождает продолжение второй, которое мы начинаем тут же выполнять синхронно в этом потоке, то есть забирая кусок стека стек фреймом данного продолжения. В свою очередь, данное продолжение разблокирует продолжение третей задачи, которое мы тоже начинаем сразу выполнять. И так далее. Если в продолжении больше нет await’ов или чего-то, что как-то сбросит стек, то мы просто будем потреблять стековое пространство до упора. Что может вызвать StackOverflow и крах приложения. В рассмотрении кода я упомяну, как с этим борется AsyncOperation.

AsyncOperation как реализация IValueTaskSource

Source code.

Внутри AsyncOperation есть поле _continuation типа Action<object>. Поле используется для, не поверите, продолжений. Но, как это часто бывает в слишком современном коде, у полей появляются дополнительные обязанности (как сборщик мусора и последний бит в ссылке на таблицу методов). Поле _continuation из той же серии. Есть 2 специальных значения, которые могут хранится в этом поле, кроме самого продолжения и null. s_availableSentinel и s_completedSentinel. Данные поля показывают, что операция доступна и завершена соответственно. Доступна она бывает как раз для переиспользования для совершенно асинхронной операции.

Также AsyncOperation реализует IThreadPoolWorkItem с единственным методом — void Execute() => SetCompletionAndInvokeContinuation(). Метод SetCompletionAndInvokeContinuation как раз и занимается выполнением продолжения. И данный метод вызывается либо напрямую в коде AsyncOperation, либо через упомянутый Execute. Ведь типы реализующие IThreadPoolWorkItem можно забрасывать в тред пул как-то вот так ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false).

Метод Execute будет выполнен тред пулом.

Само выполнение продолжения довольно тривиально.

Продолжение _continuation копируется в локальную переменную, на ее место записывается s_completedSentinel — искусственный объект-марионетка (иль часовой, не знаю, как глаголить мне в нашей речи), который указывает, что задача завершена. Ну а далее локальная копия реального продолжения просто выполняется. При наличии ExecutionContext, данные действия постятся в контекст. Никакого секрета тут нет. Этот код может быть вызван как напрямую классом — просто вызвав метод, инкапсулирующий эти действия, так и через интерфейс IThreadPoolWorkItem в тред пуле. Теперь можно догадаться, как работает функция с выполнением продолжений синхронно.

Первый метод интерфейса IValueTaskSource — GetResult (github).

Все просто, он:

  1. Инкрементирует _currentId.
    _currentId — то, что идентифицирует конкретную операцию. После инкремента она уже не будет ассоциирована с этой операцией. Поэтому не следует получать результат дважды и тп;
  2. помещает в _continuation делегат-марионетку s_availableSentinel. Как было упомянуто, это показывает, что этот экземпляр AsyncOperation можно испоьзовать повторно и не выделять лишней памяти. Делается это не всегда, а лишь если это было разрешено в конструкторе (pooled = true);
  3. Возвращает поле _result.
    Поле _result просто устанавливается в методе TrySetResult который описан ниже.

Метод TrySetResult (github).

Метод тривиален. — он сохраняет принятый параметр в _result и сигнализирует о завершении, а именно вызывает метод SignalCompleteion, который довольно интересен.

Метод SignalCompletion (github).

В данном методе используется все, о чем мы говорили в начале.

В самом начале, если _comtinuation == null, мы записываем марионетку s_completedSentinel.

Далее метод можно разделить на 4 блока. Сразу скажу для простоты понимания схемы, 4 блок — просто синхронное выполнение продолжения. То есть тривиальное выполнение продолжения через метод, как я описано в абзаце про IThreadPoolWorkItem.

  1. Если _schedulingContext == null, т.е. нет захваченного контекста (это первый if).
    Далее необходимо проверить _runContinuationsAsynchronously == true, то есть явно указано, что продолжения нужно выполнять как все привыкли — асинхронно (вложенный if).
    При соблюдении данный условий в бой идет схема с IThreadPoolWorkItem описанная выше. То есть AsyncOperation добавляется в очередь на выполнение потоком тред пула. И выходим из метода.
    Следует обратить внимание, что если первый if прошел (что будет очень часто, особенно в коре), а второй нет, то мы не попадем в 2 или 3 блок, а спустимся сразу на синхронное выполнение продолжения — т.е. 4 блок;
  2. Если _schedulingContext is SynchronizationContext, то есть захвачен контекст синхронизации (это первый if).
    По аналогии мы проверяем _runContinuationsAsynchronously = true. Но этого не достаточно. Необходимо еще проверить, контекст потока, на котором мы сейчас находимся. Если он отличен от захваченного, то мы тоже не можем просто выполнить продолжение. Поэтому если одно из этих 2 условий выполнено, мы отправляем продолжение в контекст знакомым способом:
    sc.Post(s => ((AsyncOperation<TResult>)s).SetCompletionAndInvokeContinuation(), this); 

    И выходим из метода. опять же, если первая проверка прошла, а остальные нет (то есть мы сейчас находимся на том же контексте, что и был захвачен), мы попадем сразу на 4 блок — синхронное выполнение продолжения;

  3. Выполняется, если мы не зашли в первые 2 блока. Но стоит расшифровать это условие.
    Хитрость в том, что _schedulingContext может быть на самом деле захваченным TaskScheduler, а не непосредственно контекстом. В этом случае мы поступаем также, как и в блоке 2, т.е. проверяем флаг _runContinuationsAsynchronously = true и TaskScheduler текущего потока. Если планировщик не совпадает или флаг не тот, то мы сетапим продолжение через Task.Factory.StartNew и передаем туда этот планировщик. И выходим из метода.
  4. Как и сказал в начале — просто выполняем продолжение на текущем потоке. Раз мы до сюда дошли, то все условия для этого соблюдены.

Второй метод интерфейса IValueTaskSource — GetStatus (github)
Просто как питерская пышка.

Если _continuation != _completedSentinel, то возвращаем ValueTaskSourceStatus.Pending
Если error == null, то возвращаем ValueTaskSourceStatus.Succeeded
Если _error.SourceException is OperationCanceledException, то возвращаем ValueTaskSourceStatus.Canceled
Ну а коль уж до сюда дошли, то возвращаем ValueTaskSourceStatus.Faulted

Третий и последний, но самый сложный метод интерфейса IValueTaskSource — OnCompleted (github)

Метод добавляет продолжение, которое выполняется по завершению.

При необходимости захватывает ExecutionContext и SynchronizationContext.

Далее используется Interlocked.CompareExchange, описанный выше, чтобы сохранить продолжение в поле, сравнивая его с null. Напоминаю, что CompareExchange возвращает актуальное значение переменной.

Если сохранение продолжения прошло, то возвращается значение, которое было в переменной до обновления, то есть null. Это означает, что операция еще не завершилась на момент записи продолжения. И тот, кто ее завершит сам со всем разберется (как мы смотрели выше). И нам нет смысла выполнять какие-то дополнительные действия. И на этом работа метода завершается.

Если сохранить значение не получилось, то есть из CompareExchange вернулось что-то кроме null. В этом случае кто-то успел положить значение в быстрее нас. То есть произошла одна из 2 ситуаций — или задача завершилась быстрее, чем мы до сюда дошли, или была попытка записать более 1 продолжения, что делать нельзя.

Таким образом проверяем возвращенное значение, равно ли оно s_completedSentinel — именно оно было бы записано в случае завершения.

  • Если это не s_completedSentinel, то нас использовали не по плану — попытались добавить более одного продолжения. То есть то, которое уже записано, и то, которое пишем мы. А это исключительная ситуация;
  • Если это s_completedSentinel, то это один из допустимых исходов, операция уже завершена и продолжение должны вызвать мы, здесь и сейчас. И оно будет выполнено асинхронно в любом случае, даже если _runContinuationsAsynchronously = false.
    Сделано это так, потому что если мы дошли до этого места, значит мы внутри метода OnCompleted, внутри awaiter’а. А синхронное выполнение продолжений именно здесь грозит упомянутым стек дайвом. Сейчас вспомним, для чего нам нужна эта AsyncOperation — System.Threading.Channels. А там ситуация может быть очень легко достигнута, если о ней не задуматься. Допустим, мы читатель в ограниченном канале. Мы читаем элемент и разблокируем писателя, выполняем его продолжение синхронно, что разблокирует очередного читателя(если читатель очень быстр или их несколько) и так далее. Тут стоит осознать тонкий момент, что именно внутри awaiter’а возможна эта ситуация, в других случаях продолжение выполнится и завершится, что освободит занятый стек фрейм. А постоянный зацеп новых продолжений вглубь стека порождается постоянным выполнением продолжения внутри awaiter’а.
    В целях избежания данной ситуации, несмотря ни на что необходимо запустить продолжение асинхронно. Выполняется по тем же схемам, что и первые 3 блока в методе SignalCompleteion — просто в пуле, на контексте или через фабрику и планировщик

А вот и пример синхронных продолжений:

class Program     {         static async Task Main(string[] args)         {             Channel<int> unboundedChannel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions             {                 AllowSynchronousContinuations = true             });              ChannelWriter<int> writer = unboundedChannel;             ChannelReader<int> reader = unboundedChannel;              Console.WriteLine($"Main, before await. Thread id: {Thread.CurrentThread.ManagedThreadId}");              var writerTask = Task.Run(async () =>             {                 Thread.Sleep(500);                 int objectToWriteInChannel = 555;                 Console.WriteLine($"Created thread for writing with delay, before await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");                 await writer.WriteAsync(objectToWriteInChannel);                 Console.WriteLine($"Created thread for writing with delay, after await write. Thread id: {Thread.CurrentThread.ManagedThreadId}");             });              //Blocked here because there are no items in channel             int valueFromChannel = await reader.ReadAsync();             Console.WriteLine($"Main, after await (will be processed by created thread for writing). Thread id: {Thread.CurrentThread.ManagedThreadId}");              await writerTask;              Console.Read();         }     } 

Output:

Main, before await. Thread id: 1
Created thread for writing with delay, before await write. Thread id: 4
Main, after await (will be processed by created thread for writing). Thread id: 4
Created thread for writing with delay, after await write. Thread id: 4

ссылка на оригинал статьи https://habr.com/ru/post/508726/

На пороге квантового сознания

Предпосылки появления ИИ, превосходящего мозг человека:

  • закон Мура для квантовых компьютеров;
  • появление языков программирования для квантовых компьютеров;
  • квантовый компьютер похож на работу интуиции, воли и сознания человека — почти мгновенный перебор всех возможных вариантов решения задачи и выбор оптимального ответа;
  • запрет клонирования квантовых состояний аналогичен невозможности клонировать наше сознание;
  • квантовый ИИ должен дополнять классический ИИ, также как в мозге человека различные его структуры работают как единый механизм;
  • любой классический ИИ обучается решению только одной задачи и работает эффективнее человека только в узком сегменте деятельности. Так например, обученная модель ИИ лучше играет в шахматы, но при этом не способна делать что-либо другое. Квантовый ИИ должен тиражировать подходы к решению различных типов задач, ускоряя процесс обучения новой задачи.

Предположим…

Постоянного «Я» человека не существует. «Я» индивида возникает в ответ на взаимодействие человека с миром и необходимо ему для более эффективной работы с окружающим миром. В различных состояниях возникает разное «Я»: во сне — одно «Я», наяву — другое «Я».

Поэтому принципиально неразрешимой проблемы переноса сознания на другой носитель нет.
Бессознательно в человеке работают наиболее автоматические элементы мозга, управляющие работой жизненно важных органов. Как только возникает необходимость принять решение и сделать выбор — говорят о появлении некого «я» — сознания. Выбор делает элемент мозга человека, по-видимому имеющий квантовую природу — аналог квантового ИИ. Правда, наличие сознательного выбора не обязательно означает появление самоосознания — некого «Я». Для появления «Я» требуется активное взаимодействие с памятью — с той памятью, которая может быть визуализирована, услышана либо виртуализирована (представлена) каким-либо другим способом, накопленным опытом человека. Причем различные блоки содержания опыта, доступ к которым осуществляется, например, во сне или наяву, порождает различные «Я» человека. Хотя иногда часть опыта различных состояний проникает друг в друга и таким образом одно «Я» индивида объединяется с другим его «Я», обогащая друг друга.

image

Перенос человеческого сознания на машинный носитель

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

Стремительное развитие ИИ и появление квантового компьютера, технология которого уже развивается по закону Мура, ставит вопрос о появлении сознания у компьютера. Долгое время считалось, что самосознание компьютера (в смысле осознания собственных действий и способности самостоятельно принимать решения) может возникнуть после достаточного развития и усложнения ИИ. Однако не все функции мозга могут быть транслированы на классический ИИ. Нейронная сеть может обучиться одному навыку и похоже не способна транслировать свой опыт на другие сферы деятельности. Кроме того в задачах связанным с большим количеством вариантов решений (с перебором) время нахождения оптимального решения крайне велико, что по эффективности не сопоставимо с нашим мозгом. Т.о. классический компьютер возможно является только частью комплексного «устройства», обязательно включающего в себя часть, работающей на квантовых принципах. По мере развития человеческого мозга качественный скачек в его эволюции, выделивший человека от других животных, вероятно связан с появлением той части коры головного мозга, работа которой основана на квантовых принципах.

Современные технологии ИИ шли примерно тем же путем, что и эволюция живого на Земле: сначала появились химические сети, а затем более быстрые — нейронные сети, но по прежнему выполняющие одну-единственную функцию. Жизненно важная часть информации записывались в генетическую последовательность, чтобы не повторять обучение в процессе жизни одного организма каждый раз, а передавать «лучшие практики» из поколения в поколение. В какой то момент эволюции произошел квантовый скачек, когда появился квантовый параллелизм в мозге человека, который позволил не обучаться последовательно каждому навыку, а анализировать, обобщать и находить лучшее решение с точки зрения эволюции практически мгновенно.

Мы называем сознанием некую «высшую» функцию мозга, которая делает выбор, принимает решение. Сознание непосредственно связано с мыслительной деятельностью, прогнозированием и моделированием, оценкой и анализом ситуации, воображением и выбором, т.е. это комплексная многомерная работа мозга и она связанна с перебором и анализом всех возможных вариантов. Рассмотрим простой пример — человеку захотелось съесть кусочек торта… В некий момент времени в организме человеке стало не хватать сахара и он посылает сигнал поступает в мозг — возникает мысль: «Я хочу торт». На самом деле сознание интерпретировало первоначальную потребность: «Нужен сахар». Почему этот процесс проходит через этап осознавания этой потребности? Можно было бы, минуя сознательную часть мозга, отправить человека к холодильнику и достать оттуда торт…. Человеку как бы дается право принять окончательное решение: есть или не есть торт. При этом появляется возможность смоделировать ситуацию, спрогнозировать возможные последствия, определить его важности, отранжировать потребности, определить насколько оно сейчас своевременно и пр. — т.е. подумать над этим и принять решение.

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

В нашем мозге можно выделить две составные части аналогичные двум типам существующих компьютеров: классическую и квантовую.

Для того чтобы понять какие участки человеческого мозга основаны на квантовых явлениях, сначала обозначим классические. Очевидно, первые в этом списке будут — части мозга, ответственные за обработку сигналов идущих от органов восприятия: аналог ИИ по распознаванию образов. На вход подаются сигналы из внешнего мира, на выходе «картинка» — образ объекта, с которым работают другие участки мозга. Часть сигналов обрабатывается бессознательным — автоматическими элементами нашего компьютера. Вся древняя часть мозга — области связанные с процессами обеспечения жизнедеятельности организм — это классический компьютер, область бессознательного, наработанная тысячелетней эволюцией.

Другие проходят через новейшие с точки зрения эволюции участки мозга, связанные с работой нашего сознания, которые судя по всему являются аналогом квантовых компьютеров. Сознание встраивается в информационную цепочку для принятия решения только в процессе первоначального обучения новым навыкам, т.е. первые несколько сотен раз, в зависимости от типов навыков. Так мы учимся сознательно водить автомобиль. Но после освоения навыка «до автоматизма» полностью передаем эту функцию бессознательному. После обучения сознательное принятие решения и функция управления полностью автоматизируется и переходит на бессознательный уровень — т.о. формируется новый навык. При этом многократно увеличивается скорость принятия решения — чем больше отработка навыка, тем выше скорость реакция. Примерами обучения могут являться: изучение языка или живописи, управление велосипедом или машины, обучение танцам или боевым искусствам. При появлении новой информации при уже сформировавшемся навыке, сознательная функция снова активизируется, дообучая нашу нейронную сеть, а потом снова гаснет. Характерно, что восприятие времени в период обучения или другой сознательной деятельности удлиняется (например, путешествие в незнакомых местах), а наоборот для рутинных операций субъективное время укорачивается.
(Про возникновение сознания в процессе обучения в своих работах писал еще Э.Шредингер)

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

Каким же образом, а главное для чего в процессе обучения подключается сознание — квантовый компьютер мозга? Очевидно, лучше справляясь с задачами перебора, он лучше моделирует будущую ситуацию и не дожидаясь обратной связи от внешнего мира, перебирает все возможные ситуации, которые могли бы возникнуть в реальном мире. Время обучения значительно ускоряется, повышая выживаемость организма. Т.о. естественный отбор выбирает сознательную деятельность.

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

Лучшим доказательством гипотезы о квантовой природе сознании или ее опровержение будет являться создание ИИ на базе квантового компьютера.

см. Нейроны сознания Е.Н. СОКОЛОВ

ссылка на оригинал статьи https://habr.com/ru/post/509976/

Применение CQRS & Event Sourcing в создании платформы для проведения онлайн-аукционов

Коллеги, добрый день! Меня зовут Миша, я работаю программистом.

В настоящей статье я хочу рассказать о том, как наша команда решила применить подход CQRS & Event Sourcing в проекте, представляющем собой площадку для проведения онлайн-аукционов. А также о том, что из этого получилось, какие из нашего опыта можно сделать выводы и на какие грабли важно не наступить тем, кто отправится путем CQRS & ES.
image

Прелюдия

Для начала немного истории и бизнесового бэкграунда. К нам пришел заказчик с платформой для проведения так называемых timed-аукционов, которая была уже в продакшене и по которой было собрано некоторое количество фидбэка. Заказчик хотел, чтоб мы сделали ему платформу для live-аукционов.

Теперь чуть-чуть терминологии. Аукцион — это когда продаются некие предметы — лоты (lots), а покупатели (bidders) делают ставки (bids). Обладателем лота становится покупатель, предложивший самую большую ставку. Timed-аукцион — это когда у каждого лота заранее определен момент его закрытия. Покупатели делают ставки, в какой-то момент лот закрывается. Похоже на ebay.

Timed-платформа была сделана классически, с применением CRUD. Лоты закрывало отдельное приложение, запускаясь по расписанию. Работало все это не слишком надежно: какие-то ставки терялись, какие-то делались как будто бы от лица не того покупателя, лоты не закрывались или закрывались по несколько раз.

Live-аукцион — это возможность участвовать в реальном офлайн-аукционе удаленно, через интернет. Есть помещение (в нашей внутренней терминологии — «комната»), в нем находится ведущий аукциона с молотком и аудитория, и тут же рядом с ноутбуком сидит так называемый клерк, который, нажимая кнопки в своем интерфейсе, транслирует в интернет ход аукциона, а подключившиеся к аукциону покупатели видят ставки, которые делаются офлайн, и могут делать свои ставки.

Обе платформы в принципе работают в реальном времени, но если в случае timed все покупатели находятся в равном положении, то в случае live крайне важно, чтоб онлайн-покупатели могли успешно соревноваться с находящимися «в комнате». То есть система должна быть очень быстрой и надежной. Печальный опыт timed-платформы недвусмысленно говорил нам, что классический CRUD нам не подходит.

Своего опыта работы с CQRS & ES у нас не было, так что мы посовещались с коллегами, у которых он был (компания у нас большая), презентовали им наши бизнес-реалии и совместно пришли к заключению, что CQRS & ES должен нам подойти.

Какая еще есть специфика работы онлайн-аукционов:

  • Много пользователей одновременно пытаются воздействовать на один и тот же объект в системе — текущий лот. Покупатели делают свои ставки, клерк вводит в систему ставки «из комнаты», закрывает лот, открывает следующий. В каждый момент времени в системе можно сделать ставку только одной величины — например, 5 рублей. И только один пользователь сможет сделать эту ставку.
  • Нужно хранить всю историю действий над объектами системы, чтобы в случае необходимости можно было посмотреть, кто какую ставку сделал.
  • Время отклика системы должно быть очень маленьким — ход онлайн-версии аукциона не должен отставать от офлайн, пользователям должно быть понятно, к чему привели их попытки сделать ставку — успешны они или нет.
  • Пользователи должны оперативно узнавать обо всех изменениях в ходе аукциона, а не только о результатах своих действий.
  • Решение должно быть масштабируемым — несколько аукционов могут проходить одновременно.

Краткий обзор подхода CQRS & ES

Не буду подробно останавливаться на рассмотрении подхода CQRS & ES, материалы об этом есть в интернете и в частности на Хабре (например, вот: Введение в CQRS + Event Sourcing). Однако кратко все же напомню основные моменты:

  • Самое главное в event sourcing: система хранит не данные, а историю их изменения, то есть события. Текущее состояние системы получается последовательным применением событий.
  • Доменная модель делится на сущности, называемые агрегатами. Агрегат имеет версию. События применяются к агрегатам. Применение события к агрегату инкрементирует его версию.
  • События хранятся в write-базе. В одной и той же таблице хранятся события всех агрегатов системы в том порядке, в котором они произошли.
  • Изменения в системе инициируются командами. Команда применяется к одному агрегату. Команда применяется к последней, то есть текущей, версии агрегата. Агрегат для этого выстраивается последовательным применением всех «своих» событий. Этот процесс называется регидратацией.
  • Для того, чтобы не регидрировать каждый раз с самого начала, какие-то версии агрегата (обычно каждая N-я версия) можно хранить в системе в готовом виде. Такие «снимки» агрегата называются снапшотами. Тогда для получения агрегата последней версии при регидратации к самому свежему снапшоту агрегата применяются события, случившиеся после его создания.
  • Команда обрабатывается бизнес-логикой системы, в результате чего получается, в общем случае, несколько событий, которые сохраняются в write-базу.
  • Кроме write-базы, в системе может еще быть read-база, которая хранит данные в форме, в которой их удобно получать клиентам системы. Сущности read-базы не обязаны соответствовать один к одному агрегатам системы. Read-база обновляется обработчиками событий.
  • Таким образом, у нас получается разделение команд и запросов к системе — Command Query Responsibility Segregation (CQRS): команды, изменяющие состояние системы, обрабатываются write-частью; запросы, не изменяющие состояние, обращаются к read-части.

Реализация. Тонкости и сложности.

Выбор фреймворка

В целях экономии времени, а также в силу отсутствия специфического опыта мы решили, что нужно использовать какой-то фреймворк для CQRS & ES.

В целом наш технологический стек — это Microsoft, то есть .NET и C#. База данных — Microsoft SQL Server. Хостится все в Azure. На этом стеке была сделана timed-платформа, логично было и live-платформу делать на нем.

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

Зачем вообще нужен фреймворк CQRS & ES? Он может «из коробки» решать такие задачи и поддерживать такие аспекты реализации как:

  • Сущности агрегата, команды, события, версионирование агрегатов, регидратация, механизм снапшотов.
  • Интерфейсы для работы с разными СУБД. Сохранение/загрузка событий и снапшотов агрегатов в/из write-базы (event store).
  • Интерфейсы для работы с очередями — отправка в соответствующие очереди команд и событий, чтение команд и событий из очереди.
  • Интерфейс для работы с веб-сокетами.

Таким образом, с учетом использования Chinchilla, к нашему стеку добавились:

  • Azure Service Bus в качестве шины команд и событий, Chinchilla поддерживает его «из коробки»;
  • Write- и read-базы — Microsoft SQL Server, то есть обе они — SQL-базы. Не скажу, что это является результатом осознанного выбора, скорее по историческим причинам.

Да, фронтенд сделан на Angular.

Как я уже говорил, одно из требований к системе — чтобы пользователи максимально быстро узнавали о результатах своих действий и действий других пользователей — это относится и к покупателям, и к клерку. Поэтому мы используем SignalR и веб-сокеты для оперативного обновления данных на фронтенде. Chinchilla поддерживает интеграцию с SignalR.

Выбор агрегатов

Одной из первых вещей, которую надо сделать при реализации подхода CQRS & ES — это определить, как доменная модель будет делиться на агрегаты.

В нашем случае доменная модель состоит из нескольких основных сущностей, примерно таких:

public class Auction {      public AuctionState State { get; private set; }      public Guid? CurrentLotId { get; private set; }      public List<Guid> Lots { get; } }  public class Lot {      public Guid? AuctionId { get; private set; }      public LotState State { get; private set; }      public decimal NextBid { get; private set; }      public Stack<Bid> Bids { get; } }   public class Bid {      public decimal Amount { get; set; }      public Guid? BidderId { get; set; } } 

У нас получилось два агрегата: Auction и Lot (с Bid’ами). В общем, логично, но мы не учли одного — того, что при таком делении состояние системы у нас размазалось по двум агрегатам, и в ряде случаев для сохранения консистентности мы должны вносить изменения в оба агрегата, а не в один. Например, аукцион можно поставить на паузу. Если аукцион на паузе, то нельзя делать ставки на лот. Можно было бы ставить на паузу сам лот, но аукциону на паузе тоже нельзя обрабатывать никаких команд, кроме как «снять с паузы».

В качестве альтернативного варианта можно было сделать только один агрегат, Auction, со всеми лотами и ставками внутри. Но такой объект будет довольно тяжелым, потому что лотов в аукционе может быть до нескольких тысяч и ставок на один лот может быть несколько десятков. За время жизни аукциона у такого агрегата будет очень много версий, и регидратация такого агрегата (последовательное применение к агрегату всех событий), если не делать снапшотов агрегатов, будет занимать довольно продолжительное время. Что для нашей ситуации неприемлемо. Если же использовать снапшоты (мы их используем), то сами снапшоты будут весить очень много.

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

Подобные обстоятельства нужно учитывать, разбивая доменную модель на агрегаты.

Мы на данном этапе эволюции проекта живем с двумя агрегатами, Auction и Lot, и нарушаем архитектуру, меняя в рамках некоторых команд оба агрегата.

Применение команды к определенной версии агрегата

Если несколько покупателей одновременно делают ставку на один и тот же лот, то есть отправляют в систему команду «сделать ставку», успешно пройдет только одна из ставок. Лот — это агрегат, у него есть версия. При обработке команды создаются события, каждое из которых инкрементирует версию агрегата. Можно пойти двумя путями:

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

Мы используем второй вариант. Так у команд больше шансов выполниться. Потому что в той части приложения, которая отправляет команды (в нашем случае это фронтенд), текущая версия агрегата с некоторой вероятностью будет отставать от реальной версии на бэкенде. Особенно в условиях, когда команд отправляется много, и верcия агрегата меняется часто.

Ошибки при выполнении команды с использованием очереди

В нашей реализации, в большой степени обусловленной использованием Chinchilla, обработчик команд читает команды из очереди (Microsoft Azure Service Bus). Мы у себя явно разделяем ситуации, когда команда зафейлилась по техническим причинам (таймауты, ошибки подключения к очереди/базе) и когда по бизнесовым (попытка сделать на лот ставку той же величины, что уже была принята, и проч.). В первом случае попытка выполнить команду повторяется, пока не выйдет заданное в настройках очереди число повторений, после чего команда отправляется в Dead Letter Queue (отдельный топик для необработанных сообщений в Azure Service Bus). В случае бизнесового эксепшена команда отправляется в Dead Letter Queue сразу.

Ошибки при обработке событий с использованием очереди

События, создаваемые в результате выполнения команды, в зависимости от реализации, тоже могут отправляться в очередь и браться из очереди обработчиками событий. И при обработке событий тоже случаются ошибки.

Однако, в отличие от ситуации с невыполненной командой, здесь все хуже — может получиться так, что команда выполнилась и события в write-базу записались, но обработка их обработчиками зафейлилась. И если один из этих обработчиков обновляет read-базу, то read-база не обновится. То есть окажется в неконсистентном состоянии. Благодаря наличию механизма повторных попыток обработки события read-база почти всегда, в конечном итоге, обновляется, однако вероятность того, что после всех попыток она останется поломанной, все-таки остается.

Мы у себя столкнулись с этой проблемой. Причина, впрочем, была в большой степени в том, что в обработке событий у нас присутствовала некоторая бизнес-логика, которая при интенсивном потоке ставок имеет хорошие шансы фейлиться из раза в раз. К сожалению, осознали мы это слишком поздно, переделать бизнес-реализацию быстро и просто не получилось.

В итоге, в качестве временной меры мы отказались от использования Azure Service Bus для передачи событий из write-части приложения в read-часть. Вместо нее используется так называемая In-Memory Bus, что позволяет обрабатывать команду и события в одной транзакции и в случае неудачи откатить все целиком.

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

Отправка команды в качестве реакции на событие

Такое в принципе уместно, но только в случае, когда невыполнение этой второй команды не ломает состояние системы.

Обработка множества событий одной команды

В общем случае в результате выполнения одной команды получается несколько событий. Бывает, что на каждое из событий нам нужно сделать какое-то изменение в read-базе. Бывает также, что последовательность событий тоже важна, и в неправильной последовательности обработка событий не отработает как надо. Все это означает, что мы не можем читать из очереди и обрабатывать события одной команды независимо, например, разными экземплярами кода, который читает сообщения из очереди. Плюс к этому нам нужна гарантия того, что события из очереди будут прочитаны в той же последовательности, в которой они были туда отправлены. Либо нам нужно быть готовыми к тому, что не все события команды будут успешно обработаны с первой попытки.

Обработка одного события несколькими обработчиками

Если в качестве реакции на одно событие системе нужно выполнить несколько разных действий, обычно делаются несколько обработчиков этого события. Они могут отрабатывать параллельно или последовательно. В случае последовательного запуска при неуспешном выполнении одного из обработчиков вся последовательность запускается заново (в Chinchilla это так). При такой реализации важно, чтобы обработчики были идемпотентными, чтобы второй запуск один раз успешно отработавшего обработчика не свалился. В противном случае при падении второго обработчика из цепочки она, цепочка, уже точно не отработает целиком, потому что во вторую (и последующие) попытку упадет первый обработчик.

Например, обработчик события в read-базе добавляет ставку на лот величиной 5 рублей. Первая попытка сделать это будет успешной, а вторую не даст выполнить constraint в базе.

Выводы/Заключение

Сейчас наш проект находится в стадии, когда, как нам кажется, мы наступили уже на бОльшую часть существующих граблей, актуальных для нашей бизнес-специфики. В целом мы считаем свой опыт довольно успешным, CQRS & ES хорошо подходит для нашей предметной области. Дальнейшее развитие проекта видится в отказе от Chinchilla в пользу другого фреймворка, дающего больше гибкости. Впрочем, возможен и вариант отказа от использования фреймворка вообще. Также вероятно будут какие-то изменения в направлении поиска баланса между надежностью с одной стороны и быстротой и масштабируемостью решения с другой.

Что касается бизнес-составляющей, то и здесь некоторые вопросы по-прежнему остаются открытыми — например, деление доменной модели на агрегаты.

Хочется надеяться, что наш опыт окажется для кого-то полезным, поможет сэкономить время и избежать граблей. Спасибо за внимание.

ссылка на оригинал статьи https://habr.com/ru/company/arcadia/blog/509426/

Как ультрафиолет запускает фотолиз прямо в вашей коже

image

На картинке — водитель-дальнобойщик, который 28 лет водил фуры по просторам США. Стекло было закрыто, кондиционер работал. Вот только ультрафиолет UVA-спектра прекрасно проникает сквозь него и вызывает повреждения кожи и фотостарение. Научную публикацию по его случаю можно посмотреть тут. Ультрафиолет запускает кучу неприятных реакций в организме и рвёт на куски ДНК. Это явление называется фотолизом.

Загар — штука симпатичная, но он всегда патология и способ защититься от повреждения. Вот раньше была отличная, на мой взгляд, мода на бледность и зонтики от солнца. Сейчас же все старательно загорают на пляже и в солярии. Поэтому, если вы не хотите выглядеть в 30 лет как пожилой крестьянин с рисовых полей, — надо обязательно защищать кожу специальными SPF-составами. Sun Protection Factor на этикетке показывает, насколько долго вы можете пробыть на солнце с этим средством. Например, если SPF 50+, а вы выгораете до состояния томата через десять минут, то с ним вы сможете продержаться 10 * 50 минут, то есть почти восемь часов.

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

Ура, карантин снимают

Вроде как официальные рейтинги начали выглядеть совсем красиво, а карантин временные ограничения повсеместно снимают. Все радостные, синевато-белые, срочно бегут на море. Мои друзья из Краснодара рассказывали про пробки посреди ночи, когда разрешили покидать территорию города.

Но если жители Краснодарского края, Симферополя и других жарких регионов за много лет выучили правила выживания в южном климате, то гости из северных регионов традиционно страдают. Классическая картина — пунцово-малиновые тела равномерной прожарки, лежащие в полдень на пляже. Отпуск короткий, загореть надо по максимуму. Заканчивается всё потом попытками намазаться странными субстанциями вроде сметаны, кефира или ещё какого-то кумыса. А ещё линькой в стиле сетчатого питона спустя несколько дней.

Особенности ультрафиолета

В чём же коварство ультрафиолетового излучения? Давайте возьмём двух людей и начнём аккуратно прожаривать их излучением на разной длине волны.

На первого человека направим мощный инфракрасный прожектор. Сначала ему станет тепло, а потом он начнёт ругаться и пытаться уйти, потому что жжётся. Инфракрасное излучение регистрируется нашими рецепторами сразу же и имеет быстро проявляющиеся эффекты. Видели людей, которые просидели долгое время в сухой сауне? Вот примерно такая же эритема (покраснение) будет при воздействии инфракрасных лучей. Быстро возникает и быстро проходит в течение 30–60 минут. Если жарить долго и регулярно, что характерно, например, для дачников, то на коже будет появляться некрасивая бурая пигментация пятнистого вида.

На второго человека направим ультрафиолетовый источник. Если его мощность будет не слишком велика, то в лучшем случае он ощутит лёгкое покалывание и раздражение. Это часто бывает в маломощных соляриях. Эритемы нет, видимых изменений нет. Вроде всё хорошо. Самое неприятное начинается спустя 6–12 часов, когда кожа в ответ на скрытое повреждение начинает выбрасывать медиатор воспаления гистамин. Появляется довольно болезненная ультрафиолетовая эритема. Кожа при этом красная, отёчная и болезненная в течение трёх-пяти дней. Затем она бледнеет и либо просто темнеет за счёт синтеза меланина меланоцитами, либо начинает шелушиться и отслаиваться в результате значительного повреждения.

Чуть-чуть физики и фотохимии

Фотоны с длиной волны в районе ультрафиолета уже несут достаточное количество энергии, чтобы инициировать химические реакции. Собственно, именно поэтому они относятся к ионизирующему излучению. Чем короче длина волны, тем больше энергии несёт фотон, в полном соответствии с формулой «E = hv».

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

image
Ультрафиолетовый мутагенез

А ещё фотон может прицельно попасть в ценную белковую молекулу или нить ДНК, вызвав их повреждение. Если очень не повезёт, можно получить одну из разновидностей рака кожи, например, меланому.

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

Дозы, не вызывающие видимого повреждения, называют субэритемными, то есть не провоцирующими покраснения. В идеальном варианте вы не должны покраснеть после посещения пляжа. Тогда можно говорить о полезных эффектах ультрафиолета вроде синтеза витамина D и иммуностимулирующем эффекте.

Есть несколько вариантов классификации участков спектра в ультрафиолетовой зоне. Если не брать в расчёт экстремальный ультрафиолет (EUV), то актуальными для нас являются три диапазона:

  1. UVA. Длина волны 400–315 нм. Самая мягкая часть ультрафиолета. Энергия минимальная, проходит атмосферу без проблем.
  2. UVB. Длина волны 315–280 нм. По большей части поглощается озоновым слоем атмосферы и до поверхности почти не долетает.
  3. UVC. Длина волны 280–100 нм. Жёсткий ультрафиолет, полностью поглощается атмосферой. Встречается только в искусственных источниках вроде кварцевых ламп.

UVC, очень жёсткий, на улице не встречается. Используется для убивания микробиологической живности в лабораториях, больницах и других помещениях. Ожоги вызывает сильные, но поверхностные, так как почти полностью поглощается эпидермисом и не проникает вглубь. У коллеги в практике был случай, когда пациент в больнице ночью пошёл в туалет и нашёл замечательный синий ночник. Решил, что это довольно удобно, и оставил его включенным. Утром всех быстро отправили в ожоговое отделение. Всё что торчало из-под одеяла было ярко-красного цвета. Всё обошлось, но с тех пор кнопку включения закрыли дополнительной крышкой от особо любознательных.

С точки зрения повседневного повреждения кожи нас больше интересует диапазон UVA и UVB. UVB сразу просто и понятно сделает вас пунцовым томатом. До росткового слоя — базальноклеточного — доходит примерно 15 % излучения. UVA мягкий, эритему почти не вызывает. Но он коварен тем, что неплохо проходит сквозь оконное стекло и проникает вглубь кожи. До 68 % излучения проходит ростковый слой и поглощается дермой.

Именно поэтому UVB нас интересует с точки зрения острых повреждений кожи, а UVA — хронических, ведущих к повреждению фибробластов и ростковой зоны кожи. Результат такого воздействия — преждевременное фотостарение. Это очень хорошо заметно при взгляде на кожу людей, работающих большую часть времени на открытом воздухе.

Защищаем кожу

Thomas Leveritt, английский художник, смог наглядно продемонстрировать, что происходит с нами из-за ультрафиолетовых лучей. Он использовал крайне дорогие объективы, прозрачные для ультрафиолета, и специальную камеру. На видео очень хорошо видна разница в структуре кожи детей и взрослых, чья кожа несёт следы многочисленных микроповреждений от солнечного излучения.

image
Лысая мыша породы Skh:HR-1. Тоже надо мазать кремом от загара

У нас есть несколько стратегий защиты. Мы можем отразить ультрафиолет, можем поглотить его и дополнительно защитить кожу антиоксидантами. Голая мышь — классический модельный механизм для тестирования подобных систем защиты. Скажем, одно из исследований показало, что альфа-токоферол и аскорбиновая кислота снижали риски патологических изменений кожи и образования опухолей у мышей.

Мы провели кучу исследований, после которых наша команда опытных химиков смогла подготовить оптимальную защитную формулу. В результате получился препарат «Mультипротектор SPF50+ oil free». Сейчас расскажу, что мы туда добавили и зачем там столько компонентов одновременно.

Первый компонент — наночастицы диоксида титана. Они белые. Нет, даже не так, они очень белые. Собственно, если будете покупать краску для потолка, не берите с оксидом цинка — она сероватая. Самые белые краски с диоксидом титана. Ультрафиолет он тоже отлично отражает и рассеивает. Заодно с ним не так жарко, так как видимое и инфракрасное излучение он тоже отражает. К сожалению, мало кто захочет выглядеть как мим с белоснежным лицом. Поэтому его количество в составе строго лимитировано.

image
Пример поглощения ультрафиолета защитным кремом

Дальше идут компоненты, которые поглощают ультрафиолет, не позволяя ему достигнуть кожи. Здесь уже мы подобрали сложную композицию, которую редко используют в дешёвых защитных кремах:

  1. Этилгексил метоксициннамат: органический UVB-фильтр с высоким профилем безопасности, не вызывающий образование комедонов.
  2. Гомосалат: безопасный УФ-фильтр, поглощающий UVB-лучи.
  3. Диэтиламино гидроксибензоил гексил бензоат: фотостабильный фильтр, поглощающий UVA-лучи и обеспечивающий эффективную защиту от свободных радикалов и фотостарения.
  4. Tinosorb M: фотостабильный УФ-фильтр в виде органических мелкодисперсных частиц, обеспечивающий эффективную защиту от широкого спектра ультрафиолетовых лучей. Данный фильтр охватывает значительную часть диапазона излучения — от длинноволнового ультрафиолета UVA и средневолнового UVB, которые вызывают пигментацию и солнечные ожоги, до видимого света (HEV).
  5. Этилгексил триазон: фотостабильный UVB-фильтр.

Зачем столько компонентов? Проблема в том, что вы не можете просто взять и плеснуть побольше какого-то вещества. Может возникнуть побочный эффект в виде раздражения или образования комедонов. При правильно подобранной формуле вы получите одновременно широкое перекрытие всего спектра и отсутствие негативного эффекта компонентов.

А ещё мы добавили в состав фукогель. Это интересная биосахаридная смола. Мы используем её, чтобы кормить добрых бактерий и для глубокого увлажнения кожи. В итоге получаем нормальную здоровую микрофлору и восстановление защитных барьеров кожи.

Пора на море

Море — это отлично. Просто помните, что загар в принципе попытка организма защититься. Он красивый, но с точки зрения здоровья быть белым полезнее. Тем более, что тот же витамин D можно спокойно получать в виде капсул, а не жуткого рыбьего жира в бутылке.

Если уж планируете загорать, то всегда ограничивайте время на пляже. Старайтесь не появляться на солнце с 10 до 16, чтобы не подвергать кожу лишней опасности. Субэритемные дозы, когда кожа не покраснела, — лучший вариант. Такой загар может быть менее интенсивным, но продержится дольше. Как придёте домой — вымойтесь в душе, чтобы не оставлять защитный крем. После этого нанесите что-нибудь из увлажняющих составов. Можно наш «Гель увлажняющий +». Он относительно недорогой и тоже с фукогелем для микрофлоры.

Если чувствуете, что конец ваш близок, а кожа имеет нежный багровый оттенок, то я бы не советовала мазать сметану или тем более подсолнечное масло. При ожогах любые жиры противопоказаны. Лучше холодные компрессы и гидрофильные гели с лёгкими текстурами. Если найдёте что-то вроде нашего «Интенсив-регенерация» (вот пост на Хабре про разработку), будет совсем идеально. Но бюджетным его уже не назовёшь — производство сложное и требует дорогих и особо чистых компонентов.

Минутка беззастенчивой рекламы. Мы сделали читателям Хабра скидку 10 % на Озоне на всю нашу косметику — промокод HABR10. И да, я прямо мечтала сказать что-то такое пару лет назад, когда проблемой было найти что-то нашей лаборатории кроме Блефарогеля иначе как на производстве. Ну и заглядывайте к нам в телеграм-канал (@geltek_cosmetics). Там про хроники нашей уютной лаборатории.

ссылка на оригинал статьи https://habr.com/ru/company/geltek/blog/509764/

Я не понимаю, что хочу. Как пользователю сформулировать требования к CRM

«Когда кто-то трогает крестик, должен плакать персиковый медвежонок»*, — это, пожалуй, самое милое требование из тех, что мне приходилось встречать (но, к счастью, не реализовывать). Оно было сформулировано сотрудницей с 12 годами опыта работы в одной компании. Вы поняли, что ей нужно (ответ в конце)? Уверенное второе место занимает это: «Биллинг должен запускаться по моему желанию, желание выражается на мобильнике»**.

Действительно, далёкие от IT пользователи зачастую не могут сформулировать свои требования и ведут себя с разработчиками довольно странно. Поэтому мы решили написать доступную для всех статью: простым пользователям и неайтишному бизнесу она поможет легко сформулировать требования, ну а нам, айтишникам, тема на обсудить и поделиться опытом.


Пользователь избегает ответственности за требования


Если посмотреть те запросы, которые люди пишут о CRM в социальных сетях или в профильных сообществах, есть чему удивиться. Множество возмущённых постов о том, что нельзя найти CRM для длинных продаж, дистрибуции машинного масла, агентства наружной рекламы и т.д. И если человек занимается оптовыми продажами сена, то они ищут именно CRM Seno version и никак иначе. А вот в процессе общения с вендором такие запросы как-то сразу отпадают, потому что человек, выбирающий CRM, погружается в тему и понимает, что современные CRM-системы способны решить задачи практически любого бизнеса — дело не в отраслевой версии, а в настройках и индивидуальных доработках. 

Так откуда возникают неадекватные требования?

  • Главная причина — непонимание сущности CRM-системы как технологии. Любая современная CRM-система в своей основе имеет множество разных таблиц, которые между собой связаны ключевыми полями с определёнными значениями (кто не знаком с СУБД, но когда-то работал с MS Access, легко вспомнит эту визуализацию). Поверх этих таблиц строится интерфейс: десктопный или в вебе, без разницы. Работая с интерфейсом, вы фактически работаете с теми самыми таблицами. Как правило, задачи абсолютно любого бизнеса можно решить с помощью настройки интерфейса, создания новых объектов и новых связей в совокупности с одновременным обеспечением логики их взаимодействия. (доработка). 

    Да, бывает, что сфера деятельности компании требует какие-то особенные решения: медицина, строительство, недвижимость, инженерное дело. Для них существуют свои профильные решения (например, RegionSoft CRM Media для телерадиохолдингов и операторов наружной рекламы — там особенным образом реализованы и медиапланирование, и работа с монтажными листами и эфирными справками, и управление местами размещения рекламы). 

    Но в целом компании малого бизнеса могут использовать CRM-систему даже без доработок и покрывать все нужды оперативной работы. Именно потому что CRM создаётся как универсальное решение для автоматизации бизнеса. А то, насколько она будет эффективна для вашей компании, зависит от того, как она будет настроена и наполнена данными (например, в RegionSoft CRM есть несколько классных инструментов, которые можно адаптировать точно под нужды конкретного бизнеса и даже его отделов: редактор бизнес-процессов, настраиваемый калькулятор для построения расчётов параметров продукции, механизм для настройки сложных KPI — и это подходящие механизмы для любой компании).

  • Представитель бизнеса о CRM знает от других, мнение основано на чужом негативном опыте. Он полагает, что и с ним случится что-то подобное, не подозревая, что его знакомый никогда не скажет «я не разобрался в CRM» или «я зажал деньги на внедрение и обучение, а теперь страдаю», нет, он обвинит разработчика или вендора «втюхали мне эту CRM», «продали и в кусты» и т.д. Такие ребята нередко решают, что вендор должен тратить часы времени сотрудников совершенно бесплатно (не могу понять, почему они же не требуют бесплатного обслуживания и ежедневной мойки автомобиля от завода-изготовителя или дилера, а спокойно оплачивают стоимость ТО официального дилера.
  • Потенциальные клиенты считают, что раз на рынке есть кто-то, кто предлагает CRM бесплатно (с кучей ограничений и звёздочек), то и остальные должны просто раздавать CRM-системы. Бесплатные CRM в Яндексе ищут около 4000 человек ежемесячно. На что они надеются — непонятно, ведь по сути любая бесплатная CRM, если она рассчитана более чем на одного человека, всего лишь урезанная демо-версия и маркетинговый инструмент.

Есть и другие причины, но эти три идут с большим отрывом. С такими клиентами работать довольно непросто, поскольку у них уже есть сформированный образ идеальной по их мнению CRM и они нередко ждут ответа на свой вопрос типа: «Нет, вы мне дадите CRM для продаж холодильного торгового оборудования марки Север или мне звонить в Германию и заказывать SAP?». При этом бюджета на внедрение CRM хватит разве что на звонок в эту самую Германию. Звучит немного зло, но на самом деле — идти с ультиматумом к разработчикам CRM гораздо менее продуктивно, чем обсуждать требования и прислушиваться к опытным внедренцам. 

Как формулировать требования?


Функциональные требования


Определите, что вам нужно улучшить в компании — это будет вашим ключевым требованием к CRM-системе. Можно выделить четыре наиболее распространенные задачи, для решения которых компании задумываются о покупке CRM. 

  1. Повышение эффективности работы. Если команда продаж в первую очередь и вся компания в целом увязла в администрировании, упускает важные события и теряет клиентов, забывает выполнить задачи в срок, то необходима помощь программы в управлении временем и задачами. А это значит, что среди ваших первых требований должны быть классная карточка клиента, разнообразные планировщики и возможность быстро собирать информацию по клиентам в единой базе. Довольно стандартные требования. На этом этапе можно предъявить дополнительное требование — автоматизацию бизнес-процессов, которая упорядочивает рутину в бизнесе любого размера. 
  2. Повышение объёма продаж. Если вам нужно больше продаж, особенно в кризис, который тут кружит над нашими уже седыми от нервов головами, то у вас есть ключевые подзадачи: сбор полной информации о клиенте, сегментация и персонализация обращений к клиентам, быстрая работа с оформлением сделки и информативная воронка продаж. Это всё тоже есть в стандартных CRM-системах.
  3. Отслеживание эффективности работников (не путать с контролем времени сотрудников, мы на этом поле не играем!). Вот здесь всё уже интереснее. Найти CRM, которая решит две предыдущие задачи, очень просто, найти CRM  с KPI сильно сложнее, найти CRM с настоящим, многокритериальным, аналитическим механизмом KPI совсем непросто (если ищете, то у нас есть RegionSoft CRM Professional 7.0 и выше, а в ней KPI). Если в выбранной вами CRM-системе нет системы KPI, вы можете попросить такую доработку, однако она скорее всего будет довольно дорогой, потому что это практически отдельный модуль для любого ПО.
  4. Безопасность. На первый взгляд, CRM не относится к инструментам обеспечения корпоративной безопасности. Но автоматизация без управления безопасностью выглядит несостоятельно. Нередко к выбору CRM приводит желание руководителя избавиться от серых схем, откатов и «своих персональных» клиентов у продажников. CRM-система хранит данные, сохраняет клиентскую базу от попыток копирования и передачи третьим лицам, благодаря разделению прав доступа помогает контролировать круг клиентов и компетенций каждого сотрудника. И заметьте — вы контролируете и делаете безопасной непосредственно рабочую деятельность, а не время сотрудников на работе. 

Как правило, требования формулируются не по одной из перечисленных задач, а по нескольким. Это справедливо: раз современная CRM давно стала CRM++, почему бы не использовать её возможности не только для отдела продаж, но и для всей компании сразу. Например, календарём, телефонией, планировщиками, записями о клиентах и бизнес-процессами могут пользоваться все сотрудники компании. Как итог — вся команда собрана в одном интерфейсе. Оптимальный путь, особенно сейчас, в условиях удалённой и частично удалённой работы. 

Перечисляя те функции, которые вам необходимы и примеряя их к реальным процессам в компании, вы формулируете функциональные требования к CRM. Ими дело не ограничивается.

Дополнительные требования к CRM


У малого бизнеса сегодня такая ситуация, что эти самые дополнительные требования обретают первостепенное значение, потому что CRM заработает не сразу, а вот заплатить нужно здесь и сейчас, интегрировать рабочие сервисы нужно сразу, обучить сотрудников немедленно. В целом всё это сводится к затратам. 

Как оценивать стоимость CRM?


У нас была большая статья о том, сколько стоит CRM, но там изложен универсальный подход, который можно применить и для ИП на 3 человек, и для оператора связи на 1500 сотрудников. Для малого бизнеса ситуация выглядит несколько иначе — и тем более мы призываем вас посмотреть на неё иначе в условиях нынешнего кризиса. 

Итак, вам нужна CRM и у вас в компании 10 сотрудников, каждого из которых вы хотите подключить к единому информационному ресурсу компании — пусть к RegionSoft CRM Professional (мы не имеет права рассматривать чужие решения).

Если вы решите купить CRM, то заплатите за все лицензии один раз 134 700 рублей (по состоянию на июль 2020 года). Это, с одной стороны, оптимальный путь: заплатил и забыл, эти 134.7 тыс. не прирастут ни через год, ни через три. Если вы, например, арендуете облачную CRM, то в первый месяц вы заплатите всего 9000 рублей, но через год это будет уже 108 000, через два — 216 000, через три — 324 000 (и то, если обойдётся без ежегодной индексации цен).

Но! Мы знаем, что у бизнеса сейчас может не быть 134 700, а CRM в кризис нужна больше чем когда-либо. Поэтому у нас есть рассрочка — 26 940 в месяц и аренда — 11 233 в месяц с правом выкупа. При этом вы получаете не какой-то уменьшенный пакет функций, а всё ту же мощную редакцию.

Мы сделали эту выкладку далеко не только ради рекламы. Если вы приходите к вендору, стоит правильно формулировать ценовые требования. 

  • Не просите бесплатную версию — вы по факту продадите её сами себе (потому что это бесплатно) и попадёте на маркетинговый крючок: в итоге всё равно купите, но вас немного задолбают общением, а вы потом задолбаетесь из-за ограничений функциональности.
  • Если вы не готовы оплатить год аренды или всю стоимость решения on-premise, обсуждайте возможность рассрочки и дискретных платежей.
  • Никогда не заказывайте доработку сразу, если вы не уверены, что функция понадобится прямо сейчас и её нет в CRM. Лучше начните использовать CRM-систему и постепенно сформулируйте, что вам необходимо доработать и как эта доработка будет использоваться в компании.
  • Уточните у вендора, какие дополнительные затраты обязательны: у кого-то это платный внешний почтовый клиент, обязательное подключение к единственному оператору IP-телефонии, пакет технической поддержки и проч. Эти затраты могут стать внезапным и неприятным сюрпризом.
  • Узнайте стоимость внедрения и обучения — в 90% случаев это оправданные расходы, которые окупаются благодаря быстрому и правильному старту работы в CRM-системе.

И помните: деньги не должны быть единственным требованием! Если вы будете ориентироваться только на стоимость программы, то скорее всего просто не сможете выбрать то решение, которое нужно бизнесу.

Итак, мы с вами разобрались с двумя самыми важными требованиями: функциональностью CRM-системы и с деньгами, которые предстоит за неё заплатить. 

Какие ещё требования могут быть к  CRM?

  • Нагрузка на CRM-систему. Расскажите вендору, какое количество информации планируется ежедневно вносить в базу, как она должна храниться и какие резервные копии иметь. Для большинства современных CRM это по-прежнему принципиальный момент, который может сказаться на скорости работы, стоимости, модели поставки и т.д.
  • Возможные настройки. Обговорите заранее, какие настройки для вас особенно важны. Это может быть воронка продаж, почтовый клиент, рассылки, обязательно — распределение прав доступа и т.д. Как правило, тут пожелания бывают самые специфические.
  • Совместимость с существующей инфраструктурой. Уточните, какие интеграции возможны, как организована телефония, какое серверное оборудование требуется и требуется ли (для десктопных CRM-систем). Посмотрите, с каким ПО из вашего зоопарка пересекается CRM и откажитесь от него для экономии и наведения порядка в делах.
  • Безопасность. Если у вас есть особые требования к безопасности, обговорите их отдельно, поскольку не все они могут быть выполнены для некоторых типов поставки ПО. Уточните сроки и периодичность создания бэкапов, а также уточните, платная эта услуга или нет.
  • Техническая поддержка. Мы рекомендуем у всех поставщиков CRM на первый год покупать пакет платной приоритетной поддержки — так вам будет гораздо спокойнее. В любом случае, убедитесь, что техническая поддержка есть и уточните границы её оказания.
  • Облако или десктоп. Вечный спор из разряда Apple vs Samsung, Canon vs Nikon, Linux vs Windows. Если коротко, то десктоп в конечном итоге дешевле,  местами безопаснее и быстрее в работе, лицензии принадлежат вам и не пропадут вместе с вендором. Облако удобнее для молодых, начинающих команд, когда не требуется персональное внедрение или доработка. Масштабируемость у обоих типов поставки CRM одинаковая. 

Главные ошибки пользователей при описании требований

  • Упираться в мелочи. Как правило, почти любую мелочь можно настроить, гораздо важнее обратить внимание на то, как CRM согласуется с вашими бизнес-процессами. Если вы считаете самым важным в CRM дашборд с данными или возможность заменить лого разработчика на своё (кстати, в RegionSoft CRM — легко), пообщайтесь с коллегами — они помогут вам собрать требования, весьма красочно описав все недостатки своих бизнес-процессов.  
  • Превращать требования к ПО в список покупок. Вы внимательно читаете все отзывы, социальные сети, Хабр, другие порталы, смотрите демо-версии всех CRM-систем и методично записываете всё, что вас хоть как-то заинтересовало, а затем весь этот длинный список вываливаете на наиболее подходящего вендора. А он, бедный, не понимает, почему должен разработать корпоративный портал, систему управления претензиями, модуль бухучёта и систему контроля трафика и документооборота для небольшой торговой компании в одном флаконе.

Выбирайте только то, что вам реально нужно и с чем вы можете работать. Потому что мы для вас при определённом уровне оплаты можем и экраноплан спроектировать, но а) это будет дорого; б) зачем он вам? В общем, выбирайте CRM-систему для нормальной рабочей жизни, а не для любования набором модулей и возможностей — это может просто не окупиться.

  • Включать в требования фантазии и желания. Указывайте в требованиях то, что вы реально хотите сделать в бизнесе и будете использовать; задачи, поставленные в вакууме и в отрыве от реальности, причинят вред: вы убьёте время на их обсуждение и не получите результат.
  • Говорить с вендором, как с роботом. Если вы общаетесь непосредственно с разработчиком CRM (а не с партнёрской сетью), то знайте: мы не только программисты и инженеры, мы прежде всего такой же бизнес, как и вы. Поэтому расскажите о ваших проблемах, мы их отлично поймём и подскажем, как CRM эти проблемы решит. Мы не просто поставщики решений, в большинстве случаев мы сочетаем рассказ о CRM с разбором проблем вашего бизнеса. Поэтому говорите с разработчиками на обычном, человеческом языке. Скажите, почему вам вдруг стала интересна CRM-система и мы объясним вам, как её внедрить лучшим образом.
  • Быть негибким и упрямым в каждой формулировке. Обращайте внимание на то, как вендор предлагает решать ваши задачи — у него уже есть опыт работы в сотнях проектах автоматизации и его инженеры нередко предлагают наиболее эффективное решение из всех возможных. Например, клиент может настаивать на требовании нотации BPMN 2.0 для описания процессов (потому что её хорошо «продавали» на конференции для CIO) и не признавать альтернатив, а потом попробовать удобный нативный редактор бизнес-процессов и убедиться, что с ним ВСЕ его сотрудники смогут справиться с бизнес-процессами. Выбирать удобные и практичные, а не модные и дорогие решения — идеальная практика для малого бизнеса, который тратит на автоматизацию свои деньги, а не бездонный бюджет корпорации.
  • Говорить о CRM в целом, а не о конкретной системе. Во время общения с вендором говорите именно о его CRM-системе, запросите подробную презентацию, задавайте подробные вопросы по существу. Так вы сможете понять, какие задачи вашего бизнеса сможет решить эта конкретная CRM-система.

Хорошо спланированный сбор требований — залог успеха в выборе CRM-системы. Если вы приравняете требования к «хотелкам» и «советам друга», вы получите плохо приспособленную для вашего бизнеса CRM-систему, которая и ресурсы высосет, и ощутимой выгоды не принесёт. Каждый проект внедрения это трудозатраты и ресурсы с обеих сторон, поэтому с внедренцами лучше быть честными, чтобы не загубить весь проект в самом начале. Ваш большой друг — разработчик CRM, которому, между прочим, не выгодно предлагать свой софт под любые требования. Ему важно, чтобы вы успешно работали в системе, а не просто купили её. Во всяком случае, это важно для нас. Давайте дружить!


И напоследок, простой способ определить, удалось ли внедрение CRM: если вы используете CRM и скорость бизнес-процессов выросла, внедрение проведено правильно и ваш бизнес стал эффективнее.



(осторожно, 77 МБ)

Расшифровка требований из вступления

*«Когда кто-то трогает крестик, должен плакать персиковый медвежонок» — нужно было прикрутить «донт лив попап» — картинку со скидкой, которая всплывала бы при попытке закрыть страницу. Плачущий медвежонок казался самым убедительным зверем.

**«Биллинг должен запускаться по моему желанию, желание выражается на мобильнике» — биллинг должен запускаться вручную сотрудником АСУ после получения от сотрудника коммерции СМС о завершении расчётов с партнёрами.

ссылка на оригинал статьи https://habr.com/ru/company/regionsoft/blog/509884/