Repository: WGrape/esupdater Branch: v2.x Commit: a550e67a6ece Files: 58 Total size: 100.8 KB Directory structure: gitextract_gfvoa9pt/ ├── .editorconfig ├── .github/ │ ├── CODE_OF_CONDUCT.md │ └── workflows/ │ └── release.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── bootstrap.php ├── composer.json ├── config/ │ ├── consumer.php │ ├── db.php │ ├── es.php │ ├── event.php │ ├── log.php │ └── test.php ├── doc/ │ ├── APPLICATION.md │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── HELP.md │ ├── HOWTOCODE.md │ └── README.md ├── esupdater.php ├── framework/ │ ├── Canal.php │ ├── Consumer.php │ ├── Environment.php │ ├── Listener.php │ ├── Logger.php │ ├── Manager.php │ └── Timer.php ├── install/ │ ├── README.md │ ├── container/ │ │ └── kafka.sh │ ├── image/ │ │ ├── Dockerfile │ │ ├── README.md │ │ └── make.sh │ └── install.sh ├── plugin/ │ ├── README.md │ ├── autogeneratecallback/ │ │ ├── README.md │ │ ├── autogeneratecallback.php │ │ ├── handler.template │ │ └── service.template │ └── loader.php ├── restart.sh ├── start.sh ├── stop.sh └── test/ ├── BaseTest.php ├── README.md ├── prepare-commit-msg ├── report/ │ ├── index.css │ └── index.html ├── run.php └── testcases/ └── framework/ ├── TestCanal.php ├── TestCommand.php ├── TestConsumer.php ├── TestEnvironment.php ├── TestManager.php └── TestTimer.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) ================================================ FILE: .github/workflows/release.yml ================================================ on: push: branches: - v2.x name: Release jobs: release-please: runs-on: ubuntu-latest steps: - uses: GoogleCloudPlatform/release-please-action@v3 id: release with: token: ${{ secrets.RELEASE_TOKEN }} release-type: node package-name: posture changelog-types: '[{"type": "types", "section":"Types", "hidden": false},{"type": "revert", "section":"Reverts", "hidden": false},{"type": "feat", "section": "Features", "hidden": false},{"type": "fix", "section": "Bug Fixes", "hidden": false},{"type": "improvement", "section": "Feature Improvements", "hidden": false},{"type": "docs", "section":"Docs", "hidden": false},{"type": "doc", "section":"Docs", "hidden": false},{"type": "style", "section":"Styling", "hidden": false},{"type": "refactor", "section":"Code Refactoring", "hidden": false},{"type": "perf", "section":"Performance Improvements", "hidden": false},{"type": "test", "section":"Tests", "hidden": false},{"type": "build", "section":"Build System", "hidden": false},{"type": "ci", "section":"CI", "hidden":false}]' ================================================ FILE: .gitignore ================================================ .git .idea logs .DS_Store composer.lock # Ignore directories of /app except common and alpha. app/* !app/common/ !app/alpha/ # Ignore directories of /test/testcases/app except common and alpha. test/testcases/app/* !test/testcases/app/common/ !test/testcases/app/alpha/ # Ignore runtime directory. runtime/ # Ignore vendor directory. vendor/ ================================================ FILE: .travis.yml ================================================ # CI was triggered at pull request language: php php: - 7.0 - nightly script: - sudo mkdir -p /home/log/esupdater/ - sudo chmod -R 777 /home/log/ - composer install - php test/run.php branches: only: - v1.x - v2.x ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [2.1.1](https://github.com/WGrape/esupdater/compare/v2.1.0...v2.1.1) (2022-09-19) ### Docs * fix version ([cc7f225](https://github.com/WGrape/esupdater/commit/cc7f225357253fbaa48028e9192666a6ebe89602)) ## [2.1.0](https://github.com/WGrape/esupdater/compare/v2.0.5...v2.1.0) (2022-09-19) ### Features * phpkafka镜像推到DockerHub中提供甚至无需安装都即可使用的镜像 ([d6f56cd](https://github.com/WGrape/esupdater/commit/d6f56cdd7e5086c15a51abe9cccf86844a9cfc82)) * 添加插件扩展功能-完成第一个扩展autogeneratecallback ([e1408c5](https://github.com/WGrape/esupdater/commit/e1408c5b8aa9c9dfa3bc89092026974806197069)) ### Bug Fixes * 修复事件中未获取到返回值的问题 ([480487a](https://github.com/WGrape/esupdater/commit/480487a1897f93cbff68d81d07ffe879d6fb5a57)) * 修复注释错误 ([2a1d121](https://github.com/WGrape/esupdater/commit/2a1d121d5a96d533887e498c4de9b64100d3f61a)) ### Tests * 添加环境变量的测试用例 ([f9e4b4f](https://github.com/WGrape/esupdater/commit/f9e4b4fe867889f398f3ec175af0d5dfc16de4a0)) ### Docs * QUESTION文档移至issues ([af874e5](https://github.com/WGrape/esupdater/commit/af874e5c5fe335ca34db04d92864c0fc72a73db4)) * README添加贡献者列表 ([3dec42a](https://github.com/WGrape/esupdater/commit/3dec42a2f874b45b5c3fd2f3a5d19d8507d2941b)) * 优化文档 ([995a288](https://github.com/WGrape/esupdater/commit/995a2889364551768b142b996b3f7d9172a531db)) * 修改文档描述 ([649e50a](https://github.com/WGrape/esupdater/commit/649e50a74780cf02d27788d1daec160b239b8246)) * 完善README文档 ([2192ef4](https://github.com/WGrape/esupdater/commit/2192ef4eb8abb13ff5e8c88243e614c05840e218)) * 完善业务接入文档和贡献文档 ([58042b0](https://github.com/WGrape/esupdater/commit/58042b07ab6c25f7ca042fe09f6c440631f91055)) * 完善文档-强调esupdater对数据的二次处理功能 ([35e0feb](https://github.com/WGrape/esupdater/commit/35e0feb782d294fcc96dc73216815a72954f3aa1)) * 添加runtime目录可类比为/proc目录的介绍 ([63469dc](https://github.com/WGrape/esupdater/commit/63469dc6fe67773022154cd3dcb07f47ffa8163d)) ================================================ FILE: Dockerfile ================================================ FROM phpkafka # If you failed to install phpkafka, you can use the lvsid/phpkafka:v1.0 in dockerhub: https://hub.docker.com/repository/docker/lvsid/phpkafka # FROM lvsid/phpkafka:v1.0 WORKDIR /dist COPY . /dist/ RUN mkdir -p /home/log/esupdater \ && composer install --quiet # Do not run start command here, because it means the container is equal consumer process, # once the consumer was stopped, the container would exit, # so the workers would not stopped safely. # CMD ["php", "/dist/esupdater.php", "start"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2021 WGrape Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
一个基于Canal实现ES文档增量更新的高性能轻量框架
### 1、轻量级框架
无论安装使用,还是代码设计,整个框架都非常轻量,优雅的完成数据二次处理和ES增量更新。
### 2、全面容器化
为解决各种依赖安装的复杂麻烦问题,已实现全面容器化,只需一条命令就可以轻松安装、部署、和维护。
### 3、事件驱动化
基于框架内部的事件驱动设计,可以轻松地注册不同数据表的变更事件和回调,优雅地实现增量更新。
### 4、插件化扩展
在不影响框架内部运行的前提下,支持插件化扩展,实现对内部行为的自定义扩展。
### 5、高性能消费
通过一个```Consumer```进程和多个```Worker```进程的一对多通信模型,最少提高10倍的吞吐量,实现高性能消费。
## 二、快速上手
> 预计只需要 **3分钟** 即可完成 !
以下操作中会依赖Docker,所以请先安装并启动它。如果只是试用则强烈建议你全程使用在线Docker网站,按如下步骤安装即可,非常方便。
### 1、获取项目
通过```git clone```或下载Release包即可获取项目,如果出错请参考[获取过程帮助](doc/HELP.md#12)文档。
```bash
git clone https://github.com/WGrape/esupdater
cd esupdater
```
### 2、开始安装
执行```install```目录下的```install.sh```安装脚本时,需要传递如下参数以实现[设置环境变量](./doc/APPLICATION.md#3)。如果出错请参考[安装过程帮助](doc/HELP.md#13)文档。
- ```your_local_ip``` :本机IP参数,通过```ifconfig```查看,通常为192.168开头,而不是127.0.0.1
```bash
cd install
bash install.sh ${your_local_ip}
cd ..
```
### 3、运行项目
安装成功后,执行根目录下的```start.sh```启动脚本即可。如果出错请参考[运行过程帮助](doc/HELP.md#3)文档。
```bash
bash start.sh
# 查看日志输出
tail -f /home/log/esupdater/debug.log.20220111
```
### 4、测试运行
在另一个窗口进入```kafkaContainer```容器中,按如下操作启动```Kafka生产者```
```bash
docker exec -it kafkaContainer /bin/bash
cd /opt/kafka/
# 启动时可能会出现warn, 忽略即可
./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic default_topic
```
启动成功后会进入一个生产消息的命令行,发送任意消息后,查看上一步日志中的输出,如果出现如下类似日志则说明服务已经成功运行 !
## 三、业务接入
如果需要在你的业务中接入此项目,请参考[应用接入文档](./doc/APPLICATION.md)。
## 四、扩展列表
基于插件化扩展开发,项目提供了一系列开箱即用的扩展。
### 1、AutoGenerateCallback
一个自动生成```Handler```和```Service```的事件回调模块的扩展。具体使用见[使用介绍](./plugin/autogeneratecallback/README.md)
## 五、关于项目
### 1、深入了解
如果想要深入了解本项目,在 [doc目录](./doc) 下提供了如下丰富完善的项目文档,欢迎阅读。
- [APPLICATION](doc/APPLICATION.md) :帮助你快速在业务中接入此项目
- [HOWTOCODE](doc/HOWTOCODE.md) :更深的了解项目,包括架构设计、底层原理
- [HELP](doc/HELP.md) :解决安装和部署过程中问题的帮助手册,包括镜像制作帮助、容器部署帮助等
### 2、参与项目
项目源码设计简单易懂,如有更好的想法,可参考[如何贡献](doc/CONTRIBUTING.md)文档,期待提出宝贵的 [Pull request](https://github.com/WGrape/esupdater/pulls) 。
如果在了解和使用过程中,有任何疑问,也欢迎提出宝贵的 [Issue](https://github.com/WGrape/esupdater/issues/new) 。
开源不易,如果支持本项目 **欢迎Star !** 以激励维护和更新的动力。
## 六、贡献列表
所有对本项目有过重要贡献的用户,会收录在此贡献者列表中。
- 感谢 [sick-cat](https://github.com/sick-cat) 提出的Issue :[启动配置](https://github.com/WGrape/esupdater/issues/41)
- 感谢 [onser3](https://github.com/onser3) 提出的Issue :[自动生成handler和service层](https://github.com/WGrape/esupdater/issues/44)
================================================
FILE: bootstrap.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
// PHP Configuration.
date_default_timezone_set('Asia/Shanghai');
// Define path constants.
const ROOT_PATH = __DIR__ . '/';
const APP_PATH = ROOT_PATH . 'app/';
const CONFIG_PATH = ROOT_PATH . 'config/';
const FRAMEWORK_PATH = ROOT_PATH . 'framework/';
const RUNTIME_PATH = ROOT_PATH . 'runtime/';
const VENDOR_PATH = ROOT_PATH . 'vendor/';
// Define file constants.
const RUNTIME_ESUPDATER_CONSUMER_PID_FILE = RUNTIME_PATH . 'esupdater-consumer.pid';
const RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE = RUNTIME_PATH . 'esupdater-consumer.status';
const RUNTIME_IGNORE_ERROR_TEMP_FILE = RUNTIME_PATH . 'ignore-error.temp';
const CREATE_WORKER_LOG_FILE = RUNTIME_PATH . 'create-worker.log';
const RUNTIME_ESUPDATER_WORKER_PID_FILE_PREFIX = 'esupdater-worker-';
const COMPOSER_AUTOLOAD_FILE = VENDOR_PATH . 'autoload.php';
const ENVIRONMENT_FILE = ROOT_PATH . '.env';
// Define data constants.
const DEFAULT_PID = 0;
// Load config files.
include_once CONFIG_PATH . 'consumer.php';
include_once CONFIG_PATH . 'db.php';
include_once CONFIG_PATH . 'es.php';
include_once CONFIG_PATH . 'log.php';
include_once CONFIG_PATH . 'event.php';
/**
* Include composer autoload file or register autoload.
*/
if (!file_exists(COMPOSER_AUTOLOAD_FILE)) {
function autoloadCallback(string $classname)
{
$classname = str_replace('\\', '/', $classname);
$file = ROOT_PATH . "{$classname}.php";
if (file_exists($file)) {
include_once $file;
} else {
echo 'class file' . $classname . 'not found!';
}
}
spl_autoload_register("autoloadCallback", true, true);
} else {
include_once COMPOSER_AUTOLOAD_FILE;
}
/**
* Register shutdown callback.
*/
function shutdownCallback()
{
$manager = new \framework\Manager();
// Delete the files of consumer process if it exited without delete files.
if ($manager->isConsumerProcess()) {
if (file_exists(RUNTIME_ESUPDATER_CONSUMER_PID_FILE)) {
unlink(RUNTIME_ESUPDATER_CONSUMER_PID_FILE);
}
if (file_exists(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE)) {
unlink(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE);
}
}
// Delete the pid file of worker process if it exited without delete files.
$workerPIDFile = $manager->isWorkerProcess();
if ($workerPIDFile !== false && file_exists($workerPIDFile)) {
unlink($workerPIDFile);
}
}
register_shutdown_function('shutdownCallback');
/**
* Register exception callback.
*
* @param Throwable $exception
*/
function exception_handler(Throwable $exception = null)
{
// do something.
}
set_exception_handler('exception_handler');
/**
* Register error callback.
*
* @param int $errNo
*
* @param string $errMessage
*
* @param string $errFile
*
* @param int $errLine
*/
function error_handler(int $errNo, string $errMessage, string $errFile, int $errLine)
{
// do something.
}
set_error_handler('error_handler');
================================================
FILE: composer.json
================================================
{
"name": "wgrape/esupdater",
"description": "A high-performance lightweight framework of PHP to achieve incremental update of ES documents.",
"license": "MIT",
"authors": [
{
"name": "wgrape",
"email": "wgrapeu@gmail.com"
}
],
"require": {},
"autoload": {
"psr-4": {
"app\\": "app/",
"framework\\": "framework/",
"test\\": "test/"
}
}
}
================================================
FILE: config/consumer.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$consumer = [
'check_status_interval_seconds' => 2,
'broker_list_string' => '',
'partition' => 0,
'timeout_millisecond' => 2 * 1000,
'group_id' => 'default_group',
'topic' => 'default_topic',
'max_worker_count' => 10,
];
================================================
FILE: config/db.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$db = [
'database' => [
'host' => '',
'port' => 3306,
'username' => '',
'password' => '',
'database' => '',
'charset' => 'utf8mb4',
]
];
================================================
FILE: config/es.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$es = [
'host' => '',
'port' => '',
'user_password' => '',
'doc_type' => '_doc'
];
================================================
FILE: config/event.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$event = [
// You can choose the autoCallback, it's very simple.
// 'alpha.user' => '\app\alpha\user\UserHandler',
// You can also choose the manualCallback, it's a bit complicated but powerful.
'alpha.user' => [
'onInsert' => [
'callback' => function ($parsedCanalData) {
return (new \app\alpha\user\UserHandler)->onInsert($parsedCanalData);
},
],
'onUpdate' => [
'filter' => function ($parsedCanalData) {
// Return false if you need skip this kind of canal data.
if (!isset($parsedCanalData['data'][0]) || $parsedCanalData['data'][0]['id'] < 10000000) {
return false;
}
// Return the filtered canal data.
$parsedCanalData['data'][0]['name'] .= '_filtered';
return $parsedCanalData;
},
'callback' => function (array $parsedCanalData) {
return (new \app\alpha\user\UserHandler)->onUpdate($parsedCanalData);
},
'finally' => function ($filterResult, $callbackResult) {
$filterSuccess = $filterResult ? 'success' : 'failed';
$callbackSuccess = $callbackResult ? 'success' : 'failed';
\framework\Logger::logInfo("Work finally: alpha.user.onInsert.filter is {$filterSuccess}, alpha.user.onInsert.callback is {$callbackSuccess}");
},
],
'onDelete' => [
'callback' => function (array $parsedCanalData) {
return (new \app\alpha\user\UserHandler)->onDelete($parsedCanalData);
},
],
],
];
================================================
FILE: config/log.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$log = [
'debug' => '/home/log/esupdater/debug.log',
'info' => '/home/log/esupdater/info.log',
'slow' => [
'millisecond' => 500,
'path' => '/home/log/esupdater/slow.log',
],
'warning' => '/home/log/esupdater/warning.log',
'error' => '/home/log/esupdater/error.log',
'fatal' => '/home/log/esupdater/fatal.log',
];
================================================
FILE: config/test.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
$test = [
'testcases_directory' => 'test/testcases/',
];
================================================
FILE: doc/APPLICATION.md
================================================
# 业务接入文档
- [一、快速接入](#1)
- [1、修改配置](#11)
- [2、创建应用](#12)
- [3、创建事件回调](#13)
- [4、注册事件回调](#14)
- [5、部署项目](#15)
- [二、应用配置](#2)
- [1、消费配置](#21)
- [2、数据库配置](#22)
- [3、ES配置](#23)
- [4、日志配置](#24)
- [5、事件配置](#25)
- [6、单测配置](#26)
- [三、系统变量](#3)
- [四、部署管理](#4)
- [1、容器化部署](#41)
- [2、运行时配置](#42)
- [五、单元测试](#5)
- [1、手动测试](#51)
- [2、自动测试](#52)
- [3、添加用例](#53)
- [4、测试报告](#54)
## 一、快速接入
### 1、修改配置
只需要修改 [consumer.php](./config/consumer.php) 配置文件中的```broker_list_string```、```group_id```、```topic```这三个必须的配置项即可, 否则无法正常消费数据。
其他非必须的配置请参考[应用配置](#2)文档。
### 2、创建应用
在```/app/```目录下,创建一个以业务为命名规范的应用名称,如```/app/alpha/```。
### 3、创建事件回调
在上一步中创建的应用目录下,创建一个```Handler```事件回调类
- [/app/alpha/user/UserHandler.php](./app/alpha/user/UserHandler.php) :作用类似 ```Controller```
如果需要在事件回调中做大量复杂的业务操作,可以创建一个对应的```Service```业务处理类 :
- [/app/alpha/user/UserService.php](./app/alpha/user/UserService.php) :作用类似 ```Service```
建议无论业务是否复杂,都把业务放在```Service```中操作。
> 1、在业务Service中可以自由的调用```common```应用下的```DBService```、```ESService```等服务
>
> 2、如果业务更复杂,可以考虑在应用目录下设计属于自己的业务分层,如```daos```、```services```等
>
> 3、你可以直接选择使用 [AutoGenerateCallback扩展](/plugin/autogeneratecallback/) 实现事件回调模块的自动创建,免去手动操作此步的过程 !
### 4、注册事件回调
在```/config/event.php```配置文件中添加一个新的键值对,表示当```数据库.数据表```出现变更事件时,由对应的```事件Handler```响应处理。
```php
$event = [
// 当alpha数据库中的user表发生INSERT/UPDATE/DELETE事件时,
// 系统会自动创建\app\alpha\user\UserHandler事件回调类,
// 并根据不同的事件类型调用不同的方法, 如INSERT事件则调用回调类的onInsert()方法
'alpha.user' => '\app\alpha\user\UserHandler',
];
```
除此之外,框架还支持更加强大的事件注册和驱动机制,如果需要请参考[高级事件配置](#251)。
### 5、部署项目
至此业务接入部分已经完成,参考 [部署管理](#3) 部分部署代码即可。
## 二、应用配置
### 1、消费配置
配置文件 ```/config/consumer.php```,设置Kafka的消费配置
```php
2,
// broker服务器列表,如果多个则以逗号分割,如192.168.0.18:9092,192.168.0.18:9093
'broker_list_string' => '192.168.0.18:9092',
// 消费分区
'partition' => 0,
// 消费超时时间, 单位毫秒
'timeout_millisecond' => 2 * 1000,
// 消费组id
'group_id' => '',
// 消费主题
'topic' => '',
// worker的最大进程数
'max_worker_count' => 10,
];
```
### 2、数据库配置
配置文件 ```/config/db.php```,设置访问数据库的配置
```php
[
'host' => '数据库地址',
'port' => 3306,
'username' => '用户名',
'password' => '密码',
'database' => '数据库',
'charset' => 'utf8mb4',
]
];
```
### 3、ES配置
配置文件 ```/config/es.php```,设置访问ES的配置
```php
'ES服务host',
'port' => 'ES服务端口',
'user_password' => 'ES服务凭证',
'doc_type' => '_doc'
];
```
### 4、日志配置
> 在```/start.sh```启动脚本中,```docker run -v ...``` 会把容器中配置的日志目录挂载到本机相应目录中
配置文件 ```/config/log.php```,配置了不同日志级别的文件路径,如下所示
```php
'/home/log/esupdater/debug.log',
'info' => '/home/log/esupdater/info.log',
'slow' => [
'millisecond' => 500, // work进程处理耗时超过500ms则记录慢日志
'path' => '/home/log/esupdater/slow.log',
],
'warning' => '/home/log/esupdater/warning.log',
'error' => '/home/log/esupdater/error.log',
'fatal' => '/home/log/esupdater/fatal.log',
];
```
### 5、事件配置
配置文件 ```/config/event.php```,如下所示
- Key :```数据库名.表名```
- Value : ```Handler```
表示当此数据表的数据更新时,由对应的```Handler```处理
```php
'\app\alpha\user\UserHandler',
];
```
#### (1) 高级事件配置
上面的这种Key所对应的Value为字符串的配置方式,是一种简单的自动回调配置。 如果Value是Map时,就会使用高级事件配置。
这个Map会再次以如```onInsert```、```onUpdate```、```onDelete```不同的事件为key,value则由以下几种回调函数组成,分别为 :
- ```filter``` 过滤器 \[可选\] :实现对Canal数据的过滤处理、对事件回调的拦截
- ```callback``` 事件回调 \[可选\] :实现事件的回调处理
- ```finally``` 末尾执行 \[可选\] :实现事件的兜底处理,可用于统计数据、记录日志等
关于高级事件配置可以参考 [高级配置示例](../config/event.php) 。
### 6、单测配置
配置文件 ```config/test.php```,如下所示
```php
'test/testcases/',
];
```
## 三、系统变量
在```/.env```文件中记录了服务所需要的所有系统变量,在执行```install.sh```安装脚本时完成系统变量的设置,并由```/framework/Environment.php```类解析并处理。
## 四、部署管理
### 1、容器化部署
如果部署过程中出错,请参考[容器部署帮助](HELP.md#3)文档。
#### (1) 启动
```bash
bash ./start.sh
```
#### (2) 停止
```bash
bash ./stop.sh
```
#### (3) 重启
```bash
bash ./restart.sh
```
### 2、运行时配置
可以在```/start.sh```脚本中执行```docker run```时设置```核心数```、```目录挂载```等参数,请自定义修改。
如果需要设置更多的容器参数,可以参考[官方文档](https://docs.docker.com/config/containers/resource_constraints/) 。
| Id | 配置名称 | 配置参数 | 参数值 | 默认值 | 释义 |
| --- | :----: | :----: | :---: | :---: | :---: |
| 1 | 核心数 | --cpus | float | 1.5 | 设置允许的最大核心数 |
| 2 | CPU核心集 | --cpuset-cpus | int | 未设置 | 设置允许执行的CPU核心 |
| 3 | 内存核心集 | --cpuset-mems | int | 未设置 | 设置使用哪些核心的内存 |
| 4 | 目录挂载 | -v | string | /home/log/esupdater | 设置容器挂载的目录 |
## 五、单元测试
根目录下的```/test```目录是单元测试目录,其中有一个```/test/run.php```入口文件,它会自动执行 [testcases_directory](HOWTOCODE.md#36) 目录下所有的测试用例。
### 1、手动测试
```bash
php test/run.php
```
### 2、自动测试
```bash
cp test/prepare-commit-msg ./.git/hooks
chmod +x .git/hooks/prepare-commit-msg
# 此后提交代码会自动执行单元测试,只有单测成功才会允许提交代码
git add .
git commit -m "add: xxx"
```
如下图实际使用中,每次Commit代码会自动执行测试。
### 3、添加用例
在```test/testcases/app```目录下,先创建应用目录(如```alpha```),然后在此目录下以```Test*```开头创建单测文件即可,具体内容可参考 [TestUserService](../test/testcases/app/alpha/TestUserService.php) 单测文件
### 4、测试报告
在测试运行结束后,会自动生成一个测试报告```/test/report/index.html```文件,点击这里查看报告
================================================
FILE: doc/CHANGELOG.md
================================================
### 2、版本 :v2.0.5
发布日期 :2021-01-11
#### 修复
- [重大更新更容易上手-添加系统变量机制](https://github.com/WGrape/esupdater/commit/551c5585d878ec3ebe72c142cb294aa37c64ed29)
- [完善启动脚本中判断是否已启动的逻辑](https://github.com/WGrape/esupdater/commit/495a0bf15ed8dbe696bccd41c1c2095ab10eea7d)
#### 特性
- [完善重要更新-简化你的上手流程](https://github.com/WGrape/esupdater/commit/2a430f008f0f604624ea0fa6fbd07954b2e4f152)
- [重大更新更容易上手-添加系统变量机制](https://github.com/WGrape/esupdater/commit/551c5585d878ec3ebe72c142cb294aa37c64ed29)
- [内置自动创建kafka容器的脚本](https://github.com/WGrape/esupdater/commit/49f6327cab60102172d071cce84105f6978200b3)
#### 完善
- 多个文档的完善优化
### 1、版本 :v2.0.0-beta
发布日期 :2021-01-04
#### 修复
- 修复制作镜像失败时误提示成功的bug :[PR](https://github.com/WGrape/esupdater/pull/37)
#### 特性
- 支持composer :[PR](https://github.com/WGrape/esupdater/pull/37)
#### 完善
- 完善文档
================================================
FILE: doc/CONTRIBUTING.md
================================================
### 目录
- [1、如何报告问题](#1)
- [2、如何提交PR](#2)
- [3、如何理解项目](#3)
- [4、代码提交规约](#4)
- [1、单测通过](#41)
- [2、commit message 规范](#42)
- [3、分支管理](#43)
- [5、打包Release](#5)
- [6、版本对比](#6)
- [7、项目数据](#7)
## 1、如何报告问题
如果在了解和使用过程中,有任何疑问,非常欢迎提出宝贵的 [Issue](https://github.com/WGrape/esupdater/issues/new)
## 2、如何提交PR
PR的提交不限制范围,如代码、文档等修改均在允许范围内,可 [参考这里](https://github.com/WGrape/esupdater/commit/186e229308463aa745c6b1cbfd02f77bc62ab9d4) 的PR提交
## 3、如何理解项目
在[HOWTOCODE](HOWTOCODE.md)文档中介绍了详细的实现原理和设计,帮助你了解项目
## 4、代码提交规约
在提交代码前,至少需要做到以下几项
### (1) 单测通过
整个项目的单元测试必须通过
### (2) commit message 规范
规范使用如```fix: 修复Logger中记录日志时间错误的bug```这种组合的提交规范
- fix: 修复bug相关
- doc: 文档完善相关
- refactor: 重大功能重构
- feat: 新功能、新组件等
- test: 新增测试或测试相关的修改
- style: 调整代码格式等对功能和性能无较大影响的修改
- chore: 构建过程或辅助工具的变动,如dockerfile的修改
### (3) 分支管理
```v1```版本的开发提交到```v1.x```分支,```v2```版本的开发提交到```v2.x```分支,且```CI```检查通过
### (4) 提交内容注释
对于重要代码部分,请以评论的方式写清楚原因,可以参考 [test: 添加环境变量的测试用例](https://github.com/WGrape/esupdater/commit/f9e4b4fe867889f398f3ec175af0d5dfc16de4a0) 、[feat: 支持Composer和修复制作镜像失败时误提示成功的bug](https://github.com/WGrape/esupdater/pull/37/files#r800161416)
## 5、打包Release
基于```v1.x```和```v2.x```分支分别打包不同的Release版本。
## 6、版本对比
### (1) Composer
| 主版本号 | Composer | 优势 | 劣势 |
| --- | :----: | :----: | :----: |
| v1.x | 不支持 | 不需要安装Composer也可以用 | 可能无法正常使用外部依赖 |
| v2.x | 支持 | 可以方便的调用外部依赖 | 本地开发时需要安装Composer |
## 7、项目数据
## 二、底层原理
ESUpdater的核心由```Consumer```进程和```Worker```进程组成,其中根目录下的```/esupdater.php```为入口文件
### 1、生命周期
```Consumer```进程和```Worker```进程的生命周期都是由命令控制
#### (1) Consumer
```Consumer```进程由```php esupdater.php start```命令启动,由```php esupdater.php stop```命令停止
#### (2) Worker
当```Consumer```进程从Kafka中拿到消息后,会通过```exec```的方式执行```php esupdater work```命令,以启动一个新的PHP进程,即```Worker```进程。
```Worker```进程会分为后台和非后台两种执行方式,使用哪种执行方式取决于当前```Worker```进程的数量,如果少于配置的```max_worker_count```会使用后台执行的方式,否则使用非后台执行的方式。通过这种方式可以在加快消费速度的同时,保证稳定性。
所以Worker进程的启动完全由```Consumer```控制,如果想要停止```Worker```进程,必须先停止```Consumer```进程,然后等待```Worker```进程正常执行结束即可
### 2、命令执行
#### (1) start
当使用```php esupdater.php start```命令时,会启动一个进程,这个进程会以阻塞主进程的方式订阅Kafka消息,所以这个进程叫做```Consumer```进程
```Consumer```进程启动后会先在```/runtime```目录下写```/runtime/esupdater-consumer.pid```文件和```esupdater-consumer.status```文件,分别记录进程它的进程ID和消费状态```start```。
在```Consumer```进程消费kafka消息的同时,会每隔配置的```check_status_interval_seconds```时间检测一次消费状态(```esupdater-consumer.status```文件),当消费状态变为```stop```时,进程会停止消费,此时```Consumer```进程会完全结束。
#### (2) stop
当使用```php esupdater.php stop```命令时,会启动一个进程,这个进程会向```/runtime/esupdater-consumer.status```文件中写入```stop```指定。
然后每隔一秒钟就会检测```Consumer```进程和```Worker```进程是否都已经完全结束,如果已经检测10秒钟还未完全结束就会通知停止失败,否则停止成功。
#### (3) work
当```Consumer```进程使用```php esupdater work```命令启动```Worker```进程时,```Worker```进程会记录下```/runtime/esupdater-worker-{pid}.pid```进程ID文件,只有当结束后才会删除此文件。
### 3、binlog数据处理过程
处理过程为```binlog => canalData => urlencode(canalData)```,可以参考文件 [/framework/Canal.php](../framework/Canal.php)
1. Canal将```binlog```数据解析为```json```格式并投递至kafka
2. Consumer进程消费kafka,使用```urlencode```方式编码获取到的消息数据
3. Consumer进程把编码后的消息数据,传递至Worker进程
4. Worker进程再依次拆解数据即可
### 4、文件目录规范
#### (1) 目录结构
- ```app```目录 :应用目录
- ```config```目录 :项目的唯一配置入口
- ```doc```目录 :项目文档目录
- ```framework```目录 :项目的核心框架目录
- ```install```目录 :安装目录
- ```runtime```目录 :服务运行时产生的中间文件目录,如PID文件,但不包括日志文件。设计思想基于[/proc/](https://en.wikipedia.org/wiki/Procfs)
- ```test```目录 :单元测试目录
- ```/```目录 :根目录下存放所有上述目录,和必要的一级文件如```.gitignore```文件
#### (2) 文件规范
- ```shell```脚本不能省略```.sh```后缀,且统一以```bash xxx.sh```的方式执行
- 文档统一以大写英文命名,如```README.md``` / ```HELP.md```
### 5、程序设计规范
关于设计规范可以参考文章 [漫谈编程之编程规范](https://github.com/WGrape/Blog/issues/25)
- 调用类的时候使用命名空间前缀,不使用在头部声明```use```的方式
## 三、部署过程
> 容器化部署方案依赖于```phpkafka```镜像,所以请确保```phpkafka```镜像已经生成。为了避免重复构建耗时,建议把```phpkafka```镜像推到Docker远程仓库中。
容器构建主要通过根目录下的```/Dockerfile```镜像文件,它会基于```phpkafka```镜像构建一个新的镜像,名为```esupdater```。
### 1、启动
当执行如下命令时,会使用```/Dockerfile```文件创建```esupdater```镜像,并创建```esupdaterContainer```容器,最后通过在容器中执行```php esupdater.php start```命令实现服务的启动
```bash
bash ./start.sh
```
启动成功后,除命令行输出```Start success```外,在宿主机```/home/log/esupdater/info.log.{date}```日志中会输出启动日志,如下图所示
### 2、停止
当执行以下命令时,会先在容器中执行```php esupdater.php stop```命令,等待容器内```Consumer```进程和```Worker```进程全部停止后,删除镜像和容器
```bash
bash ./stop.sh
```
停止成功后,除命令行输出```Stop success```外,同样的在宿主机```/home/log/esupdater/info.log.{date}```日志中会输出停止成功日志,如下图所示
### 3、重启
当执行以下命令时,会先执行```bash stop.sh```命令,再执行```bash start.sh```命令,以防止出现重复启动的问题
```bash
bash ./restart.sh
```
## 四、参考文档
- 有关```php-rdkafka```的配置可以 [参考文档](https://github.com/arnaud-lb/php-rdkafka)
- 有关```librdkafka```的配置可以 [参考文档](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md)
- 有关```PHP Kafka```类的使用可以 [参考文档](https://arnaud.le-blanc.net/php-rdkafka-doc/phpdoc/class.rdkafka-consumertopic.html)
================================================
FILE: doc/README.md
================================================
### What is this directory
It's the directory of different docs.
================================================
FILE: esupdater.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
include_once 'bootstrap.php';
$command = strtolower(isset($argv[1]) ? $argv[1] : '');
if (empty($command)) {
echo "Command empty!\n";
return;
}
$manager = new \framework\Manager();
switch ($command) {
case "start":
$manager->commandStart();
break;
case "stop":
$success = $manager->commandStop();
echo "{$success}\n";
break;
case "work":
$canalData = isset($argv[2]) ? $argv[2] : "";
if (empty($canalData)) {
return;
}
$success = $manager->commandWork($canalData);
echo "{$success}\n";
break;
default:
echo "Not support command: {$command}\n";
return;
}
================================================
FILE: framework/Canal.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Canal
{
/**
* Encode the canal data to string.
*
* @param string $unEncodedCanalData the json format data in kafka queue (canal put it into kafka)
*
* @return string
*/
public function encode(string $unEncodedCanalData): string
{
return urlencode($unEncodedCanalData);
}
/**
* Decode the canal data to string.
*
* @param string $encodedCanalData the urlencoded canal data
*
* @return string
*/
public function decode(string $encodedCanalData): string
{
return urldecode($encodedCanalData);
}
/**
* Parse the canal data to array.
*
* @param string $encodedCanalData the urlencoded canal data
*
* @return array
*/
public function parse(string $encodedCanalData): array
{
return json_decode($this->decode($encodedCanalData), true);
}
/**
* Check the canal data format.
*
* @param array $parsedCanalData
*
* @return bool
*/
public function checkParsedCanalData(array $parsedCanalData): bool
{
if (empty($parsedCanalData) || empty($parsedCanalData['data'])) {
return false;
}
if (!isset($parsedCanalData['database']) || !isset($parsedCanalData['table']) || !isset($parsedCanalData['type'])) {
return false;
}
if (!isset($parsedCanalData['id']) || !isset($parsedCanalData['ts'])) {
return false;
}
return true;
}
}
================================================
FILE: framework/Consumer.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Consumer
{
const TIMER_MARK = 'consume';
const CONSUMER_EXIT_WITH_EMPTY_STRING = '';
const START_FLAG_STRING = 'start';
const STOP_FLAG_STRING = 'stop';
private $checkStatusIntervalSeconds;
private $brokerListString;
private $partition;
private $timeoutMillisecond;
private $groupId;
private $topic;
private $maxWorkerCount;
public function __construct($consumer)
{
$this->checkStatusIntervalSeconds = (isset($consumer['check_status_interval_seconds']) && !empty($consumer['check_status_interval_seconds'])) ? $consumer['check_status_interval_seconds'] : 2;
$this->brokerListString = (isset($consumer['broker_list_string']) && !empty($consumer['broker_list_string'])) ? $consumer['broker_list_string'] : ($this->getLocalIP() . ':9092');
$this->partition = (isset($consumer['partition']) && !empty($consumer['partition'])) ? $consumer['partition'] : 0;
$this->timeoutMillisecond = (isset($consumer['timeout_millisecond']) && !empty($consumer['timeout_millisecond'])) ? $consumer['timeout_millisecond'] : 2 * 1000;
$this->groupId = (isset($consumer['group_id']) && !empty($consumer['group_id'])) ? $consumer['group_id'] : 'default_group';
$this->topic = (isset($consumer['topic']) && !empty($consumer['topic'])) ? $consumer['topic'] : 'default_topic';
$this->maxWorkerCount = (isset($consumer['max_worker_count']) && !empty($consumer['max_worker_count'])) ? $consumer['max_worker_count'] : 100;
}
/**
* Get localIP by ifconfig command
* @DEPRECATED: It would get error local IP in docker container.
* @return string
*/
public function getLocalIPByIfconfig(): string
{
$localIP = '127.0.0.1';
$command = "ifconfig -a | grep inet | grep -v 127.0.0.1 | grep -v inet6 | grep '192.168' | head -n 1 | awk '{print $2}' | tr -d \"addr:\"";
exec($command, $output);
if (isset($output[0])) {
$localIP = $output[0];
if (stripos($localIP, '192.168') !== 0) {
$localIP = '127.0.0.1';
}
}
return $localIP;
}
/**
* Get local host IP
* @return string
*/
public function getLocalIP(): string
{
$localIP = \framework\Environment::getSystemVariable('ESUPDATER_LOCAL_IP');
if (empty($localIP)) {
$localIP = '127.0.0.1';
}
return $localIP;
}
/**
* Whether need to stop consume or not
* @return bool
*/
public function isNeedStop(): bool
{
return file_get_contents(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE) === self::STOP_FLAG_STRING;
}
/**
* Whether need to check status or not
* @return bool
*/
public function isNeedCheckStatus(): bool
{
$millisecond = \framework\Timer::elapsed(self::TIMER_MARK);
return ($millisecond / 1000) >= $this->checkStatusIntervalSeconds;
}
/**
* Get property
* @param string $propertyName
* @return string | false | array | int | mixed
*/
public function getProperty(string $propertyName)
{
return property_exists($this, $propertyName) ? $this->$propertyName : '';
}
/**
* DEPRECATED: low level consuming, but now it's not available
* @param false $onlyForTest
* @return string
*/
public function lowLevelConsuming($onlyForTest = false): string
{
// Create consumer config object
$consumerConfigObject = new \RdKafka\Conf();
$consumerConfigObject->set('group.id', $this->groupId);
// Create topic config object
$topicConfigObject = new \RdKafka\TopicConf();
$topicConfigObject->set('auto.offset.reset', 'smallest');
// Create consumer object
$consumerObject = new \RdKafka\Consumer($consumerConfigObject);
$consumerObject->addBrokers($this->brokerListString);
// Create topic object
$topicObject = $consumerObject->newTopic($this->topic, $topicConfigObject);
// Consume start from last offset
$topicObject->consumeStart($this->partition, RD_KAFKA_OFFSET_STORED);
while (true) {
$message = $topicObject->consume($this->partition, $this->timeoutMillisecond);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
if (is_null($message) || empty($message->payload)) {
echo "Message is null or payload is empty\n";
continue;
}
var_dump($message);
break;
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
echo "No more messages\n";
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
echo "Timeout\n";
break;
default:
echo "Unknown message error\n";
break;
}
}
}
/**
* High level consuming
* @param false $onlyForTest
* @return string
*/
public function highLevelConsuming($onlyForTest = false): string
{
$canal = new Canal();
$manager = new Manager();
// Create topic config object
$topicConfigObject = new \RdKafka\TopicConf();
$topicConfigObject->set('request.required.acks', true);
$topicConfigObject->set('auto.commit.interval.ms', 100);
$topicConfigObject->set('auto.offset.reset', 'smallest');
// Create consumer config object
$consumerConfigObject = new \RdKafka\Conf();
$consumerConfigObject->setRebalanceCb(function (\RdKafka\KafkaConsumer $kafka, $err, array $partitions = null) {
switch ($err) {
case RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS:
$kafka->assign($partitions);
break;
case RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS:
$kafka->assign(NULL);
break;
default:
Logger::logError("Consumer setRebalanceCb occurs exception: {$err}");
}
});
$consumerConfigObject->set('group.id', $this->groupId);
$consumerConfigObject->set('metadata.broker.list', $this->brokerListString);
$consumerConfigObject->setDefaultTopicConf($topicConfigObject);
// Create consumer object
try {
$consumerObject = new \RdKafka\KafkaConsumer($consumerConfigObject);
$consumerObject->subscribe([$this->topic]);
} catch (\Throwable $e) {
Logger::logFatal("Consumer failed to new KafkaConsumer: " . $e->getMessage());
return self::CONSUMER_EXIT_WITH_EMPTY_STRING;
}
// Timer start
\framework\Timer::start(self::TIMER_MARK);
// Consuming loop
while (true) {
// Check consume status
if ($this->isNeedCheckStatus() && $this->isNeedStop()) {
Logger::logInfo('Consumer need to stop');
return self::CONSUMER_EXIT_WITH_EMPTY_STRING;
}
// Fetch message
$message = $consumerObject->consume($this->timeoutMillisecond);
switch ($message->err) {
case RD_KAFKA_RESP_ERR_NO_ERROR:
if (is_null($message) || empty($message->payload)) {
Logger::logDebug('Consumer fetch message is null or payload is empty');
continue;
}
if ($onlyForTest) {
return $message->payload;
}
$logFile = CREATE_WORKER_LOG_FILE;
$canalData = $canal->encode($message->payload);
$count = $manager->getRunningWorkersCount();
if ($count !== false && $count < ($this->maxWorkerCount - 1)) {
Logger::logDebug("Consumer handle message: create non-block worker process, current count is {$count}");
exec("nohup php esupdater.php work {$canalData} >> {$logFile} &");
} else {
Logger::logDebug("Consumer handle message: create block worker process, current count is {$count}");
exec("php esupdater.php work {$canalData} >> {$logFile}");
}
break;
case RD_KAFKA_RESP_ERR__PARTITION_EOF:
Logger::logDebug('Consumer highLevelConsuming fetch no more messages');
break;
case RD_KAFKA_RESP_ERR__TIMED_OUT:
Logger::logDebug('Consumer highLevelConsuming fetch timeout');
break;
default:
Logger::logError('Consumer highLevelConsuming catch unknown message error');
break;
}
}
}
}
================================================
FILE: framework/Environment.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Environment
{
/**
* Variable container.
* @var array
*/
public static $variableContainer = [];
/**
* Get system variable.
* @param $variable
* @return string
*/
public static function getSystemVariable($variable): string
{
$result = '';
if (empty(self::$variableContainer)) {
self::parseEnvFile();
}
if (isset(self::$variableContainer[$variable])) {
$result = self::$variableContainer[$variable];
}
return $result;
}
/**
* Parse the .env file.
*/
public static function parseEnvFile()
{
$file = ENVIRONMENT_FILE;
$handler = fopen($file, 'r+');
while (!feof($handler)) {
$content = trim(fgets($handler));
$slices = explode('=', $content);
if (count($slices) === 2) {
self::$variableContainer[$slices[0]] = $slices[1];
}
}
}
}
================================================
FILE: framework/Listener.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Listener
{
/**
* Database event type: insert.
*/
const TYPE_INSERT = 'INSERT';
/**
* Database event type: update.
*/
const TYPE_UPDATE = 'UPDATE';
/**
* Database event type: delete.
*/
const TYPE_DELETE = 'DELETE';
/**
* Timer mark of work
*/
const TIMER_MARK = 'work';
/**
* Find and
* @param string $canalData
*/
public function dispatch(string $canalData)
{
// Timer start.
Timer::start(self::TIMER_MARK);
// Parse and check canal data.
$canalParser = new Canal();
$parsedCanalData = $canalParser->parse($canalData);
Logger::setLogIdByParsedCanalData($parsedCanalData);
if (!$canalParser->checkParsedCanalData($parsedCanalData)) {
Logger::logError("Check canal data error");
return;
}
// Check the key is valid.
global $event;
$database = $parsedCanalData['database'];
$table = $parsedCanalData['table'];
$key = "{$database}.{$table}";
if (!isset($event[$key])) {
Logger::logError("Not found the valid event config for key: key={$key}");
return;
}
$onWhichEvent = false;
switch ($parsedCanalData['type']) {
case self::TYPE_INSERT:
$onWhichEvent = 'onInsert';
break;
case self::TYPE_UPDATE:
$onWhichEvent = 'onUpdate';
break;
case self::TYPE_DELETE:
$onWhichEvent = 'onDelete';
break;
}
$isAutoCallback = is_string($event[$key]);
if ($isAutoCallback) {
$this->autoCallback($key, $onWhichEvent, $parsedCanalData);
} else {
$this->manualCallback($key, $onWhichEvent, $parsedCanalData);
}
global $log;
$cost = Timer::elapsed(self::TIMER_MARK);
if (isset($log['slow']['millisecond']) && $cost > $log['slow']['millisecond']) {
Logger::logSlow("Work slow: key={$key}, onWhichEvent={$onWhichEvent}, cost={$cost}ms");
}
}
/**
* Auto callback
* @param string $key
* @param string $onWhichEvent
* @param array $parsedCanalData
*/
public function autoCallback(string $key, string $onWhichEvent, array $parsedCanalData)
{
global $event;
if ($onWhichEvent === false || !method_exists($event[$key], $onWhichEvent)) {
Logger::logError("Not found the valid auto callback: key={$key}, type={$parsedCanalData['type']}");
return;
}
$handler = new $event[$key];
$handler->$onWhichEvent($parsedCanalData);
}
/**
* Manual callback
* @param string $key
* @param string $onWhichEvent
* @param array $parsedCanalData
*/
public function manualCallback(string $key, string $onWhichEvent, array $parsedCanalData)
{
global $event;
if ($onWhichEvent === false || !isset($event[$key][$onWhichEvent]) || !is_array($event[$key][$onWhichEvent])) {
Logger::logError("Not found the valid manual callback: key={$key}, type={$parsedCanalData['type']}");
return;
}
$filterResult = true;
if (isset($event[$key][$onWhichEvent]['filter']) && is_callable($event[$key][$onWhichEvent]['filter'])) {
$filterResult = $event[$key][$onWhichEvent]['filter']($parsedCanalData);
if ($filterResult) {
$parsedCanalData = $filterResult;
}
}
$callbackResult = false;
if ($filterResult && isset($event[$key][$onWhichEvent]['callback']) && is_callable($event[$key][$onWhichEvent]['callback'])) {
$callbackResult = $event[$key][$onWhichEvent]['callback']($parsedCanalData);
}
if (isset($event[$key][$onWhichEvent]['finally']) && is_callable($event[$key][$onWhichEvent]['finally'])) {
$event[$key][$onWhichEvent]['finally']($filterResult, $callbackResult);
}
}
}
================================================
FILE: framework/Logger.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Logger
{
/**
* Support log level.
*/
const LEVEL_DEBUG = 'debug';
const LEVEL_INFO = 'info';
const LEVEL_SLOW = 'slow';
const LEVEL_WARNING = 'warning';
const LEVEL_ERROR = 'error';
const LEVEL_FATAL = 'fatal';
/**
* The unique id of log.
*
* @var string
*/
private static $logId;
/**
* The formula of logId.
*
* @var string
*/
private static $formula;
/**
* Setup the logId.
*
* @param string $logIdParam
*
* @param string $formulaParam
*/
public static function setLogId(string $logIdParam, string $formulaParam)
{
self::$logId = $logIdParam;
self::$formula = $formulaParam;
}
/**
* Setup the logId according the parsed canal data.
*
* @param array $parsedCanalData
*/
public static function setLogIdByParsedCanalData(array $parsedCanalData)
{
$database = isset($parsedCanalData['database']) ? $parsedCanalData['database'] : '';
$table = isset($parsedCanalData['table']) ? $parsedCanalData['table'] : '';
$type = isset($parsedCanalData['type']) ? $parsedCanalData['type'] : '';
$id = isset($parsedCanalData['id']) ? $parsedCanalData['id'] : 0;
$ts = isset($parsedCanalData['ts']) ? $parsedCanalData['ts'] : 0;
$formula = self::$formula = "{$database}+{$table}+{$type}+{$id}+{$ts}";
self::$logId = md5($formula);
}
/**
* Return the debug data.
*
* @return array[]
*/
public static function returnDumpData(): array
{
$result = [
'runtime_files' => [],
];
$handle = opendir(RUNTIME_PATH);
while ($handle && ($file = readdir($handle)) !== false) {
if (!is_file($file)) {
continue;
}
$result['runtime_files'][] = $file;
}
closedir($handle);
return $result;
}
/**
* Write log in debug mode.
*
* @param string $data the message to write
*/
public static function logDebug(string $data)
{
self::write(self::LEVEL_DEBUG, $data);
}
/**
* Write log in info mode.
*
* @param string $data the message to write
*/
public static function logInfo(string $data)
{
self::write(self::LEVEL_INFO, $data);
}
/**
* Write log in slow mode.
*
* @param string $data the message to write
*/
public static function logSlow(string $data)
{
self::write(self::LEVEL_SLOW, $data);
}
/**
* Write log in warning mode.
*
* @param string $data the message to write
*/
public static function logWarning(string $data)
{
self::write(self::LEVEL_WARNING, $data);
}
/**
* Write log in error mode.
*
* @param string $data the message to write
*/
public static function logError(string $data)
{
self::write(self::LEVEL_ERROR, $data);
}
/**
* Write log in fatal mode.
*
* @param string $data the message to write
*/
public static function logFatal(string $data)
{
self::write(self::LEVEL_FATAL, $data);
}
/**
* Get the path of log file in different level mode.
*
* @param $logLevel
*
* @return string
*/
public static function getLogFilePath($logLevel): string
{
global $log;
$date = date('Ymd');
if ($logLevel === self::LEVEL_SLOW) {
return "{$log[$logLevel]['path']}.{$date}";
}
return "{$log[$logLevel]}.{$date}";
}
/**
* The common method for writing log.
*
* @param $level
*
* @param $data
*/
public static function write($level, $data)
{
$logId = self::$logId;
$formula = self::$formula;
$datetime = date('Y-m-d H:i:s');
$header = "{$datetime} | logid = {$logId} = {$formula}";
$body = $data;
$footer = "";
if ($level === self::LEVEL_FATAL) {
$dumpData = self::returnDumpData();
$footer .= "----------Dump is data as follows----------\n";
$footer .= ("runtime_files: " . implode(', ', $dumpData['runtime_files']));
}
$content = "{$header}\n{$body}\n{$footer}\n";
$file = self::getLogFilePath($level);
file_put_contents($file, $content, FILE_APPEND | LOCK_EX);
}
}
================================================
FILE: framework/Manager.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Manager
{
const COMMAND_WORK_SUCCESS = 'success';
const COMMAND_STOP_SUCCESS = 'success';
const COMMAND_STOP_FAILED = 'failed';
/**
* Get running workers count.
*
* @notice You must do something when this function return false
*
* @return false|int
*/
public function getRunningWorkersCount()
{
$handle = opendir(RUNTIME_PATH);
if (!$handle) {
return false;
}
$count = 0;
while (($file = readdir($handle)) !== false) {
if (strpos($file, RUNTIME_ESUPDATER_WORKER_PID_FILE_PREFIX) === 0) {
++$count;
}
}
closedir($handle);
return $count;
}
/**
* Start consumer and blocking.
*/
public function startConsumerAndBlocking()
{
file_put_contents(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE, Consumer::START_FLAG_STRING);
file_put_contents(RUNTIME_ESUPDATER_CONSUMER_PID_FILE, intval(getmypid()));
global $consumer;
(new Consumer($consumer))->highLevelConsuming();
}
/**
* Stop consumer by IPC(InterProcess Communication): Shared File.
*/
public function stopConsumerByIPC()
{
// Consumer process may not been created, so communicate only when consumer was created, or will be mistaken for stop failed
if (file_exists(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE)) {
file_put_contents(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE, Consumer::STOP_FLAG_STRING);
}
}
/**
* Whether consumer was stopped or not.
*
* @return bool
*/
public function isConsumerStopped(): bool
{
return !file_exists(RUNTIME_ESUPDATER_CONSUMER_PID_FILE) && !file_exists(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE);
}
/**
* Whether all workers were stopped or not.
*
* @return bool
*/
public function isWorkersStopped(): bool
{
$count = $this->getRunningWorkersCount();
if ($count === false || $count > 0) {
return false;
}
return true;
}
/**
* Whether the current process is consumer process.
*/
public function isConsumerProcess()
{
$pid = getmypid();
$consumerPIDFile = RUNTIME_ESUPDATER_CONSUMER_PID_FILE;
if (file_exists($consumerPIDFile) && file_get_contents($consumerPIDFile) == $pid) {
return $consumerPIDFile;
}
return false;
}
/**
* Whether the current process is worker process.
*/
public function isWorkerProcess()
{
$pid = getmypid();
$workerPIDFile = RUNTIME_PATH . RUNTIME_ESUPDATER_WORKER_PID_FILE_PREFIX . $pid . ".pid";
if (file_exists($workerPIDFile) && file_get_contents($workerPIDFile) == $pid) {
return $workerPIDFile;
}
return false;
}
/**
* Command: start.
*/
public function commandStart()
{
$formula = "start+" . date('Y-m-d H:i:s');
$logId = md5($formula);
Logger::setLogId($logId, $formula);
Logger::logInfo('Start esupdater');
// Start consumer and blocking ...
$this->startConsumerAndBlocking();
// After blocking, it means consumer is stopped, so remove consumer runtime files now
unlink(RUNTIME_ESUPDATER_CONSUMER_STATUS_FILE);
unlink(RUNTIME_ESUPDATER_CONSUMER_PID_FILE);
}
/**
* Command: stop.
*
* @return string
*/
public function commandStop(): string
{
$formula = "stop+" . date('Y-m-d H:i:s');
$logId = md5($formula);
Logger::setLogId($logId, $formula);
Logger::logInfo('Stop esupdater');
// Stop consumer by IPC(InterProcess Communication)
$this->stopConsumerByIPC();
// Wait consumer and all workers were stopped, the max wait time is 10 seconds
$maxWaitSecond = 10;
$startTimestamp = time();
while (true) {
if ($this->isConsumerStopped() && $this->isWorkersStopped()) {
Logger::logInfo('Stop esupdater successfully');
return self::COMMAND_STOP_SUCCESS;
}
if ((time() - $startTimestamp) > $maxWaitSecond) {
Logger::logFatal('Failed to stop esupdater');
return self::COMMAND_STOP_FAILED;
}
sleep(1);
}
}
/**
* Command: work.
*
* @param string $canalData
*
* @return string
*/
public function commandWork(string $canalData): string
{
$pid = getmypid();
$workerPIDFile = RUNTIME_PATH . RUNTIME_ESUPDATER_WORKER_PID_FILE_PREFIX . $pid . ".pid";
file_put_contents($workerPIDFile, intval($pid));
(new \framework\Listener())->dispatch($canalData);
unlink($workerPIDFile);
return self::COMMAND_WORK_SUCCESS;
}
}
================================================
FILE: framework/Timer.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace framework;
class Timer
{
/**
* Store the benchmark of time.
*
* @var array
*/
public static $container = [];
/**
* Return nowSecondTimestampWithMicrosecond.
*
* @return float
*/
public static function getNowSecondTimestampWithMicrosecond(): float
{
list($microsecond, $secondTimestamp) = explode(' ', microtime());
return ((float)$microsecond + (float)$secondTimestamp);
}
/**
* Timer start and return nowSecondTimestampWithMicrosecond.
*
* @param string $mark
*
* @return float
*/
public static function start(string $mark): float
{
self::$container[$mark] = $nowSecondTimestampWithMicrosecond = self::getNowSecondTimestampWithMicrosecond();
return $nowSecondTimestampWithMicrosecond;
}
/**
* Return elapsed milliseconds from start.
*
* @param string $mark
*
* @return int
*/
public static function elapsed(string $mark): int
{
if (!isset(self::$container[$mark])) {
return 0;
}
$startSecondTimestampWithMicrosecond = self::$container[$mark];
$nowSecondTimestampWithMicrosecond = self::getNowSecondTimestampWithMicrosecond();
return (int)(($nowSecondTimestampWithMicrosecond - $startSecondTimestampWithMicrosecond) * 1000);
}
}
================================================
FILE: install/README.md
================================================
### What is this directory
It's the installation directory, you just run ```bash install.sh``` would help you to install automatically.
================================================
FILE: install/container/kafka.sh
================================================
#!/usr/bin/env bash
# Pull images first.
docker pull wurstmeister/zookeeper
docker pull wurstmeister/kafka
# Start zookeeper.
containerCount=0
for file in $(docker container ls -f name=zookeeperContainer -q)
do
((containerCount++))
done
if [ $containerCount -ne 0 ]; then
echo -e "Start zookeeperContainer already, is skipped."
else
docker run -d --name zookeeperContainer -p 2181:2181 -t wurstmeister/zookeeper
fi
# Start kafka server.
# use ifconfig command to get the local ip: 192.168.x.x not 127.0.0.1
localIP=$(ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|grep '192.168' |awk '{print $2}'|tr -d "addr:")
containerCount=0
for file in $(docker container ls -f name=kafkaContainer -q)
do
((containerCount++))
done
if [ $containerCount -ne 0 ]; then
echo -e "Start kafkaContainer already, is skipped."
else
docker run -d --name kafkaContainer -p 9092:9092 -e KAFKA_BROKER_ID=0 -e KAFKA_ZOOKEEPER_CONNECT=${localIP}:2181 -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://${localIP}:9092 -e KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 -t wurstmeister/kafka
fi
# Login kafka server container
# docker exec -it kafkaContainer /bin/bash
# cd /opt/kafka/
# ./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic default_topic
# ./bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic default_topic --from-beginning
================================================
FILE: install/image/Dockerfile
================================================
# This dockerfile will make images of phpkafka.
# Must use version 7.0, or it would be error.
FROM php:7.0-fpm
# Use homeland image.
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
&& echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list \
&& echo 'deb http://mirrors.163.com/debian/ stretch-updates main non-free contrib' >> /etc/apt/sources.list \
&& echo 'deb http://mirrors.163.com/debian-security/ stretch/updates main non-free contrib' >> /etc/apt/sources.list \
&& apt-get update
# Disable commandline interactive.
ENV DEBIAN_FRONTEND noninteractive
# Install git.
# -y is input yes automatically.
RUN apt-get update \
&& apt-get -y install git
# Install vim.
RUN apt-get update \
&& apt-get -y install vim
# Install composer
RUN curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer
# Install pecl.
RUN apt-get update \
&& apt-get -y install autoconf \
&& apt-get -y install libz-dev
# Install libkakfa.
RUN apt-get update \
&& apt-get -y install librdkafka-dev=0.9.3-1
# Install the php kafka extension.
# Need librdkafka-dev=0.9.3-1 / rdkafka-3.0.0 this version will work, or it would be error.
# Check rdkafka versions: https://pecl.php.net/package/rdkafka
# Check librdkafka versions: apt search librdkafka-dev
RUN pecl channel-update pecl.php.net \
&& pecl install rdkafka-3.0.0 \
&& docker-php-ext-enable rdkafka
# Install and enable mysqli extension
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
================================================
FILE: install/image/README.md
================================================
### What is this directory
It's a docker image directory, you just run ```bash make.sh``` would help you to make a ```php-kafka``` image
================================================
FILE: install/image/make.sh
================================================
#!/usr/bin/env bash
docker build -t phpkafka .
if [ $? -ne 0 ]; then
echo -e ""
echo -e ">>>>>>>>Make image failure<<<<<<<<"
exit 1
else
echo -e ""
echo -e "========Make image success========"
docker images
fi
# Push to docker repository
# docker login
# input your name and password
# docker tag adf2495d561e lvsid/phpkafka:v1.0
# docker push lvsid/phpkafka:v1.0
================================================
FILE: install/install.sh
================================================
#!/usr/bin/env bash
# Check params.
if [ ! -n "$1" ] ;then
echo -e "Please input the localIP param."
exit 1
else
export ESUPDATER_LOCAL_IP=$1
systemVariables="ESUPDATER_LOCAL_IP=$1\n"
echo -e $systemVariables > ../.env
fi
# The part of you must do.
# 1. Make image
cd image && bash make.sh && cd ..
# The part of you could do.
# 1. Run kafka container
cd container && bash kafka.sh && cd ..
================================================
FILE: plugin/README.md
================================================
### What is this directory
It's a plugin directory, all plugins were stored here.
================================================
FILE: plugin/autogeneratecallback/README.md
================================================
## AutoGenerateCallback
一个自动生成```Handler```和```Service```的事件回调模块的扩展。
### 1、如何使用
- $namespace 参数 :新增模块的命名空间,如 ```app\alpha\account```
- $moduleName 参数 :新增的模块名称,如 ```Account```
```shell
php plugin/autogeneratecallback/autogeneratecallback.php {$namespace} {$moduleName}
```
#### 注意事项
- 命名空间中的``` \ ```符号(首位不需要)需要转义,所以别忘记输入两次``` \\ ```,如```app\alpha\account```
- 模块名称使用大驼峰命名,如```MyProfile```
### 2、使用示例
项目中 [account](/app/alpha/account) 模块下的文件即是通过如下命令自动生成而来的。
```shell
php plugin/autogeneratecallback/autogeneratecallback.php app\\alpha\\account Account
```
### 3、实现原理
基于 [模板替换](https://www.google.com/search?q=%E6%A8%A1%E6%9D%BF%E6%9B%BF%E6%8D%A2) 原理, 先在模板文件 [handler.template](./handler.template) 和 [service.template](./service.template) 中定义如```{{变量}}```此类的```占位符```,再使用正则匹配把```占位符```替换为目标文本。
================================================
FILE: plugin/autogeneratecallback/autogeneratecallback.php
================================================
$pluginDirectory . 'handler.template',
'php_file' => "{$namespaceDirectory}/{$moduleName}Handler.php",
],
[
'template_file' => $pluginDirectory . 'service.template',
'php_file' => "{$namespaceDirectory}/{$moduleName}Service.php",
],
];
foreach ($configList as $item) {
if (file_exists($item['php_file'])) {
echo "The file already exist: {$item['php_file']}\n";
continue;
}
if (!is_dir($namespaceDirectory)) {
mkdir($namespaceDirectory);
}
$handle = fopen($item['template_file'], 'r');
while (!feof($handle)) {
$content = fgets($handle);
if (preg_match($namespacePattern, $content) > 0) {
$content = str_replace($namespacePattern, $namespace, $content);
}
if (preg_match($moduleNamePattern, $content) > 0) {
$content = str_replace($moduleNamePattern, $moduleName, $content);
}
file_put_contents($item['php_file'], $content, FILE_APPEND);
}
}
================================================
FILE: plugin/autogeneratecallback/handler.template
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace {{namespace}};
class {{moduleName}}Handler
{
/**
* The event callback when table of {{moduleName}} trigger insert event.
* @param array $parsedCanalData
* @return bool
*/
public function onInsert(array $parsedCanalData): bool
{
return (new {{moduleName}}Service())->doInsert($parsedCanalData);
}
/**
* The event callback when table of {{moduleName}} trigger update event.
* @param array $parsedCanalData
* @return bool
*/
public function onUpdate(array $parsedCanalData): bool
{
return (new {{moduleName}}Service())->doUpdate($parsedCanalData);
}
/**
* The event callback when table of {{moduleName}} trigger delete event.
* @param array $parsedCanalData
* @return bool
*/
public function onDelete(array $parsedCanalData): bool
{
return (new {{moduleName}}Service())->doDelete($parsedCanalData);
}
}
================================================
FILE: plugin/autogeneratecallback/service.template
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace {{namespace}};
class {{moduleName}}Service
{
/**
* Update different indexes when insert.
* @param array $parsedCanalData
* @return bool
*/
public function doInsert(array $parsedCanalData): bool
{
$success = true;
if (!$this->updateAIndexWhenInsert($parsedCanalData)) {
$success = false;
}
if (!$this->updateBIndexWhenInsert($parsedCanalData)) {
$success = false;
}
// update some other indexes...
return $success;
}
/**
* Update different indexes when update.
* @param array $parsedCanalData
* @return bool
*/
public function doUpdate(array $parsedCanalData): bool
{
$success = true;
if (!$this->updateAIndexWhenUpdate($parsedCanalData)) {
$success = false;
}
if (!$this->updateBIndexWhenUpdate($parsedCanalData)) {
$success = false;
}
// update some other indexes...
return $success;
}
/**
* Update different indexes when delete.
* @param array $parsedCanalData
* @return bool
*/
public function doDelete(array $parsedCanalData): bool
{
$success = true;
if (!$this->updateAIndexWhenDelete($parsedCanalData)) {
$success = false;
}
if (!$this->updateBIndexWhenDelete($parsedCanalData)) {
$success = false;
}
// update some other indexes...
return $success;
}
/**
* Update a index: event of insert
* @param array $parsedCanalData
* @return bool
*/
public function updateAIndexWhenInsert(array $parsedCanalData): bool
{
return true;
}
/**
* Update b index: event of insert
* @param array $parsedCanalData
* @return bool
*/
public function updateBIndexWhenInsert(array $parsedCanalData): bool
{
return true;
}
/**
* Update a index: event of update
* @param array $parsedCanalData
* @return bool
*/
public function updateAIndexWhenUpdate(array $parsedCanalData): bool
{
return true;
}
/**
* Update b index: event of update
* @param array $parsedCanalData
* @return bool
*/
public function updateBIndexWhenUpdate(array $parsedCanalData): bool
{
return true;
}
/**
* Update a index: event of delete
* @param array $parsedCanalData
* @return bool
*/
public function updateAIndexWhenDelete(array $parsedCanalData): bool
{
return true;
}
/**
* Update b index: event of delete
* @param array $parsedCanalData
* @return bool
*/
public function updateBIndexWhenDelete(array $parsedCanalData): bool
{
return true;
}
}
================================================
FILE: plugin/loader.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
// PHP Configuration.
date_default_timezone_set('Asia/Shanghai');
const PLUGIN_PATH = __DIR__ . '/';
const ROOT_PATH = PLUGIN_PATH . '../';
================================================
FILE: restart.sh
================================================
#!/usr/bin/env bash
bash stop.sh && bash start.sh
================================================
FILE: start.sh
================================================
#!/usr/bin/env bash
# Prevent start repeatedly
containerCount=0
for file in $(docker container ls -f name=esupdaterContainer -q)
do
((containerCount++))
done
if [ $containerCount -ne 0 ]; then
echo -e ">>>>>>>>Start failure: please run stop.sh first<<<<<<<<"
exit 1
fi
# Pull without password
git config --global credential.helper store
git pull
# Build esupdater image
docker build -t esupdater .
if [ $? -ne 0 ]; then
echo -e ">>>>>>>>Start failure: failed to build<<<<<<<<"
exit 1
fi
# Run container
# docker run --cpuset-cpus="0,1" --cpus=1.5 --cpuset-mems="2,3" --name {ContainerName} -d -v {LocalPath:ContainerPath} {imageName}
docker run --cpus=1.5 --name esupdaterContainer -d -v /home/log/esupdater/:/home/log/esupdater/ esupdater
if [ $? -ne 0 ]; then
echo -e ">>>>>>>>Start failure: failed to run<<<<<<<<"
exit 1
fi
# Exec command
docker exec -d esupdaterContainer php esupdater.php start
if [ $? -ne 0 ]; then
echo -e ">>>>>>>>Start failure: failed to exec<<<<<<<<"
exit 1
else
echo -e "========Start success========"
fi
================================================
FILE: stop.sh
================================================
#!/usr/bin/env bash
# Exec command: php esupdater.php stop
# Use exec command with -i argument would run and return result synchronously: docker exec -i esupdaterContainer php -r "var_dump(123);"
docker exec -i esupdaterContainer php esupdater.php stop
# Stop and remove container
docker stop esupdaterContainer
docker container rm esupdaterContainer
# Remove image
docker rmi esupdater
if [ $? -ne 0 ]; then
echo -e ">>>>>>>>Stop failure<<<<<<<<"
exit 1
else
echo -e "========Stop success========"
fi
================================================
FILE: test/BaseTest.php
================================================
* @license https://github.com/WGrape/esupdater/blob/master/LICENSE MIT Licence
*/
namespace test;
class BaseTest
{
/**
* Test success.
*
* @return bool
*/
protected function success(): bool
{
$debugTrace = debug_backtrace();
$fileShortName = $this->getCallerFileName($debugTrace);
$functionName = $this->getCallerFunctionName($debugTrace);
echo "Test Success: {$fileShortName} -> {$functionName}\n";
return true;
}
/**
* Test failed.
* @param string $err
*
* @return bool
*/
protected function failed($err = ""): bool
{
$debugTrace = debug_backtrace();
$fileShortName = $this->getCallerFileName($debugTrace);
$functionName = $this->getCallerFunctionName($debugTrace);
echo "Test Failed: {$fileShortName} -> {$functionName}\n";
if (!empty($err)) {
echo "$err\n";
}
return false;
}
/**
* Return text with success color.
*
* @param $text
*
* @return string
*/
public function decorateSuccessText($text): string
{
return "\033[32m{$text}\033[0m";
}
/**
* Return text with failed color.
*
* @param $text
*
* @return string
*/
public function decorateFailedText($text): string
{
return "\033[31;4m{$text}\033[0m";
}
/**
* Get file name of caller.
*
* @param array $debugTrace the data of debug_backtrace() return
*
* @return string
*/
protected function getCallerFileName(array $debugTrace): string
{
if (empty($debugTrace)) {
return '';
}
$fileFullName = $debugTrace[0]['file'];
$sliceList = explode('/test/', $fileFullName);
$fileShortName = '';
if (isset($sliceList[1])) {
$fileShortName = "/test/" . $sliceList[1];
}
return $fileShortName;
}
/**
* Get function name of caller.
*
* @param array $debugTrace the data of debug_backtrace() return
*
* @return string
*/
protected function getCallerFunctionName(array $debugTrace): string
{
if (!isset($debugTrace[1])) {
return '';
}
return $debugTrace[1]['function'];
}
}
================================================
FILE: test/README.md
================================================
### What is this directory
It's a unit test directory, all testcases are stored here, includes ```/app/alpha/```, ```/app/common```,```/framework``` and so on.
================================================
FILE: test/prepare-commit-msg
================================================
#!/bin/sh
# Run test
result=`php test/run.php | grep 'Test Failed'`
if [ -n "$result" ]; then
echo "Sorry, you would not commit if you failed the test "
exit 1
fi
================================================
FILE: test/report/index.css
================================================
#Container {
text-align: center;
}
.section{
margin-top: 30px;
}
#table-1 thead, #table-1 tr {
border-top-width: 1px;
border-top-style: solid;
border-top-color: rgb(230, 189, 189);
}
#table-1 {
width: 700px;
margin: 20px auto;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: rgb(230, 189, 189);
}
/* Padding and font style */
#table-1 td, #table-1 th {
padding: 5px 10px;
font-size: 12px;
font-family: Verdana;
color: rgb(177, 106, 104);
}
/* Alternating background colors */
#table-1 tr:nth-child(even) {
background: rgb(238, 211, 210)
}
#table-1 tr:nth-child(odd) {
background: #FFF
}
#table-2 thead, #table-2 tr {
border-top-width: 1px;
border-top-style: solid;
border-top-color: rgb(235, 242, 224);
}
#table-2 {
width: 700px;
margin: 20px auto;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: rgb(235, 242, 224);
}
/* Padding and font style */
#table-2 td, #table-2 th {
padding: 5px 10px;
font-size: 12px;
font-family: Verdana;
color: rgb(149, 170, 109);
}
/* Alternating background colors */
#table-2 tr:nth-child(even) {
background: rgb(230, 238, 214)
}
#table-2 tr:nth-child(odd) {
background: #FFF
}
================================================
FILE: test/report/index.html
================================================
| Id | TestClass | TestMethod | TestResult |
|---|---|---|---|
| 1 | test\testcases\app\common\TestESService | testIsSuccess | success |
| 2 | test\testcases\app\common\TestESService | testIsNeedToUpdate | success |
| 3 | test\testcases\app\alpha\TestUserService | testGetUserId | success |
| 4 | test\testcases\framework\TestConsumer | testConstruct | success |
| 5 | test\testcases\framework\TestConsumer | testIsNeedStop | success |
| 6 | test\testcases\framework\TestConsumer | testIsNeedCheckStatus | success |
| 7 | test\testcases\framework\TestConsumer | testHighLevelConsuming | success |
| 8 | test\testcases\framework\TestTimer | testElapsed | success |
| 9 | test\testcases\framework\TestCanal | testCheckParsedCanalData | success |
| 10 | test\testcases\framework\TestCommand | testWork | success |
| 11 | test\testcases\framework\TestManager | testGetRunningWorkersCount | success |
| 12 | test\testcases\framework\TestManager | testStopConsumerByIPC | success |
| 13 | test\testcases\framework\TestManager | testIsConsumerStopped | success |
| 14 | test\testcases\framework\TestManager | testIsWorkersStopped | success |
| 15 | test\testcases\framework\TestManager | testIsConsumerProcess | success |
| 16 | test\testcases\framework\TestManager | testIsWorkerProcess | success |
| 17 | test\testcases\framework\TestEnvironment | testParseEnvFile | success |
| Id | TestClass | TestMethod | TestResult |
|---|