🚀 | Durable Entities Walkthrough



Добро пожаловать в День 6 из #30DaysOfServerless!

Сегодня у нас есть специальная подборка постов из нашей инициативы Zero To Hero 🚀, в которой представлены сообщения из блогов наших команд разработчиков продуктов для #ServerlessSeptember. Посты были первоначально опубликованы в блоге Apps on Azure на Microsoft Tech Community.


О чем мы расскажем

  • Что такое долговечные сущности
  • Некоторая предыстория
  • Модель программирования
  • Сущности для платформы микроблогов

Durable Entities — это особый тип Azure Function, который позволяет реализовать объекты с состоянием в бессерверной среде. Они упрощают внедрение stateful компонентов в ваше приложение без необходимости вручную сохранять данные во внешнем хранилище, чтобы вы могли сосредоточиться на бизнес-логике. В последнем разделе мы продемонстрируем их возможности на реальном примере.


Сущности 101: немного предыстории

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

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

Сущности являются частью расширения Durable Functions Extension, расширения Azure Functions, которое предоставляет программистам абстракции с учетом состояния для бессерверных функций, таких как оркестрации (т.е. рабочие процессы).

Durable Functions доступны в большинстве сред исполнения Azure Functions: .NET, Node.js, Python, PowerShell и Java (предварительная версия). В этой статье мы сосредоточимся на работе с C#, но отметим, что Entities также доступны в Node.js и Python; их доступность на других языках находится в процессе разработки.


Сущности 102: модель программирования

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

Если вы реализуете это объектно-ориентированным способом, вы, вероятно, определите класс (скажем, «Counter») с методом для получения текущего счета (скажем, «Counter.Get»), методом для добавления к счету (скажем, «Counter.Add») и методом для сброса счета (скажем, «Counter.Reset»). Что ж, реализация сущности в C# не сильно отличается от этого скетча:

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

    public void Add(int amount)  
    { 
        this.Value += amount; 
    } 

    public Task Reset()  
    { 
        this.Value = 0; 
        return Task.CompletedTask; 
    } 

    public Task<int> Get()  
    { 
        return Task.FromResult(this.Value); 
    } 
    [FunctionName(nameof(Counter))] 
    public static Task Run([EntityTrigger] IDurableEntityContext ctx) 
        => ctx.DispatchAsync<Counter>(); 
}  
Вход в полноэкранный режим Выход из полноэкранного режима

Мы определили класс с именем Counter, с внутренним счетом, хранящимся в переменной «Value», которым манипулируют через методы «Add» и «Reset», и который можно считать через «Get».

Метод «Run» является просто шаблоном, необходимым для взаимодействия фреймворка Azure Functions с объектом, который мы определили — это метод, который фреймворк вызывает внутри, когда ему нужно загрузить объект Entity. Когда вызывается DispatchAsync, сущность и соответствующее ей состояние (последнее число в «Value») загружается из хранилища. Опять же, это в основном просто кодовая таблица: бизнес-логика вашей сущности лежит в остальной части класса.

Наконец, аннотация Json поверх класса и поля Value сообщает фреймворку Durable Functions, что поле «Value» должно долго сохраняться как часть долговременного состояния при каждом вызове Entity. Если бы вы аннотировали другие переменные класса с помощью JsonProperty, они также стали бы частью управляемого состояния.

Сущности для платформы микроблогов

Мы попробуем реализовать простую платформу микроблогов, а-ля Twitter. Назовем ее «Chirper». В Chirper пользователи пишут сообщения (т.е. твиты), они могут следовать и не следовать за другими пользователями, а также читать сообщения пользователей, за которыми они следуют.

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

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

Ниже приведен потенциальный способ реализации пользователя для чирпера как сущности:

 [JsonObject(MemberSerialization = MemberSerialization.OptIn)] 
  public class User: IUser  
  { 
      [JsonProperty] 
      public List<string> FollowedUsers { get; set; }  = new List<string>(); 

      public void Add(string user) 
      { 
          FollowedUsers.Add(user); 
      } 

      public void Remove(string user) 
      { 
          FollowedUsers.Remove(user); 
      } 

      public Task<List<string>> Get() 
      { 
          return Task.FromResult(FollowedUsers); 
      } 
      // note: removed boilerplate “Run” method, for conciseness. 
  } 
Войти в полноэкранный режим Выйти из полноэкранного режима

В этом случае внутреннее состояние нашей сущности хранится в «FollowedUsers», который представляет собой массив аккаунтов, за которыми следит данный пользователь. Операции, открытые этой сущностью, позволяют клиентам читать и изменять эти данные: они могут быть прочитаны с помощью «Get», новый последователь может быть добавлен с помощью «Add», а пользователь может быть удален с помощью «Remove».

Таким образом, мы смоделировали пользователя Chirper как сущность! Напомним, что каждый экземпляр сущности имеет уникальный ID, так что мы можем считать, что этот уникальный ID соответствует конкретному аккаунту пользователя.

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

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

Ниже приведена простая реализация UserChirps:

[JsonObject(MemberSerialization = MemberSerialization.OptIn)] 
  public class UserChirps : IUserChirps 
  { 
      [JsonProperty] 
      public List<Chirp> Chirps { get; set; } = new List<Chirp>(); 

      public void Add(Chirp chirp) 
      { 
          Chirps.Add(chirp); 
      } 

      public void Remove(DateTime timestamp) 
      { 
          Chirps.RemoveAll(chirp => chirp.Timestamp == timestamp); 
      } 

      public Task<List<Chirp>> Get() 
      { 
          return Task.FromResult(Chirps); 
      } 

      // Omitted boilerplate “Run” function 
  } 
Вход в полноэкранный режим Выход из полноэкранного режима

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

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


Взаимодействие с Entity

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

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

Теперь, скажем, пользователь с заданным идентификатором (скажем, «durableFan99») хочет опубликовать сообщение. Для этого вы можете написать конечную точку HTTP, чтобы подать сигнал сущности UserChips для записи этого чириканья. Мы можем использовать функциональность HTTP Trigger из Azure Functions и использовать ее в паре с привязкой клиента сущности, которая сигнализирует об операции Add нашей сущности Chirp:

[FunctionName("UserChirpsPost")] 
public static async Task<HttpResponseMessage> UserChirpsPost( 
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = "user/{userId}/chirps")] 
    HttpRequestMessage req, 
    DurableClient] IDurableClient client, 
    ILogger log,  
    string userId) 
    { 
        Authenticate(req, userId); 
        var chirp = new Chirp() 
        { 
            UserId = userId, 
            Timestamp = DateTime.UtcNow, 
            Content = await req.Content.ReadAsStringAsync(), 
        }; 
        await client.SignalEntityAsync<IUserChirps>(userId, x => x.Add(chirp)); 
        return req.CreateResponse(HttpStatusCode.Accepted, chirp); 
    } 
Вход в полноэкранный режим Выход из полноэкранного режима

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

[FunctionName("UserChirpsGet")] 
public static async Task<HttpResponseMessage> UserChirpsGet( 
  [HttpTrigger(AuthorizationLevel.Function, "get", Route = "user/{userId}/chirps")] HttpRequestMessage req, 
  [DurableClient] IDurableClient client, 
  ILogger log, 
  string userId) 
  { 

      Authenticate(req, userId); 
      var target = new EntityId(nameof(UserChirps), userId); 
      var chirps = await client.ReadEntityStateAsync<UserChirps>(target); 
      return chirps.EntityExists 
            ? req.CreateResponse(HttpStatusCode.OK, chirps.EntityState.Chirps) 
            : req.CreateResponse(HttpStatusCode.NotFound); 
  } 
Войти в полноэкранный режим Выход из полноэкранного режима

Вот и все! Чтобы поиграть с полной реализацией Chirper, вы можете попробовать наш пример в репозитории расширения Durable Functions.

Спасибо!


Спасибо, что следили за нами, и мы надеемся, что вы найдете Entities столь же полезными, как и мы! Если у вас есть вопросы или отзывы, пожалуйста, напишите о проблемах в репо выше или отметьте нас @AzureFunctions в Twitter

Оцените статью
devanswers.ru
Добавить комментарий