水利网站建设,温州做网站推广,国家电子商务平台,推广平台的方法在上一章中#xff0c;我们使用代理来表示存储容器。在 mix 的介绍中#xff0c;我们指定要命名每个存储容器#xff0c;以便我们可以执行以下操作#xff1a; 在上面的会话中#xff0c;我们与“购物”存储容器进行了交互。
由于代理是进程#xff0c;因此每个存储容器…在上一章中我们使用代理来表示存储容器。在 mix 的介绍中我们指定要命名每个存储容器以便我们可以执行以下操作 在上面的会话中我们与“购物”存储容器进行了交互。
由于代理是进程因此每个存储容器都有一个进程标识符 (PID)但存储容器没有名称。在“进程”一章中我们了解到可以通过为进程赋予原子名称来在 Elixir 中注册进程 但是用原子命名动态进程是一个糟糕的想法如果我们使用原子我们需要将存储容器名称通常从外部客户端接收转换为原子并且我们永远不应该将用户输入转换为原子。这是因为原子不会被垃圾收集。一旦创建了原子它就永远不会被回收。从用户输入生成原子意味着用户可以注入足够多的不同名称来耗尽我们的系统内存
实际上在内存耗尽之前您更有可能达到 Erlang VM 的最大原子数限制这无论如何都会使您的系统崩溃。
我们不会滥用内置名称功能而是创建自己的进程注册表将存储容器名称与存储容器进程关联起来。
注册表需要保证它始终是最新的。例如如果其中一个存储容器进程由于错误而崩溃注册表必须注意到这一变化并避免提供过时的条目。在 Elixir 中我们说注册表需要监视每个存储容器。由于我们的注册表需要能够接收和处理来自系统的临时消息因此 Agent API 是不够的。
我们将使用 GenServer 创建一个可以监控存储桶进程的注册表进程。GenServer 为在 Elixir 和 OTP 中构建服务器提供了工业级功能。
如果您还没有阅读 GenServer 模块文档请阅读概述。一旦您这样做我们就可以继续了。
GenServer 回调
GenServer 是在特定条件下调用一组有限函数的过程。当我们使用 Agent 时我们会将客户端代码和服务器代码并排放置如下所示 让我们稍微分解一下这段代码 在上面的代码中我们有一个进程我们称之为“客户端”它向代理即“服务器”发送请求。该请求包含一个匿名函数该函数必须由服务器执行。
在 GenServer 中上面的代码将是两个独立的函数大致如下 GenServer 代码中还有相当多的繁琐但正如我们将看到的它也带来了一些好处。
目前我们将只为存储容器注册逻辑编写服务器回调而不提供适当的 API稍后我们将提供。
在 lib/kv/registry.ex 创建一个新文件内容如下 您可以向 GenServer 发送两种类型的请求调用和强制类型转换。调用是同步的服务器必须向此类请求发送响应。当服务器计算响应时客户端正在等待。强制类型转换是异步的服务器不会发送响应因此客户端不会等待响应。这两种请求都是发送到服务器的消息将按顺序处理。在上面的实现中我们对 :create 消息进行模式匹配将其作为强制类型转换处理对 :lookup 消息进行模式匹配将其作为调用处理。
为了调用上面的回调我们需要遍历相应的 GenServer 函数。让我们启动一个注册表创建一个命名的 bucket然后查找它 我们的 KV.Registry 进程按此顺序接收到一个带有 {:create, shopping} 的转换和一个带有 {:lookup, shopping} 的调用。一旦消息发送到注册表GenServer.cast 将立即返回。另一方面我们将在 GenServer.call 中等待由上述 KV.Registry.handle_call 回调提供的答案。
您可能还注意到我们在每个回调之前都添加了 impl true。impl true 通知编译器我们对后续函数定义的意图是定义一个回调。如果我们在函数名称或参数数量上犯了错误例如我们定义了一个 handle_call/2编译器会警告我们没有任何 handle_call/2 可定义并为我们提供 GenServer 模块已知回调的完整列表。
这一切都很好但我们仍然希望为用户提供一个允许我们隐藏实现细节的 API。
客户端 API
GenServer 由两部分实现客户端 API 和服务器回调。您可以将两个部分组合成一个模块也可以将它们分成客户端模块和服务器模块。客户端是调用客户端函数的任何进程。服务器始终是我们将明确作为参数传递给客户端 API 的进程标识符或进程名称。在这里我们将对服务器回调和客户端 API 使用单个模块。
编辑 lib/kv/registry.ex 文件填写客户端 API 的空白 第一个函数是 start_link/1它通过传递选项列表来启动一个新的 GenServer。start_link/1 调用 GenServer.start_link/3它接受三个参数
1. 实现服务器回调的模块在本例中为 __MODULE__表示当前模块 2. 初始化参数在本例中为原子 :ok 3. 可用于指定服务器名称等内容的选项列表。现在我们将在 start_link/1 上收到的选项列表转发到 GenServer.start_link/3
接下来的两个函数 lookup/2 和 create/2 负责将这些请求发送到服务器。在本例中我们分别使用了 {:lookup, name} 和 {:create, name}。请求通常被指定为元组就像这样以便在第一个参数槽中提供多个“参数”。通常将请求的操作指定为元组的第一个元素并在其余元素中指定该操作的参数。请注意请求必须与 handle_call/3 或 handle_cast/2 的第一个参数匹配。
这就是客户端 API。在服务器端我们可以实现各种回调来保证服务器初始化、终止和处理请求。这些回调是可选的目前我们只实现了我们关心的回调。让我们回顾一下。
第一个是 init/1 回调它接收给 GenServer.start_link/3 的第二个参数并返回 {:ok, state}其中 state 是一个新的映射。我们已经注意到 GenServer API 如何使客户端/服务器隔离更加明显。start_link/3 发生在客户端而 init/1 是在服务器上运行的相应回调。
对于 call/2 请求我们实现了一个 handle_call/3 回调它接收请求、我们从中接收请求的进程 (_from) 和当前服务器状态 (names)。handle_call/3 回调返回一个格式为 {:reply, reply, new_state} 的元组。元组的第一个元素 :reply 表示服务器应该将回复发送回客户端。第二个元素 reply 是将发送给客户端的内容而第三个元素 new_state 是新的服务器状态。
对于 cast/2 请求我们实现了一个 handle_cast/2 回调它接收请求和当前服务器状态 (names)。handle_cast/2 回调返回一个格式为 {:noreply, new_state} 的元组。请注意在实际应用中我们可能会使用同步调用而不是异步转换来实现 :create 的回调。我们这样做是为了说明如何实现转换回调。
handle_call/3 和 handle_cast/2 回调都可能返回其他元组格式。我们还可以实现其他回调例如terminate/2 和 code_change/3。欢迎您浏览完整的 GenServer 文档以了解有关这些内容的更多信息。
现在让我们编写一些测试来保证我们的 GenServer 能够按预期工作。
测试 GenServer
测试 GenServer 与测试代理没有太大区别。我们将在设置回调中生成服务器并在整个测试中使用它。在 test/kv/registry_test.exs 创建一个文件其中包含以下内容 我们的测试用例首先断言我们的注册表中没有存储容器创建一个命名的存储容器查找它并断言它表现为存储容器。
我们为 KV.Registry 编写的设置块和为 KV.Bucket 编写的设置块之间有一个重要的区别。我们没有通过调用 KV.Registry.start_link/1 手动启动注册表而是调用 ExUnit.Callbacks.start_supervised!/2 函数传递 KV.Registry 模块。
通过使用 ExUnit.Case 将 start_supervised! 函数注入到我们的测试模块中。它通过调用其 start_link/1 函数来完成启动 KV.Registry 进程的工作。使用 start_supervised! 的优势是 ExUnit 将保证在下一个测试开始之前关闭注册表进程。换句话说它有助于保证一个测试的状态不会干扰下一个测试以防它们依赖于共享资源。
在测试期间启动进程时我们应该始终优先使用 start_supervised!。我们建议您将 bucket_test.exs 中的设置块也更改为使用 start_supervised!。
运行测试它们都应该通过
监控的必要性
到目前为止我们所做的一切都可以通过 Agent 来实现。在本节中我们将看到 GenServer 可以实现的众多功能之一而 Agent 无法实现这些功能。
让我们从一个测试开始该测试描述了当存储容器停止或崩溃时我们希望注册表如何表现 上面的测试将在最后一个断言上失败因为即使我们停止存储容器进程存储容器名称仍保留在注册表中。
为了修复这个错误我们需要注册表监控它生成的每个存储容器。一旦我们设置了一个监视器每次存储容器进程退出时注册表都会收到通知让我们可以清理注册表。
让我们首先使用 iex -S mix 启动一个新控制台来使用监视器 注意 Process.monitor(pid) 返回一个唯一引用允许我们将即将到来的消息与该监视引用匹配。停止代理后我们可以 flush/0 所有消息并注意到 :DOWN 消息到达其中包含监视器返回的确切引用通知存储容器进程因 :normal 原因退出。
让我们重新实现服务器回调以修复错误并使测试通过。首先我们将 GenServer 状态修改为两个字典一个包含 name - pid另一个包含 ref - name。然后我们需要在 handle_cast/2 上监视存储桶并实现 handle_info/2 回调来处理监视消息。完整的服务器回调实现如下所示 请注意我们能够在不更改任何客户端 API 的情况下显著更改服务器实现。这是明确隔离服务器和客户端的好处之一。
最后与其他回调不同我们为 handle_info/2 定义了一个“catch-all”子句用于丢弃和记录任何未知消息。要了解原因让我们继续下一节。
call, cast or info?
到目前为止我们已经使用了三个回调handle_call/3、handle_cast/2 和 handle_info/2。在决定何时使用每个回调时我们需要考虑以下几点
1.handle_call/3 必须用于同步请求。这应该是默认选择因为等待服务器回复是一种有用的背压机制。 2.handle_cast/2 必须用于异步请求当您不关心回复时。强制转换不能保证服务器已收到消息因此应谨慎使用。例如我们在本章中定义的 create/2 函数应该使用 call/2。我们使用 cast/2 是为了教学目的。 3.handle_info/2 必须用于服务器可能收到的所有其他未通过 GenServer.call/2 或 GenServer.cast/2 发送的消息包括使用 send/2 发送的常规消息。监控 :DOWN 消息就是一个例子。
由于任何消息包括通过 send/2 发送的消息都会转到 handle_info/2因此可能会有意外消息到达服务器。因此如果我们不定义 catch-all 子句这些消息可能会导致我们的注册表崩溃因为没有子句匹配。不过我们不必担心 handle_call/3 和 handle_cast/2 的此类情况。调用和强制转换仅通过 GenServer API 完成因此未知消息很可能是开发人员的错误。
为了帮助开发人员记住 call、cast 和 info 之间的区别、支持的返回值等我们准备了一个小型 GenServer 备忘单。
监视器还是链接
我们之前在“进程”一章中学习了链接。现在注册表已完成您可能想知道我们何时应该使用监视器何时应该使用链接
链接是双向的。如果您链接两个进程其中一个崩溃则另一端也会崩溃除非它捕获退出。监视器是单向的只有监视进程会收到有关被监视进程的通知。换句话说当您想要链接崩溃时使用链接当您只想收到崩溃、退出等通知时使用监视器。
回到我们的 handle_cast/2 实现您可以看到注册表既链接又监视存储容器 这是一个坏主意因为我们不希望注册表在存储容器崩溃时崩溃。正确的解决方法是实际上不将存储桶链接到注册表。相反我们将每个存储容器链接到一种称为 Supervisors 的特殊类型的进程这些进程明确设计用于处理故障和崩溃。我们将在下一章中了解有关它们的更多信息。