Поделиться через


Устойчивые сущности

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

Замечание

Функции сущностей и связанные функции доступны в Durable Functions 2.0 и более поздних версий. Они поддерживаются в .NET местопроцессе, изолированном исполнителе .NET, JavaScript и Python, но не в PowerShell или Java.

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

Замечание

Устойчивые сущности поддерживаются в .NET и Python пакетах SDK для устойчивых задач. Java SDK не поддерживает сущности.

Общие концепции

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

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

Замечание

При вызове сущности она обрабатывает полезные данные до завершения, а затем планирует новое выполнение, активируемое при поступлении новых входных данных. В результате журналы выполнения сущностей могут показать дополнительное выполнение после каждого вызова. Ожидается.

Идентификатор сущности

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

  • Имя сущности, определяющее тип сущности. Например: Counter. Это имя соответствует имени функции сущности, реализующей сущность. Это не чувствительно к регистру.
  • Ключ сущности, который однозначно идентифицирует сущность среди всех других сущностей с одинаковым именем. Например, глобальный уникальный идентификатор (GUID).

Например, функцию Counter сущности можно использовать для ведения счёта в онлайн-игре. Каждый экземпляр игры имеет уникальный идентификатор сущности, например @Counter@Game1 и @Counter@Game2. Чтобы нацелить сущность, укажите его идентификатор сущности.

Операции с сущностями

Чтобы вызвать операцию для сущности, укажите:

  • Идентификатор сущности целевой сущности.
  • Имя операции, представляющее собой строку, указывающую операцию для выполнения. Например, сущность Counter может поддерживать операции add, get или reset.
  • Входные данные операции, которые являются необязательным параметром для операции. Например, операция add принимает целочисленное значение в качестве входных данных.
  • Запланированное время, которое является необязательным параметром для указания времени доставки операции. Например, запланируйте выполнение операции через несколько дней.

Операции могут возвращать значение результата или значение ошибки, включая ошибки JavaScript или исключение .NET. Вызывающая оркестрация получает результат или ошибку.

Операции могут возвращать значение результата или результат ошибки, такие как исключение .NET или исключение Python. Вызывающий объект получает результат или ошибку.

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

Определение сущностей

Используйте один из двух API для определения сущностей в .NET:

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

Синтаксис на основе классов: в синтаксисе на основе классов .NET классы и методы моделируют сущности и операции. Этот синтаксис делает код более читаемым и позволяет вызывать операции типобезопасным образом. Это тонкий слой на вершине синтаксиса на основе функций, поэтому можно смешивать оба варианта в одном приложении.

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

Пример на основе функции в рамках процесса:

В этом примере показана простая Counter сущность, реализованная как устойчивая функция. Он определяет три операции —addreset и get, которые используют целочисленное состояние.

[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
    switch (ctx.OperationName.ToLowerInvariant())
    {
        case "add":
            ctx.SetState(ctx.GetState<int>() + ctx.GetInput<int>());
            break;
        case "reset":
            ctx.SetState(0);
            break;
        case "get":
            ctx.Return(ctx.GetState<int>());
            break;
    }
}

Дополнительные сведения см. в синтаксисе на основе функций.

Пример класса, работающего в процессе:

В этом примере показана та же Counter сущность, реализованная с помощью классов и методов.

[JsonObject(MemberSerialization.OptIn)]
public class Counter
{
    [JsonProperty("value")]
    public int CurrentValue { get; set; }

    public void Add(int amount) => this.CurrentValue += amount;

    public void Reset() => this.CurrentValue = 0;

    public int Get() => this.CurrentValue;

    [FunctionName(nameof(Counter))]
    public static Task Run([EntityTrigger] IDurableEntityContext ctx)
        => ctx.DispatchAsync<Counter>();
}

Эта сущность сохраняет состояние в объекте Counter , который содержит текущее значение счетчика. Durable Functions сериализует и десериализирует этот объект с помощью библиотеки Json.NET.

Дополнительные сведения см. в разделе "Определение классов сущностей".

Пример функции изолированного рабочего процесса:

В следующем примере показана функция на основе сущности Counter в изолированном рабочем процессе. Он поддерживает add, resetgetи delete.

[Function(nameof(Counter))]
public static Task Counter([EntityTrigger] TaskEntityDispatcher dispatcher)
{
    return dispatcher.DispatchAsync(operation =>
    {
        if (operation.State.GetState(typeof(int)) is null)
        {
            operation.State.SetState(0);
        }

        switch (operation.Name.ToLowerInvariant())
        {
            case "add":
                int state = operation.State.GetState<int>();
                state += operation.GetInput<int>();
                operation.State.SetState(state);
                return new(state);
            case "reset":
                operation.State.SetState(0);
                break;
            case "get":
                return new(operation.State.GetState<int>());
            case "delete":
                operation.State.SetState(null);
                break;
        }

        return default;
    });
}

Пример на основе класса изолированного рабочего процесса:

В следующем примере показана реализация сущности Counter с помощью классов и методов.

public class Counter : TaskEntity<int>
{
    readonly ILogger logger;

    public Counter(ILogger<Counter> logger)
    {
        this.logger = logger;
    }

    public void Add(int amount) => this.State += amount;

    public void Reset() => this.State = 0;

    public int Get() => this.State;

    [Function(nameof(Counter))]
    public Task RunEntityAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
    {
        return dispatcher.DispatchAsync(this);
    }
}

Пакет SDK устойчивых задач для .NET поддерживает определение сущностей с помощью синтаксиса на основе классов. Вы можете реализовать базовый класс TaskEntity<TState>, чтобы определить свою сущность.

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

using Microsoft.DurableTask.Entities;

public class Counter : TaskEntity<int>
{
    public void Add(int amount) => this.State += amount;

    public void Reset() => this.State = 0;

    public int Get() => this.State;
}

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

builder.Services.AddDurableTaskWorker()
    .AddTasks(registry =>
    {
        registry.AddEntity<Counter>();
    })
    .UseDurableTaskScheduler(connectionString);

Для отправки сигнала или вызова сущности из оркестратора:

public class EntityOrchestration : TaskOrchestrator<string, int>
{
    public override async Task<int> RunAsync(TaskOrchestrationContext context, string entityKey)
    {
        var entityId = new EntityInstanceId(nameof(Counter), entityKey);

        // Signal the entity (fire-and-forget)
        await context.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);

        // Call the entity and wait for response
        int currentValue = await context.Entities.CallEntityAsync<int>(entityId, nameof(Counter.Get));

        return currentValue;
    }
}

Доступ к сущностям

Доступ к сущностям посредством односторонней или двусторонней связи.

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

Доступ к сущностям из клиентских функций, функций оркестратора или функций сущностей. Не каждый контекст поддерживает оба типа связи:

  • Клиентские функции поддерживают сущности сигнализации и чтение состояния сущности.
  • Функции Оркестратора поддерживают сущности для сигнализации и вызова.
  • Функции сущностей поддерживают сигнализирующие сущности.

Доступ к сущностям посредством односторонней или двусторонней связи.

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

Доступ к объектам из клиентских приложений или оркестраторов. Не каждый контекст поддерживает оба типа связи:

  • Клиенты поддерживают сигнальные сущности и чтение состояния сущности.
  • Оркестраторы поддерживают сигнализирующие и вызывающие объекты.

В следующих примерах показано, как получить доступ к сущностям.

Пример: клиент сигнализирует сущность

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

Замечание

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

В процессе:

В процессе:

[FunctionName("AddFromQueue")]
public static Task Run(
    [QueueTrigger("durable-function-trigger")] string input,
    [DurableClient] IDurableEntityClient client)
{
    // Entity operation input comes from the queue message content.
    var entityId = new EntityId(nameof(Counter), "myCounter");
    int amount = int.Parse(input);
    return client.SignalEntityAsync(entityId, "Add", amount);
}

Изолированный рабочий процесс:

[Function("AddFromQueue")]
public static Task Run(
    [QueueTrigger("durable-function-trigger")] string input,
    [DurableClient] DurableTaskClient client)
{
    // Entity operation input comes from the queue message content.
    var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
    int amount = int.Parse(input);
    return client.Entities.SignalEntityAsync(entityId, "Add", amount);
}

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

Чтобы получить доступ к сущностям из клиента, используйте DurableTaskClient для сигнала или чтения состояния сущности.

// Signal an entity
var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
await client.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);

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

Пример: клиент считывает состояние сущности

Запрос состояния сущности из клиентской функции:

В процессе:

[FunctionName("QueryCounter")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function)] HttpRequestMessage req,
    [DurableClient] IDurableEntityClient client)
{
    var entityId = new EntityId(nameof(Counter), "myCounter");
    EntityStateResponse<JObject> stateResponse = await client.ReadEntityStateAsync<JObject>(entityId);
    return req.CreateResponse(HttpStatusCode.OK, stateResponse.EntityState);
}

Изолированный рабочий процесс:

[Function("QueryCounter")]
public static async Task<HttpResponseData> Run(
    [HttpTrigger(AuthorizationLevel.Function)] HttpRequestData req,
    [DurableClient] DurableTaskClient client)
{
    var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
    EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);

    if (entity is null)
    {
        return req.CreateResponse(HttpStatusCode.NotFound);
    }

    HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
    await response.WriteAsJsonAsync(entity);

    return response;
}

Клиенты могут запрашивать состояние сущности:

var entityId = new EntityInstanceId(nameof(Counter), "myCounter");
EntityMetadata<int>? entity = await client.Entities.GetEntityAsync<int>(entityId);

if (entity != null)
{
    Console.WriteLine($"Current value: {entity.State}");
}

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

Пример: оркестрация сигналов и вызовов сущности

Функции Оркестратора могут получить доступ к сущностям с помощью API-интерфейсов в привязке триггера оркестрации. В следующем примере кода показана функция оркестратора, в которой функция оркестратора вызывает и посылает сигнал сущности Counter.

В процессе:

[FunctionName("CounterOrchestration")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var entityId = new EntityId(nameof(Counter), "myCounter");

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await context.CallEntityAsync<int>(entityId, "Get");
    if (currentValue < 10)
    {
        // One-way signal to the entity which updates the value - does not await a response
        context.SignalEntity(entityId, "Add", 1);
    }
}

Изолированный рабочий процесс:

[Function("CounterOrchestration")]
public static async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var entityId = new EntityInstanceId(nameof(Counter), "myCounter");

    // Two-way call to the entity which returns a value - awaits the response
    int currentValue = await context.Entities.CallEntityAsync<int>(entityId, "Get");

    if (currentValue < 10)
    {
        // One-way signal to the entity which updates the value - does not await a response
        await context.Entities.SignalEntityAsync(entityId, "Add", 1);
    }
}

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

Оркестраторы могут получить доступ к сущностям с помощью API сущностей контекста:

public class CounterOrchestration : TaskOrchestrator<string, int>
{
    public override async Task<int> RunAsync(TaskOrchestrationContext context, string entityKey)
    {
        var entityId = new EntityInstanceId(nameof(Counter), entityKey);

        // Two-way call to the entity which returns a value - awaits the response
        int currentValue = await context.Entities.CallEntityAsync<int>(entityId, nameof(Counter.Get));

        if (currentValue < 10)
        {
            // One-way signal to the entity - does not await a response
            await context.Entities.SignalEntityAsync(entityId, nameof(Counter.Add), 1);
        }

        return currentValue;
    }
}

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

Замечание

Вызов сущности из оркестратора схож с вызовом функции. Основное различие заключается в том, что сущности являются устойчивыми объектами с адресом (идентификатором сущности) и поддерживают указание имени операции. Действия без отслеживания состояния и не имеют концепции операций.

Пример: одна сущность сигнализирует другой сущности

Функция сущности может отправлять сигналы сущностям (в том числе сама себе) при выполнении любой операции. Например, измените предыдущий пример сущности Counter, чтобы отправить сигнал "достигнутое вехой" в сущность монитора, когда счетчик достигает 100.

В процессе:

   case "add":
        var currentValue = ctx.GetState<int>();
        var amount = ctx.GetInput<int>();
        if (currentValue < 100 && currentValue + amount >= 100)
        {
            ctx.SignalEntity(new EntityId("MonitorEntity", ""), "milestone-reached", ctx.EntityKey);
        }

        ctx.SetState(currentValue + amount);
        break;

Изолированный рабочий процесс:

case "add":
    var currentValue = operation.State.GetState<int>();
    var amount = operation.GetInput<int>();
    if (currentValue < 100 && currentValue + amount >= 100)
    {
        operation.Context.SignalEntity(new EntityInstanceId("MonitorEntity", ""), "milestone-reached", operation.Context.EntityInstanceId);
    }

    operation.State.SetState(currentValue + amount);
    break;

Координация сущностей

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

Пример: передача средств (C#)

Следующий пример кода перемещает средства между двумя сущностями счетов с помощью функции оркестрации. Для координации обновлений сущностей используйте LockAsync метод для создания критического раздела в оркестрации.

Замечание

Для простоты этот пример повторно использует сущность, определенную Counter ранее. В реальном приложении лучше определить более подробную BankAccount сущность.

// This is a method called by an orchestrator function
public static async Task<bool> TransferFundsAsync(
    string sourceId,
    string destinationId,
    int transferAmount,
    IDurableOrchestrationContext context)
{
    var sourceEntity = new EntityId(nameof(Counter), sourceId);
    var destinationEntity = new EntityId(nameof(Counter), destinationId);

    // Create a critical section to avoid race conditions.
    // No operations can be performed on either the source or
    // destination accounts until the locks are released.
    using (await context.LockAsync(sourceEntity, destinationEntity))
    {
        ICounter sourceProxy =
            context.CreateEntityProxy<ICounter>(sourceEntity);
        ICounter destinationProxy =
            context.CreateEntityProxy<ICounter>(destinationEntity);

        int sourceBalance = await sourceProxy.Get();

        if (sourceBalance >= transferAmount)
        {
            await sourceProxy.Add(-transferAmount);
            await destinationProxy.Add(transferAmount);

            // the transfer succeeded
            return true;
        }
        else
        {
            // the transfer failed due to insufficient funds
            return false;
        }
    }
}

В .NET LockAsync возвращает IDisposable. Удаление завершает критически важный раздел. Используйте его с блоком using для представления критического раздела.

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

Замечание

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

Поведение критической секции

Метод LockAsync создает критически важный раздел в оркестрации. Эти критические секции предотвращают внесение перекрывающихся изменений в указанный набор сущностей. LockAsync Данный API отправляет операции "блокировки" сущностям и возвращается после получения от каждой сущности ответа "блокировка получена". Блокировка и разблокировка являются встроенными операциями, поддерживаемыми всеми сущностями.

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

Замечание

Это поведение немного отличается от примитивов синхронизации, используемых в большинстве языков программирования, таких как lock оператор в C#. Например, в C# все потоки должны использовать lock инструкцию, чтобы обеспечить надлежащую синхронизацию. Однако для сущностей не требуется, чтобы все вызывающие объекты явно блокировали сущность. Если любой вызывающий объект блокирует сущность, все остальные операции с этой сущностью будут заблокированы и помещены в очередь после этой блокировки.

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

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

Правила критической секции

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

  • Критические секции не могут быть вложенными.
  • Критические секции не могут создавать подоркестрации.
  • Критические разделы могут вызывать только те сущности, которые они блокируют.
  • Критические секции не могут вызывать одну и ту же сущность с помощью нескольких параллельных вызовов.
  • Критические разделы могут сигнализировать только объектам вне набора блокировок.

Любые нарушения этих правил вызывают ошибку среды выполнения, например LockingRulesViolationException в .NET, которая содержит сообщение, объясняющее, что правило было нарушено.

Сравнение с виртуальными субъектами

Устойчивые сущности используют множество идей из модели субъекта. Если вы знакомы с акторами, вы можете распознать несколько концепций в этой статье. Устойчивые сущности похожи на виртуальные акторы, также называемые зернами, из проекта Orleans. Рассмотрим пример.

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

К ключевым различиям относятся:

  • Устойчивые сущности отдают приоритет устойчивости над задержкой, поэтому они могут не соответствовать приложениям со строгими требованиями к задержке.
  • Устойчивые сущности не вызывают истечения времени ожидания сообщений. В Orleans сообщения завершаются по таймауту после настраиваемого периода (по умолчанию — 30 секунд).
  • Сущности обеспечивают надежное и упорядочение сообщений. Орлеан поддерживает надежную, упорядоченную доставку для потоковых сообщений, но не гарантирует ее для всех сообщений между зернами.
  • Оркестрации — это единственное место, где можно использовать модель запрос-ответ с сущностями. Внутри сущности используйте односторонние сообщения (сигналирование), как и в исходной модели субъектов.
  • Устойчивые сущности не приводят к взаимоблокировке. Орлеан может войти в состояние взаимоблокировки, и взаимоблокировки сохраняются до тех пор, пока не истечет время ожидания сообщений.
  • Устойчивые сущности работают с надёжными оркестрациями и поддерживают распределенные механизмы блокировки.

Дальнейшие действия