# Manticore Buddy: pluggable design

本文是为 Buddy v1.x 编写的。[在此处查看最新版本](./manticoresearch-buddy-pluggable-design/../manticoresearch-buddy-pluggable-design-v2)。

大家好。关于 Manticore Buddy 有令人兴奋的消息：我们已经完成了向可插拔设计的迁移！这意味着您可以将自己构建的 Manticore Search SQL/JSON 查询作为插件，发布在 [packagist.org](http://packagist.org/) 上，并使用 `CREATE PLUGIN` SQL 命令进行安装。让我们深入了解可插拔系统的架构以及一个简单的教程，帮助您入门。

## 架构

### 简介

如果您不熟悉 Buddy，我们建议您阅读以下博客文章：

- [介绍 Buddy：Manticore Search 的 PHP 侧车](./manticoresearch-buddy-pluggable-design/../manticoresearch-buddy-intro/)
- 以及 [Manticore Buddy：挑战与解决方案](./manticoresearch-buddy-pluggable-design/../manticoresearch-buddy-challenges-and-solutions/)

这些是我们之前发布的文章。

最初，Buddy 是用 PHP 开发的，所有扩展和额外命令处理程序都在同一个代码库中。在 [Manticore Search 6](https://manticoresearch.com/blog/manticore-search-6-0-0/) 发布后，我们收到了关于添加自定义功能的问题，并意识到当前系统缺乏灵活性。因此，我们决定迁移到可插拔架构。

我们认为软件开发中最重要的原则是不要重复发明轮子并保持简单。因此，我们没有花太多时间犹豫，选择了 PHP 世界中最知名和流行的包管理器来管理插件系统 – [Packagist](https://packagist.org/)。

### 插件类型

有三种类型的插件：

- `core` - 默认包含在 Buddy 中，并且是您安装 Manticore 时获得的包的一部分。
- `local` - 用于发布前的开发和调试。
- `external` - 使用 `CREATE PLUGIN` 命令安装。

本教程将解释如何首先创建一个 `local` 插件，然后将其制作成 `external` 插件，使任何 Manticore 用户都可以通过使用您的插件来增强其设置。

### 请求 -> 响应流程

为了使其工作，我们将 Buddy 和所有插件中常用的组件提取到一个新的包中 – [Buddy Core](https://github.com/manticoresoftware/buddy-core)。在开发您的插件时，应包含此包，以便在 IDE 中更轻松地使用并支持提示和自动补全。

使 Buddy 能够作为守护进程运行并执行内部任务（包括连接 Core 和插件源代码）的代码称为 Buddy Base。这是一个 ReactPHP 应用程序，也使用 [Buddy Core](https://github.com/manticoresoftware/buddy-core)，并具有基本逻辑以使一切正常运行并找到正确的插件来执行命令。

为了更清楚地理解这个过程，这里有一个图示说明请求 → 响应流程：

![Manticore Buddy 请求到响应流程](./manticoresearch-buddy-pluggable-design/manticoresearch-buddy-request-response-flow.png)

## 教程

为了使您更容易上手并开始开发自己的插件，让我们看看如何为 Buddy 构建一个简单的插件，以支持 `SHOW HOSTNAME` 命令并返回当前主机的主机名。这将帮助您了解在创建自己的插件时应遵循的流程，而且这也是一个极其简单的入门示例。如果我们查看在实现其逻辑之前 `SHOW HOSTNAME` 在 Manticore Search 中的功能，我们可以预期结果如下：

```sql
mysql> SHOW HOSTNAME;
ERROR 1064 (42000): sphinxql: syntax error, unexpected identifier, expecting VARIABLES near 'HOSTNAME'
```

### 准备工作

我们首先要做的就是准备开发环境。您只需执行一次，之后可以通过从 [GIT 仓库](https://github.com/manticoresoftware/manticoresearch-buddy) 更新 Buddy 来获取最新更新。让我们打开终端，将 Buddy 克隆到名为 `manticoresearch-buddy` 的文件夹中，并导航到克隆的文件夹。

```bash
git clone https://github.com/manticoresoftware/manticoresearch-buddy.git
cd manticoresearch-buddy
git checkout v1.x
```

现在您已经有了 Buddy 的全部源代码，但还没有安装任何包。Buddy 使用 Composer 来管理其依赖项，我们需要首先安装它们才能运行应用程序。不过，在此之前，我们将运行一个我们专门为开发准备的特殊 Docker 容器。Manticore Executor Kit 镜像包含了所有必要的工具，以确保 Manticore Buddy 的开发体验顺畅，无论您是创建新插件还是为 Buddy 本身做贡献。

```bash
docker pull ghcr.io/manticoresoftware/manticoresearch:test-kit-6.2.12
docker create --privileged --entrypoint bash \
  -v $(pwd):/workdir -w /workdir --name manticore-buddy \
  -it ghcr.io/manticoresoftware/manticoresearch:test-kit-6.2.12
docker start manticore-buddy
```

恭喜！您现在已启动了一个包含 **Manticore Search** 和 **Manticore Executor** 的 Docker 容器，用于开发 Buddy 和插件。工作目录设置为 `/workdir`，您的源代码文件夹已绑定到容器。下一步是安装 Composer 依赖项。请记住，此命令和所有后续命令都必须在 `manticore-buddy` 容器内执行。

```bash
docker exec -it manticore-buddy bash
composer install
```

您的 Buddy 现在已准备好用于使用、开发、调试和测试。接下来，您需要完成两件事：

1. 首先，编辑 `/etc/manticoresearch/manticore.conf` 并添加 `buddy_path`，将我们的源代码路径添加到其中。这将让 Manticore Search 知道我们需要从源代码运行 Buddy，而不是从已安装的模块中运行。在 `searchd` 部分添加以下内容：
   ```text
   buddy_path = manticore-executor /workdir/src/main.php --debug
   ```
2. 第二，尝试运行 `searchd`（Manticore Search 服务器）并确保其正常工作。
   ```bash
   # Launch daemon inside the container and keep it foreground
   searchd --nodetach
   ```
   您应该会看到以 `[BUDDY]` 开头的几行。当您想要停止进程时，只需使用 **Ctrl + C**。

到目前为止，所有内容都已设置好，可以准备 Buddy 的开发环境并开始实现新插件。您仍然可以对其进行实验并尝试执行一些查询以确保其正常工作。例如，打开另一个终端窗口并运行以下命令：

```bash
$ docker exec -it manticore-buddy mysql -h0 -P9306
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 6.0.5 3bcbd00fa@230320 dev (columnar 2.0.5 8171c1a@230320) (secondary 2.0.5 8171c1a@230320) git branch HEAD (no branch)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show queries;
+------+--------------+----------+-----------------+
| id   | query        | protocol | host            |
+------+--------------+----------+-----------------+
|   10 | select       | http     | 127.0.0.1:19148 |
|    9 | show queries | mysql    | 127.0.0.1:54484 |
+------+--------------+----------+-----------------+
2 rows in set (0.008 sec)

MySQL [(none)]>
```

你刚刚向 Manticore Search 发送了命令 `SHOW QUERIES`，该命令随后被路由到 Buddy 并成功执行。你可以在第一个终端标签页中的 searchd 日志中看到 Buddy 接收到的内容及其响应。因此，我们可以确认 Buddy 正常工作，所有设置都正确完成。

### 插件模板

我们创建了一个 [特殊的 GitHub 模板仓库](https://github.com/manticoresoftware/buddy-plugin-template)，强烈建议你使用它来创建任何 Manticore Buddy 插件。所有插件名称都应以 `buddy-plugin-[your-name]` 开头，其中 `[your-name]` 是你的插件名称。例如，因为我们正在开发一个主机名选择插件，我们将它命名为 `buddy-plugin-show-hostname`。因此：

1. 打开 [https://github.com/manticoresoftware/buddy-plugin-template](https://github.com/manticoresoftware/buddy-plugin-template)。
2. 选择 `v1.x` 分支
3. 点击 `Use this template` 然后 `Create a new repository`。
4. 填写表单并创建你的仓库。确保你的仓库名称以 `buddy-plugin-` 开头，这是强制要求的。
5. 将你的新仓库 `git clone` 到之前部署的 Manticore Buddy 仓库的 `plugins` 目录中。请注意，最好在容器外部执行此操作，特别是如果你从私有仓库克隆时。

在 [https://github.com/manticoresoftware/buddy-plugin-show-hostname](https://github.com/manticoresoftware/buddy-plugin-show-hostname.git)（分支：`v1.x`）中，你可以找到整个任务的实现。如果你不想深入细节，可以直接 `git clone` 该仓库，而不是你分叉的仓库。然而，如果你想掌握创建插件的技能，让我们继续。

### 添加真实代码

现在我们需要按照以下步骤更新我们的模板，以添加与新插件相关的内容。

1. 首先，我们打开 `composer.json` 并更新插件名称、描述和命名空间。最终，你的更改将如下所示（请确保使用你的插件仓库名称）：

```diff
diff --git a/composer.json b/composer.json
index 23c252b..f36cb6e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,11 +1,11 @@
 {
- "name": "manticoresoftware/buddy-plugin-template",
- "description": "The Buddy template handler plugin",
+ "name": "manticoresoftware/buddy-plugin-show-hostname",
+ "description": "The Buddy SHOW hostname handler plugin",
  "type": "library",
  "license": "GPL-2.0-or-later",
  "autoload": {
    "psr-4": {
-     "Manticoresearch\\Buddy\\Plugin\\Template\\": "src/"
+     "Manticoresearch\\Buddy\\Plugin\\ShowHostname\\": "src/"
    }
  },
  "authors": [
```

2. 我们还需要更新两个类的命名空间 – `Payload` 和 `Handler`。差异应如下所示：

```diff
diff --git a/src/Handler.php b/src/Handler.php
index 3756dc3..07fcfd0 100644
--- a/src/Handler.php
+++ b/src/Handler.php
@@ -8,7 +8,7 @@
   version. You should have received a copy of the GPL license along with this
   program; if you did not, you can find it at http://www.gnu.org/
 */
-namespace Manticoresearch\Buddy\Plugin\Template;
+namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Plugin\BaseHandler;
 use Manticoresearch\Buddy\Core\Task\Task;
diff --git a/src/Payload.php b/src/Payload.php
index b170340..a35f201 100644
--- a/src/Payload.php
+++ b/src/Payload.php
@@ -8,7 +8,7 @@
   version. You should have received a copy of the GPL license along with this
   program; if you did not, you can find it at http://www.gnu.org/
 */
-namespace Manticoresearch\Buddy\Plugin\Template;
+namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Network\Request;
 use Manticoresearch\Buddy\Core\Plugin\BasePayload;
```

3. 作为最后一步，不要忘记通过在容器内运行 `composer install` 安装新依赖项。

### 从 Manticore Search 到你的插件并返回的请求流程

我们现在准备实现我们的逻辑。但首先，让我们了解一些关于其内部处理方式的更多信息。

![Manticore Buddy 请求到插件流程](./manticoresearch-buddy-pluggable-design/manticoresearch-buddy-request-plugin-flow.png)

假设我们使用 MySQL 客户端向启动的 `searchd` 进程发送 `SHOW HOSTNAME` 查询。Manticore Search 无法处理它，并将其发送到 Buddy 以等待响应。Buddy 可以返回确切的响应，该响应将简单地代理回 MySQL 客户端。

一旦 Buddy 从 Manticore Search 接收到查询，它会解析它并在所有插件中运行验证：首先核心插件，然后是外部插件。为了运行此验证，它会执行 `Payload::hasMatch($request)` 方法，将请求传递给它，该请求包含查询和一些元数据，例如请求的端点、类型（JSON 或 SQL）等。

`Payload` 表示包含处理命令所需所有数据的结构。如果插件的 `Payload` 在 `hasMatch` 方法中返回 true，则表示我们将使用此插件来处理请求。它是从 `Request` 创建的，此时我们需要解析查询，提取有价值的信息，并将其设置到我们的 payload 数据中，该数据将传递给 `Handler`。

`Handler` 是实际执行实现命令的类。它在使用并行扩展的线程环境中执行此操作，而不会阻塞主服务器循环。有一个简单的 `run` 方法，它创建一个闭包，必须执行任务并将响应返回给 Manticore Search，然后该响应将被代理给用户。

在我们的情况下，我们只需要添加对支持 `SHOW HOSTNAME` 查询语法的检查，然后编写 `Handler` 逻辑以调用 `gethostname` PHP 函数并将其作为响应返回。就是这样！让我们从 `Payload` 开始。

### 实现 Payload

当你打开 `src/Payload.php` 时，你会看到只有两个方法：`fromRequest` 和 `hasMatch`。我们用 `TODO` 注释标记了需要编辑的行，因此你应该很容易找到需要做的事情。在我们的情况下，查询非常简单且完全不灵活，因此我们不需要在 `fromRequest` 方法中做任何更改。我们只需删除 `TODO` 注释。我们需要做的是更新静态函数 `hasMatch()`。在那里，我们需要实现一个检查，确保查询严格匹配字符串 `SHOW HOSTNAME`（不区分大小写）。一旦匹配成功，我们应该返回 true。否则，我们应该返回 false。这就是基础系统理解在接收到显示主机名的查询时需要使用此插件的方式。

我们对 `Payload` 做出的最终差异应如下所示：

```diff
diff --git a/src/Payload.php b/src/Payload.php
index a35f201..bd0795e 100644
--- a/src/Payload.php
+++ b/src/Payload.php
@@ -26,7 +26,6 @@ final class Payload extends BasePayload {
   */
  public static function fromRequest(Request $request): static {
    $self = new static();
-   // TODO: add logic of parsing request into payload here
    // We just need to do something, but actually its' just for PHPstan
    $self->path = $request->path;
    return $self;
@@ -37,7 +36,6 @@ final class Payload extends BasePayload {
   * @return bool
   */
  public static function hasMatch(Request $request): bool {
-   // TODO: validate $request->payload and return true, if your plugin should handle it
-   return $request->payload === 'template';
+   return stripos($request->payload, 'show hostname') !== false;
  }
 }
```

我们完成了 `Payload` 的更改，现在可以继续到 `Handler` 来实现真正的逻辑。

### 实现 Handler

现在，让我们打开 `src/Handler.php` 并查看 `TODO` 标记，以及对发生情况的简要描述。我们需要导航到 `run` 方法并更新它。

我们可以看到一个返回 `TaskResult` 的闭包，这是一种特殊的返回类型，用于包装标准的 JSON 响应。

我们将包含一个 `gethostname()` 调用，并通过将其包装在将返回给客户端的 `TaskResult` 中来准备响应。最终差异将如下所示：

```diff
diff --git a/src/Handler.php b/src/Handler.php
index 07fcfd0..944899b 100644
--- a/src/Handler.php
+++ b/src/Handler.php
@@ -11,6 +11,7 @@
 namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Plugin\BaseHandler;
+use Manticoresearch\Buddy\Core\Task\Column;
 use Manticoresearch\Buddy\Core\Task\Task;
 use Manticoresearch\Buddy\Core\Task\TaskResult;
 use RuntimeException;
@@ -33,9 +34,11 @@ final class Handler extends BaseHandler {
   * @throws RuntimeException
   */
  public function run(Runtime $runtime): Task {
-   // TODO: your logic goes into closure and should return TaskResult as response
    $taskFn = static function (): TaskResult {
-     return TaskResult::none();
+     $hostname = gethostname();
+     return TaskResult::withRow([
+       'hostname' => $hostname,
+     ])->column('hostname', Column::String);
    };

    return Task::createInRuntime(
```

### 调试和开发

现在你已经完成了实现，是时候测试和调试（如果需要）你的插件了。有一种专门为此目的设计的插件类型，称为 `local`。由于你的插件已经在 `plugins` 目录中，它会自动成为 `local` 插件。要使用它，只需：

1. 在 Buddy 根目录中，运行命令 `composer require [your-plugin-name]:dev-main` 以包含插件。`your-plugin-name` 是您之前编辑的插件的 `composer.json` 中的名称。
2. 重新启动 `searchd` 以确保代码已更新。

此方法允许您在不将插件推送到 Git 仓库并在 [packagist.org](http://packagist.org/) 上发布的情况下，实时开发、调试、编辑和测试您的插件。

### 发布和安装

调试完成后，准备好发布插件时，只需将更改提交到 Git 仓库，并在 [packagist.org](https://packagist.org/) 上发布您的包。

之后，通过运行 `CREATE PLUGIN [your-plugin-name] TYPE 'buddy' VERSION 'dev-main'` 命令检查其是否按预期工作，该命令将尝试使用 Composer 下载插件并将其安装到 `plugin_dir`。

像这样：

```bash
$ docker exec -it manticore-buddy mysql -h0 -P9306
MySQL [(none)]> CREATE PLUGIN manticoresoftware/buddy-plugin-show-hostname type 'buddy' VERSION 'v1.x';

Query OK, 0 rows affected (1 min 12.213 sec)

MySQL [(none)]> show hostname;
+----------+
| hostname |
+----------+
| dev      |
+----------+
1 row in set (0.011 sec)

MySQL [(none)]>
```

恭喜！您的插件运行良好，并且允许您使用 `SHOW hostname` 查询获取主机名。

## 结论

希望您喜欢这篇文章，并加深了对新 Buddy 可插拔架构的理解，该架构已包含在 [Linux 开发包](https://manual.manticoresearch.com/nightly) 中，并且 **即将在下一个版本中发布**（在 Manticore 6.0.4 之后，因此如果您在阅读时有更新的版本，很可能已经包含）。您可以按照我们提供的说明立即开始开发您的插件。虽然这只是对可插拔系统的初步介绍，但我们计划创建并发布一篇关于开发复杂插件的更高级文章。不要犹豫在我们的 [Slack](https://slack.manticoresearch.com/) 或 [Telegram](https://t.me/manticoresearch_en) 群组中向我们提问，并随时建议复杂插件教程的主题。

记得查看我们的 [核心插件代码](https://github.com/manticoresoftware/?q=buddy-plugin&type=all&language=&sort=)，以深入了解更复杂情况下的工作原理。

此致敬礼，  
Manticore 团队。
