大师。支持Shopify Flow的编排语言

380 阅读15分钟

大师。支持Shopify Flow的编排语言

神奇的故事

Shopify最近发布了新版本的Shopify Flow。商家广泛使用Flow的工作流程语言和相关的执行引擎来定制Shopify,将繁琐、重复的任务自动化,并专注于重要的事情。Flow为常见的用例提供了一个全面的模板库,并有详细的文档指导商家定制其工作流程。

在过去的几年里,我的团队一直致力于将Flow从一个成功的Shopify Plus应用程序过渡到一个平台,旨在为整个Shopify日益增长的自动化和定制需求提供动力。我们必须解决的主要技术挑战之一是Flow编辑器和引擎之间的过度耦合。由于它们共享相同的数据结构,编辑器和引擎不能独立发展,而且我们为它们的特殊需求定制这些数据结构的能力有限。这个问题很重要,因为编辑器和引擎的需求从根本上来说是非常不同的。

Flow编辑器提供了一种面向商家的可视化工作流语言。它的语言必须是陈述性的,捕捉商家的意图,而不涉及如何执行该意图。编辑器主要关注工作流程的可用性、可理解性和互动编辑。反过来,流程引擎需要以容错的方式有效地大规模执行工作流程。它的语言可以是命令式的,但它必须对优化有良好的支持,并有至少一次的执行语义,以确保工作流的执行从崩溃中恢复。然而,编辑器和引擎也需要很好地一起工作。例如,他们需要在类型系统上达成一致,该系统用于查找用户错误,并支持类似于IDE的功能,如代码完成和视觉体验中的内联错误报告。

我们意识到立即解决这个问题是很重要的,而且在尽量减少对商户的干扰的同时把它做好也是很关键的。我们循序渐进地进行。

首先,我们设计并实施了一种新的特定领域协调语言,以满足Flow引擎的要求。我们把这种语言称为Maestro。然后,我们实施了一个新的、可横向扩展的引擎来执行Maestro协调。接下来,我们创建了一个翻译层,将原来的Flow工作流数据结构翻译成Maestro协调程序。这使我们能够用新引擎执行现有的Flow工作流。最后,我们慢慢地将所有Flow工作流迁移到新引擎中,到2020年BFCM时,基本上所有工作流都在新引擎中执行。

然后,我们终于有能力处理视觉语言了。因此,我们实施了一个全新的视觉体验,包括为Flow编辑器提供了一种新的语言。这种语言比原来的语言更灵活,更有表现力,所以任何现有的工作流程都可以很容易地被迁移。这种语言还可以翻译成Maestro协调程序,因此它可以直接由新引擎执行。最后,一旦我们对新的体验感到满意,我们就开始迁移现有的Flow工作流程,到2022年初,所有Flow工作流程都被迁移到使用新的编辑器和新的引擎。

在这篇文章的其余部分,我想重点介绍新的协调语言,Maestro。我将给你一个关于它的设计和实现的概述,然后重点介绍它如何与Shopify Flow的新版本整齐地整合并满足其要求。

Maestro的一个例子

优美的乐章

让我们快速浏览一下,以了解Maestro的外观和它的具体功能。Maestro不是一种通用的编程语言,而是一种协调语言,只专注于协调对某些主机语言上的函数进行调用的顺序,同时捕捉这些函数调用之间传递的数据。例如,假设你想实现调用一个远程服务来获取一些相关客户的代码,然后从数据库中删除这些客户。Maestro语言不能实现远程服务调用或数据库调用本身,但它可以以容错的方式协调这些调用。使用Maestro的主要好处是,执行的状态被精确地捕捉到,并可以使其持久化,所以你可以观察到执行的进程,并在出现崩溃的情况下从原地重新开始。

下面的Maestro代码,为了便于展示,稍作了简化,实现了一个与上面的例子类似的协调过程。它首先定义了参与协调的数据的形状:一个叫做Customer 的对象类型,带有一些属性。然后它定义了三个函数。函数fetch_customers ,不需要任何参数,返回一个客户数组。它的实现只是向适当的服务执行一个GET HTTP请求。在这个例子中,delete_customer 函数通过调用标准库中的print函数来模拟数据库的删除。orchestration 函数代表主入口点。它使用序列表达式来协调函数的调用:首先调用fetch_customers ,将结果绑定到customers变量上,然后映射到每个客户上调用delete_customer

Maestro函数声明了封装表达式的接口:fetch_customersdelete_customer 的主体是调用表达式,而orchestration 的主体是一个序列表达式,由其他表达式组成。但在某些时候,我们必须向主机语言屈服,以实现实际的服务请求、数据库调用、打印等等。这是通过一个函数来实现的,这个函数的主体是一个原始表达式,这意味着它与在声明的键下注册的主机语言代码绑定。例如,这些是我们Ruby实现的标准库中的getprint 函数的声明。

我们现在可以使用Maestro解释器来执行orchestration 函数。这是命令行的一个可能的简化输出。

该输出包含了调用print两次的结果,对fetch服务返回的每个客户都有一次。这里有趣的是,-c 标志指示解释器也将检查点转储到标准输出。

检查点是Maestro用来存储执行状态的东西。它们包含足够的信息来了解协调中已经发生的事情和尚未完成的事情。例如,第一个检查点包含服务请求的结果,其中包括一个包含要删除的客户信息的JSON对象。在实践中,检查点被发送到持久存储,如Kafka、Redis或MySQL。然后,如果解释器因某种原因停止,我们可以重新启动并将其指向现有的检查点。解释器可以通过跳过已经存在检查点的表达来恢复。例如,如果我们在从数据库中删除客户时崩溃,我们就不会重新执行获取请求,因为我们已经有了它的结果。

检查点机制允许Maestro为原始调用提供至少一次的语义,这正是Shopify Flow工作流程所期望的。事实上,新的Flow引擎,在高层次上,基本上是一个水平可扩展的、分布式的工作池,在传入的事件上执行Maestro解释器,用于Flow生成的协调。检查点用于容错,以及给商户反馈每个执行步骤、当前状态等。

Flow和Maestro合奏

胜利的喜悦

现在我们知道了Maestro的能力,让我们看看它是如何与Flow一起发挥作用的。例如,下面的工作流程显示了一个典型的Flow自动化用例。当商店中的订单被创建时,它就会触发,并根据折扣代码的存在或客户的电子邮件,检查该订单中的某些条件。如果条件谓词成功匹配,它就会给订单添加一个标签,随后向店主发送一封电子邮件以提醒折扣。

一个典型的Flow自动化用例

考虑一个商家使用Flow应用来创建和执行这个工作流程。涉及的主要活动有四个

  1. 浏览工作流程中可能使用的任务和类型
  2. 验证工作流程是否正确
  3. 激活工作流,使其在事件中开始执行
  4. 监控执行情况。

任务和类型的目录

流程编辑器显示了一个任务目录,供商户挑选。这些是由Shopify和Shopify应用程序通过Shopify Flow连接器提供的触发器、条件和行动。此外,Flow允许商家浏览Shopify的GraphQL Admin API对象,以便为工作流程选择相关数据。例如,这个工作流程中的Order created 触发器在概念上带来了一个订单资源,代表了刚刚创建的订单。因此,当商家在定义一个条件或向行动传递参数时,Flow会协助浏览该Order 对象可达到的属性。要做到这一点,Flow必须有一个GraphQL API类型的模型,并理解任务所期望和提供的接口。Flow通过分别建立在Maestro类型和函数之上来实现这一点。

Flow将类型建模为经过装饰的Maestro类型:结构由Maestro类型定义,但Flow添加了信息,如字段和类型描述。工作流程中涉及的大多数类型来自API,如Shopify GraphQL Admin API。因此,Flow有一个自动流程来消费API并生成相应的Maestro类型。可以定义额外的类型,例如,为对应触发器的事件中包含的数据建模,以及为行动的预期界面建模。例如,以下类型是例子中涉及的事件数据和Shopify对象的简化版本。

然后,Flow使用Maestro函数和调用来模拟触发器、条件和行动的行为。下面的Maestro代码显示了上述工作流程中涉及的触发器和动作的函数定义。

行动被直接映射到Maestro函数中,这些函数定义了预期的参数和返回类型。在工作流程中使用的行动是对相应函数的调用。然而,一个触发器被映射到一个数据水合函数,该函数获取事件数据,通常只包括由IDs ,并加载工作流程所需的额外数据。例如,order_created 函数接收一个OrderCreatedTrigger ,其中包含订单ID 作为Integer ,并执行 API 请求来加载一个Order 对象,其中包含额外的字段,如名称和discountCode 。最后,条件目前是一个特殊的情况,它们被翻译成基于为条件定义的谓词的函数调用序列(在下一节有更多介绍)。

工作流验证

一旦一个工作流被创建,它就需要验证。为此,Flow构成了一个代表整个工作流的Maestro函数。工作流函数的参数是触发器数据,因为它是其执行的输入。函数的主体对应于工作流中任务的转换和配置。例如,下面的函数对应于这个例子。

序列中的第一个调用对应于触发器函数,它被用来从事件数据中水化对象。接下来的三个步骤对应于为条件配置的逻辑表达式。每个二元结分支成为一个函数调用(分别调用eqends_with ),结果用or 计算。一个Maestro匹配表达式被用来对结果进行模式匹配。如果是true ,控制流就会流向调用工作流动作所对应的函数的序列表达式。

流现在可以依靠Maestro静态分析来验证工作流的功能。Maestro会进行类型检查,验证每个被引用的变量是否在范围内,验证对象导航是否正确(例如,order.customer.email 是有效的),等等。然后,通过静态分析发现的任何错误都会被映射回相应的工作流节点,并在编辑器中以上下文形式呈现。除了返回错误外,静态分析结果还包含每个表达式的符号表,表明哪些变量在范围内,它们的类型是什么。这支持编辑器提供代码完成和其他针对每个工作流程步骤的建议。例如,下面的截图显示了编辑器如何指导用户在选择Add order tags 行动时浏览对象中存在的字段。

请注意,转换和验证是在Flow工作流程被编辑的时候运行的,无论是在Flow编辑器中还是通过API。这种操作是同步的,因此,必须非常快,因为商家在等待结果。这种架构类似于现代IDE将源代码发送到语言服务,语言服务将代码解析为较低级别的表示,并返回潜在错误和额外的静态分析结果。

工作流的激活

一旦一个工作流准备好了,就需要激活它以开始执行。这个过程最初与验证类似,因为Flow会生成相应的Maestro函数。然而,还有一些额外的步骤。首先,Maestro进行静态使用分析:对于每一个对原始函数的调用,它都会计算返回类型的哪些属性被后续步骤所使用。例如,对shopify::admin::order_created 的调用返回一个元组(Shop,Order),但不是所有这些类型的属性都被使用。特别是,order.customer.name 并没有被这个工作流程所使用。将该值水化不仅是低效的,在存在递归定义的情况下(比如Order 有一个Customer ,后者有Orders ),将无法确定在哪里停止对类型图的挖掘。使用分析的结果在运行时被传递给主函数实现。运行时可以使用它来定制它如何计算返回的值,例如,通过优化对管理员GraphQL API的查询。

第二,Maestro执行一个编译步骤。其目的是应用优化,删除任何对函数的运行时执行不必要的东西,如类型定义和不被工作流函数调用的辅助函数。其结果是一个简化的、小型的、高效的Maestro函数。然后,编译后的函数与使用分析的结果一起打包,成为一个协调程序。最后,协调程序被序列化并部署到观察事件并在协调程序上运行Maestro解释器的流引擎。

监控执行情况

当Flow引擎执行编排时,Maestro解释器会发出检查点。正如我们之前所讨论的,检查点被引擎在重启解释器时使用,以确保动作的一次性语义。此外,检查点被送回Flow,以提供活动页面,该页面列出工作流的执行情况。由于检查点有关于每个原始函数调用的输出的详细信息,它们可以被用来映射到原始工作流步骤,并提供对执行行为的洞察力。

活动页面中的Shopify Flow运行日志

例如,上面的图片显示了一个例子的具体执行的运行日志,可以从活动页面访问。请注意,Flow突出显示了执行的工作流程的分支,以及在运行时条件二择一的哪个分支实际评估为真。所有这些信息都直接来自于对检查点的解释和对工作流的映射。

结束语。未来的工作

大纲

在这篇文章中,我介绍了Maestro,这是我们为支持Shopify Flow而开发的一种特定领域协调语言。我举了一个例子,说明Maestro是什么样子的,以及它如何与Flow整齐地整合,支持Flow编辑器和Flow引擎的功能。Maestro已经为Flow提供了一段时间的支持,但我们正在计划更多,比如。

  • 改进Flow工作流语言的表达能力,更好地利用Maestro提供的所有功能。例如,允许定义变量以绑定行动的结果,供后续使用,支持迭代、模式匹配,等等。
  • 在部署时实施额外的优化,例如将Flow工作流合并为一个单一的协调,以避免对同一事件进行多余的水化调用。
  • 使用Maestro解释器来支持Flow工作流的预览和测试,采用检查点来显示结果和验证断言。

如果你对使用Flow和Maestro或从头开始构建系统以解决现实世界的问题感兴趣。请访问我们的工程职业页面,了解我们的空缺职位。加入我们的远程团队,(几乎)在任何地方工作。了解我们如何通过招聘来共同设计未来--一个通过设计实现数字化的未来。

在您的收件箱中获得这样的故事!

来自构建和扩展Shopify的团队的故事。这个商务平台为全球数以百万计的企业提供动力。

电子邮件地址是的,给我登记

与我们分享您的电子邮件并接收每月的更新。

谢谢您的订阅。

您将很快开始收到免费的提示和资源。