如何使用开源的Cadence进行轮询?

94 阅读15分钟

如何使用开源的Cadence进行轮询

开源的Cadence对任何应用开发者来说都是一个有利的新工具。通过这些逐步的说明,了解如何使用该项目进行轮询。

本指南适用于希望了解Cadence(相对较新(完全开源)的容错有状态代码平台,最初由Uber开发(现在由其他公司支持,包括我们Instaclustr)中的投票工作的各层次的开发人员和工程师。本指南提供了 "Hello, World!"类型的例子,基于简单的场景和用例。

你将学到什么

如何在Instaclustr的管理服务平台上设置一个简单的Cadence轮询应用程序。

您需要什么

  • Instaclustr平台上的一个免费注册账户
  • 基本的Java 11和Gradle安装
  • IntelliJ社区版(或任何其他支持Gradle的IDE)
  • Docker(可选:仅在运行Cadence命令行客户端时需要)

那么,Cadence有什么了不起的地方?

大量的用例跨越了单一的请求-回复,需要跟踪复杂的状态,响应异步事件,并与外部不可靠的依赖关系沟通。构建此类应用的通常方法是无状态服务、数据库、Cron作业和队列系统的大杂烩。这对开发人员的工作效率产生了负面影响,因为大部分代码都是用于铺设管道--掩盖了无数低层次细节背后的实际业务逻辑。

Cadence是一个完全开源的协调框架,帮助开发人员编写容错的、长期运行的应用程序,也称为工作流。从本质上讲,它提供了一个持久的虚拟内存,与特定的进程相联系,并在各种主机和软件故障中保留完整的应用程序状态,包括函数栈,与本地变量。这允许你使用编程语言的全部功能来编写代码,而Cadence负责应用程序的耐久性、可用性和可扩展性。

什么是轮询?

轮询是执行一个周期性的动作来检查状态变化。例如,ping一个主机,调用一个REST API,或列出一个存储桶的新上传文件。 图 1: 轮询过程的流程图

应该尽可能避免轮询(而倾向于事件触发的中断),因为忙碌的等待通常会不必要地吃掉大量的CPU周期,除非是这样。

  1. 你只打算在很短的时间内进行轮询,或者
  2. 你可以在你的轮询循环中睡眠一段合理的时间。

对计算机来说,这相当于在长途旅行中每5分钟问一次你离目的地有多远。尽管如此,有些时候这也是唯一可用的选择。Cadence对持久性定时器、长期运行的活动和无限重试的支持使它成为一个很好的选择。

用Cadence轮询外部服务

有几种方法来实现轮询机制。我们将专注于轮询外部服务,以及在这样做时我们如何从Cadence中获益。

首先,让我们简单地解释一下Cadence的一些概念。Cadence的核心抽象是一个无故障的有状态工作流。这意味着工作流代码的状态,包括局部变量和它所创建的任何线程,对进程和Cadence服务故障是免疫的。这是一个非常强大的概念,因为它封装了状态、处理线程、持久计时器和事件处理程序。

为了满足确定性的执行要求,工作流不允许直接调用任何外部API。相反,他们协调了活动的执行。一个活动是一个业务层面的功能,实现应用逻辑,如调用服务或转码媒体文件。Cadence不会在失败的情况下恢复活动状态;因此,一个活动函数被允许包含任何代码而不受限制。

编写我们的轮询循环

代码本身非常简单--我们将逐行解释每件事的作用。

State polledState = externalServiceActivities.getState();   while(!expectedState.equals(polledState)) {  

  Workflow.sleep(Duration.ofSeconds(30));  

polledState = externalServiceActivities.getState();  

}  

我们已经达到了我们的预期状态!

我们从调用一个活动开始,在这种情况下,是一个外部服务,可能是一个REST API。然后我们有我们的条件,与图1中的钻石相匹配。如果还没有达到预期的状态,我们就安排一个10秒的睡眠。这不是任何形式的睡眠,而是一个持久的计时器。在这种情况下,我们的轮询等待的时间很短,但也可能更长,在这些情况下,如果你的执行失败,你不会希望等待整个间隔时间。

Cadence通过将计时器持久化为事件并在完成后提醒相应的工作者(托管工作流和活动实现的服务)来解决这个问题。这些计时器可以管理从秒到分钟、小时、天,甚至是月或年的时间间隔。

最后,我们通过再次调用我们的外部服务来刷新我们的状态。就这么简单!

在我们继续之前,让我们快速看一下Cadence在幕后实际做了什么,以避免潜在的问题。

重要: Cadence历史和轮询的注意事项

Cadence是如何实现无故障的有状态工作流的?秘密在于Cadence如何在工作流执行中坚持下去。工作流状态恢复利用了事件源,这对代码的编写方式有一些限制。事件源将状态持久化为一连串状态变化的事件。每当我们的工作流的状态发生变化时,一个新的事件就会被附加到它的历史事件中。然后,Cadence通过重放事件来重建工作流的当前状态。

这就是为什么所有与外部世界的通信都应该通过活动进行,并且必须使用Cadence的API来获取当前时间、睡眠和创建新线程。

为什么在轮询时要小心翼翼?

轮询需要一遍又一遍地循环一个条件。由于每个活动调用和定时器事件都是持久性的,你可以想象一个短的轮询间隔是如何导致一个巨大的时间线的。 让我们研究一下我们的轮询片段的历史会是什么样子:

  1. 我们首先调度轮询外部服务所需的活动。
  2. 这个活动是由一个工作者启动的。
  3. 该活动完成并返回其结果。
  4. 条件尚未满足,所以启动了一个定时器。
  5. 一旦时间过去,就会触发一个事件来唤醒工作流。
  6. 步骤1到5重复进行,直到条件得到满足。
  7. 最后的投票确认了条件的满足(不需要设置定时器)。
  8. 工作流程被标记为完成。 图2.我们的轮询片段代码的事件的Cadence历史

如果工作流在中间某个地方失败了,必须重放其历史,这可能会导致翻阅一个巨大的事件列表。有几种方法可以控制它:避免使用短的轮询期,在你的工作流上设置合理的超时,并将轮询限制在一定数量。

一句话: 记住所有的行动都是持久的,可能需要由工人重放。

设置活动重试

如果我们的外部服务因某种原因而失败,会发生什么?我们需要尝试、尝试、再尝试

我们简要地提到了Cadence是如何将活动用于一些可能意外失败的非决定性的地方,如消费API。这使得Cadence能够记录活动的结果,并能够无缝地恢复工作流,同时也增加了对重试逻辑等额外功能的支持。

下面是一个启用重试选项的活动配置的例子。

private final ExternalServiceActivities externalServiceActivities =    Workflow.newActivityStub(ExternalServiceActivities.class,   new ActivityOptions.Builder()  

 .setRetryOptions(new RetryOptions.Builder()   .setInitialInterval(Duration.ofSeconds(10))    .setMaximumAttempts(3)  

 .build())  

 .setScheduleToCloseTimeout(Duration.ofMinutes(5))    .build());  

通过这样做,我们告诉Cadence,ExternalServiceActivities 中出现的动作最多应该重试3次,每次尝试之间的间隔为10秒。这样一来,对外部服务活动的每次调用都将被透明地重试,而不需要编写任何重试逻辑。

用例:Instafood与MegaBurgers相遇

为了看到这种模式的作用,我们将在我们的样本项目上进行一次虚构的轮询集成。

Instafood简介

Instafood是一个基于在线应用的送餐服务。客户可以通过Instafood的移动应用程序从他们最喜欢的当地餐馆下单购买食物。订单可以是提货或送货。如果选择送餐,Instafood将组织他们众多的送餐司机之一从餐厅取走订单,并将其送到客户手中。Instafood为每家餐厅提供一个亭子/平板电脑,用于Instafood和餐厅之间的沟通。 当有订单时,Instafood会通知餐厅,然后餐厅可以接受订单,提供ETA,标记为已准备好,等等。对于外卖订单,Instafood会根据ETA值协调外卖司机来取货。

投票 "MegaBurgers"

MegaBurgers是一家大型的跨国快餐汉堡包连锁店。他们有一个现有的移动应用和网站,使用后端REST API让顾客下订单。Instafood和MegaBurgers达成了一项协议,Instafood的客户可以通过Instafood的应用程序下达MegaBurger的订单,以便提货和送货。双方同意,Instafood的后台订单工作流程系统将为MegaBurgers提供特殊服务,并直接与MegaBurgers现有的基于REST的订购系统整合,以下订单和接收更新,而不是在所有MegaBurger地点安装Instafood亭。 图3.MegaBurger的订单状态机

MegaBurger的REST API没有推送式机制(WebSockets、WebHooks等)来接收订单状态更新。相反,需要定期发出GET请求来确定订单状态,而这些投票的结果可能会导致订单工作流程在Instafood方面的进展(比如安排送货司机取货)。

设置Instafood项目

为了自己运行这个示例项目,你需要设置一个Cadence集群。在这个例子中,我们将使用Instaclustr平台来完成。

步骤1:创建Instaclustr管理的集群

一个Cadence集群需要一个Apache Cassandra集群来连接其持久层。 为了设置Cadence和Cassandra集群,我们将遵循 "创建Cadence集群"文档。

以下操作将自动为你处理:

  • 防火墙规则将自动在Cassandra集群上为Cadence节点配置。
  • Cadence和Cassandra之间的认证将被配置,包括客户端加密设置。
  • Cadence默认的和可见的密钥空间将在Cassandra中自动创建。
  • 两个集群之间将创建一个链接,确保你不会在Cadence之前意外地删除Cassandra集群。
  • 一个负载平衡器将被创建。建议使用负载平衡器的地址来连接到你的集群。

第2步:设置Cadence域

Cadence是由一个多租户服务支持的,其中隔离的单位被称为域。为了让我们的Instafood应用程序运行,我们首先需要为它注册一个域。

为了与我们的Cadence集群互动,我们需要安装它的命令行界面客户端。

macOS

如果使用macOS客户端,Cadence CLI可以用Homebrew安装,如下所示:

brew install cadence-workflow 

# run command line client  

cadence <command> <arguments> 

其他系统

如果没有,可以通过Docker Hub镜像使用CLIubercadence/cli :

# run command line client  

docker run --network=host --rm ubercadence/cli:master <command>  <arguments> 

在剩下的步骤中,我们将使用cadence来指代客户端。

2.为了连接,建议使用负载均衡器地址来连接到你的集群。这可以在连接信息标签的顶部找到,看起来像这样。

“ab-cd12ef23-45gh-4baf-ad99-df4xy-azba45bc0c8da111.elb.us-east 1.amazonaws.com” 

我们将其称为<cadence_host>

3.现在我们可以通过列出当前域来测试我们的连接。

cadence --ad <cadence_host>:7933 admin domain list 

4.添加instafood 域名。

cadence --ad <cadence_host>:7933 --do instafood domain register --global_domain=false 

5.检查它是否被相应地注册。

cadence --ad <cadence_host>:7933 --do instafood domain describe

第3步:运行Instafood示例项目

1.从Instafood项目的Git资源库中克隆Gradle项目。

2.打开instafood/src/main/resources/instafood.properties中的属性文件,用你的负载均衡器地址替换cadenceHost 值。

cadenceHost=<cadence_host>  

3.现在你可以通过以下方式运行该应用程序。

  cadence-cookbooks-instafood/instafood$ ./gradlew run 

或从你的IDE执行InstafoodApplication 主类。

4.通过查看其终端输出来检查它是否正在运行。

观察MegaBurger的API

在研究Instafood如何与MegaBurger整合之前,让我们先快速了解一下他们的API。

运行MegaBurger服务器

让我们从运行服务器开始。这可以通过运行以下程序来实现

cadence-cookbooks-instafood/megaburger$ ./gradlew run

MegaburgerRestApplication 从你的IDE。

这是一个简单的Spring Boot Rest API,有一个内存持久层,用于演示目的。当应用程序关闭时,所有数据都会丢失。

MegaBurger的订单API

MegaBurger公开了它的Orders API,以便跟踪和更新每个食品订单的状态。

POST /orders

这将创建一个订单并返回其ID。

请求:

curl -X POST localhost:8080/orders -H “Content-Type: application/json” --data ‘{“meal”: “Vegan Burger”, “quantity”: 1}’ 

响应:

{  

  “id”: 1,  

  “meal”: “Vegan Burger”, 

  “quantity”: 1,  

  “status”: “PENDING”, 

  “eta_minutes”: null 

}  

GET /orders

返回一个包含所有订单的列表。

请求:

curl -X GET localhost:8080/orders 

响应:

[  

{  

    “id”: 0,  

    “meal”: “Vegan Burger”, 

    “quantity”: 1,  

    “status”: “PENDING”, 

    “eta_minutes”: null 

},  

{  

    “id”: 1,  

    “meal”: “Onion Rings”, 

    “quantity”: 2,  

    “status”: “PENDING”, 

    “eta_minutes”: null  

}  

]

GET /orders / {orderId}。

这将返回id等于orderId的订单。

请求:

curl -X GET localhost:8080/orders/1 

响应:

{  

  “id”: 1,  

  “meal”: “Onion Rings”, 

  “quantity”: 2,  

  “status”: “PENDING”, 

  “eta_minutes”: null  

}  

PATCH /orders/{orderId}

更新id等于orderId的订单

请求:

curl -X PATCH localhost:8080/orders/1 -H “Content-Type: application/ json” --data ‘{“status”:“ACCEPTED”}’ 

响应:

{  

    “id”: 1,  

    “meal”: “Onion Rings”, 

    “quantity”: 2,  

    “status”: “ACCEPTED”, 

    “eta_minutes”: null  

}  

MegaBurger轮询集成回顾

现在我们已经设置好了一切,让我们看看Instafood和MegaBurger之间的实际整合。

投票工作流程

我们首先定义了一个新的工作流程,MegaBurgerOrderWorkflow

public interface MegaBurgerOrderWorkflow { 

@WorkflowMethod 

void orderFood(FoodOrder order); 

// ...  

}  

这个工作流程有一个orderFood 方法,它将通过与MegaBurger整合来发送和跟踪相应的FoodOrder

让我们来看看它的实现:

public class MegaBurgerOrderWorkflowImpl implements MegaBurgerOrderWork flow { 

// ...  

@Override 

public void orderFood(FoodOrder order) {  

  OrderWorkflow parentOrderWorkflow = getParentOrderWorkflow();  

Integer orderId = megaBurgerOrderActivities.createOrder(mapMega   BurgerFoodOrder(order));  

 updateOrderStatus(parentOrderWorkflow, OrderStatus.PENDING);  

// Poll until Order is accepted/rejected  

updateOrderStatus(parentOrderWorkflow,              pollOrderStatusTransition(orderId, OrderStatus.  

PENDING));  

if (OrderStatus.REJECTED.equals(currentStatus)) {  

 throw new RuntimeException(“Order with id “ + orderId + “      was rejected”); 

}  

  // Send ETA to parent workflow 

  parentOrderWorkflow.updateEta(getOrderEta(orderId));  // Poll until Order is cooking  

 updateOrderStatus(parentOrderWorkflow,            pollOrderStatusTransition(orderId, OrderStatus.ACCEPTED)); //   Poll until Order is ready 

updateOrderStatus(parentOrderWorkflow,                pollOrderStatusTransition(orderId, OrderStatus.COOKING)); //   Poll until Order is delivered  

updateOrderStatus(parentOrderWorkflow, 

pollOrderStatusTransition(orderId, OrderStatus.READY)); }  

// ...  

} 

该工作流程首先获得其父工作流程。我们的MegaBurgerOrderWorkflow ,只处理与MegaBurger的整合,将订单交付给客户是由一个单独的工作流管理的;这意味着我们正在使用一个子工作流

然后,我们通过一个活动创建订单,并获得一个订单ID。这个活动只是一个API客户端的封装器,它执行POST/orders

在创建订单后,父工作流被一个信号(对工作流的外部异步请求)通知,该订单现在是待定的。

现在我们必须等待,直到订单从待定过渡到接受或拒绝。这就是轮询开始发挥作用的地方。让我们看看我们的函数pollOrderStatusTransition做什么。

private OrderStatus pollOrderStatusTransition(Integer orderId,  OrderStatus orderStatus) { OrderStatus polledStatus =  

megaBurgerOrderActivities.getOrderById(orderId).getStatus();  while (orderStatus.equals(polledStatus)) {  

    Workflow.sleep(Duration.ofSeconds(30));  

 polledStatus = megaBurgerOrderActivities.  

 getOrderById(orderId).getStatus();   

}  

return polledStatus;  

}  

这与我们在本文介绍中提出的轮询循环非常相似。唯一的区别是,它不是在等待一个特定的状态,而是在轮询,直到有一个过渡。再一次,用于通过id获取订单的实际API调用隐藏在一个启用了重试的活动后面。

如果订单被拒绝,就会抛出一个运行时异常,使工作流失败。如果订单被接受了,则会通知父方MegaBurger的ETA(父方工作流会用这个来估计配送的时间)。

最后,图3所示的其余各状态 ,直到订单被标记为交付。

运行一个 "快乐路径 "场景

为了总结,让我们运行一个完整的订单场景。这个场景是我们的示例项目中包含的测试套件的一部分。唯一的要求是同时运行Instafood和MegaBurger服务器,如前面的步骤中所述。这个测试案例描述了一个客户通过Instafood MegaBurger的新素食汉堡订餐取货。

cadence-cookbooks-instafood/instafood$ ./gradlew test

InstafoodApplicationTest 从你的IDE。

class InstafoodApplicationTest { 

// ...  

@Test 

public void  

givenAnOrderItShouldBeSentToMegaBurgerAndBeDeliveredAccordingly() {   FoodOrder order = new FoodOrder(Restaurant.MEGABURGER,      “Vegan Burger”, 2, “+54 11 2343-2324”, “Díaz velez 433, La        lucila”, true);  

  // Client orders food 

        WorkflowExecution workflowExecution= WorkflowClient       start(orderWorkflow::orderFood, order); 

  // Wait until order is pending Megaburger’s acceptance await().until(() -> OrderStatus.PENDING.equals(orderWorkflow.    getStatus())); 

// Megaburger accepts order and sends ETA  

megaBurgerOrdersApiClient.updateStatusAndEta(getLastOrderId(),    “ACCEPTED”, 15);  

await().until(() -> OrderStatus.ACCEPTED.equals(orderWorkflow.    getStatus()));   

// Megaburger starts cooking order  

megaBurgerOrdersApiClient.updateStatus(getLastOrderId(),    “COOKING”); 

await().until(() -> OrderStatus.COOKING.equals(orderWorkflow.    getStatus()));  

// Megaburger signals order is ready  

megaBurgerOrdersApiClient.updateStatus(getLastOrderId(),    “READY”); 

await().until(() -> OrderStatus.READY.equals(orderWorkflow.      getStatus()));  

// Megaburger signals order has been picked-up  

megaBurgerOrdersApiClient.updateStatus(getLastOrderId(),    “RESTAURANT_DELIVERED”); 

await().until(() -> OrderStatus.RESTAURANT_DELIVERED.  

equals(orderWorkflow.getStatus()));  

await().until(() -> workflowHistoryHasEvent(workflowClient,         workflowExecution, EventType.WorkflowExecutionCompleted)):   }  

 } 

在这个场景中,我们有三个角色:Instafood、MegaBurger和客户:

  1. 客户将订单发送给Instafood。
  2. 一旦订单到达MegaBurger(订单状态为待定),MegaBurgers就将其标记为ACCEPTED,并发送ETA。
  3. 然后我们就有了整个状态更新的序列。
    1. MegaBurger将该订单标记为 "正在制作"。
    2. MegaBurger将该订单标记为READY(这意味着它已准备好送货/取货)。
    3. MegaBurger将该订单标记为RESTAURANT_DELIVERED。
  4. 由于这是一个以取货方式创建的订单,一旦客户这样做了,工作流程就完成了。

总结

在这篇文章中,我们获得了关于Cadence的第一手经验,以及如何使用它来进行投票。我们还向你展示了如何让Cadence集群在我们的Instaclustr平台上运行,以及让一个应用程序连接到它是多么容易。