Merge branch 'master' into 2.1-merge

# Conflicts:
#	.travis.yml
#	composer.json
#	src/json-rpc/src/CoreMiddleware.php
This commit is contained in:
李铭昕 2020-10-28 09:18:07 +08:00
commit 11dabbf792
45 changed files with 749 additions and 62 deletions

49
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: PHPUnit for Hyperf
on: [push, pull_request]
env:
SW_VERSION: '4.5.6'
jobs:
ci:
name: Test on PHP ${{ matrix.php-version }} MySQL ${{ matrix.mysql-version }}
runs-on: "${{ matrix.os }}"
strategy:
matrix:
os: [ubuntu-latest]
php-version: ['7.2', '7.3', '7.4']
mysql-version: ['5.7', '8.0']
max-parallel: 6
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: phpize
extensions: redis
ini-values: extension=swoole, opcache.enable_cli=1
coverage: none
- name: Build Swoole
run: ./.travis/swoole.install.sh
- name: Setup Packages
run: composer update -o
- name: Setup Services
run: |
docker run --name mysql -p 3306:3306 -e MYSQL_ALLOW_EMPTY_PASSWORD=true -d mysql:${{ matrix.mysql-version }} --bind-address=0.0.0.0 --default-authentication-plugin=mysql_native_password
docker run --name redis -p 6379:6379 -d redis
docker run -d --name dev-consul -e CONSUL_BIND_INTERFACE=eth0 -p 8500:8500 consul
docker run --name nsq -p 4150:4150 -p 4151:4151 -p 4160:4160 -p 4161:4161 -p 4170:4170 -p 4171:4171 --entrypoint /bin/nsqd -d nsqio/nsq:latest
docker build --tag grpc-server:latest src/grpc-client/tests/Mock
docker run -d --name grpc-server -p 50051:50051 grpc-server:latest
- name: Setup Mysql
run: export TRAVIS_BUILD_DIR=$(pwd) && bash ./.travis/setup.mysql.sh
- name: Run Scripts Before Test
run: cp .travis/.env.example .env
- name: Run Test Cases
run: |
composer analyse src
composer test -- --exclude-group NonCoroutine
vendor/bin/phpunit --group NonCoroutine

View File

@ -5,13 +5,13 @@ sudo: required
matrix:
include:
- php: 7.3
env: SW_VERSION="4.5.5"
env: SW_VERSION="4.5.6"
- php: 7.3
env: SW_VERSION="4.5.5" GUZZLE_7=1
env: SW_VERSION="4.5.6" GUZZLE_7=1
- php: 7.4
env: SW_VERSION="4.5.5"
env: SW_VERSION="4.5.6"
- php: 7.4
env: SW_VERSION="4.5.5" GUZZLE_7=1
env: SW_VERSION="4.5.6" GUZZLE_7=1
services:
- mysql

View File

@ -4,9 +4,9 @@ CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}"
echo -e "Create MySQL database..."
mysql -u root -e "CREATE DATABASE IF NOT EXISTS hyperf charset=utf8mb4 collate=utf8mb4_unicode_ci;"
cat "${TRAVIS_BUILD_DIR}/.travis/hyperf.sql" | mysql -u root hyperf
mysql -h 127.0.0.1 -u root -e "CREATE DATABASE IF NOT EXISTS hyperf charset=utf8mb4 collate=utf8mb4_unicode_ci;"
cat "${TRAVIS_BUILD_DIR}/.travis/hyperf.sql" | mysql -h 127.0.0.1 -u root hyperf
echo -e "Done\n"
wait
wait

View File

@ -7,4 +7,4 @@ cd swoole
phpize
./configure --enable-openssl --enable-mysqlnd --enable-http2
make -j$(nproc)
make install
sudo make install

View File

@ -1,4 +1,34 @@
# v2.0.15 - TBD
# v2.0.17 - TBD
## Fixed
- [#2719](https://github.com/hyperf/hyperf/pull/2719) Fixed method `Arr::merge` does not works when `array1` does not constains the `$key`.
# v2.0.16 - 2020-10-26
## Added
- [#2682](https://github.com/hyperf/hyperf/pull/2682) Added method `getCacheTTL` for `CacheableInterface` which can control cache time each models.
- [#2696](https://github.com/hyperf/hyperf/pull/2696) Added swoole tracker leak tool.
## Fixed
- [#2680](https://github.com/hyperf/hyperf/pull/2680) Fixed Type error for `CastsValue`, because `$isSynchronized` don't have default value.
- [#2680](https://github.com/hyperf/hyperf/pull/2680) Fixed default value in `$items` will be replaced by `__construct` for `CastsValue`.
- [#2693](https://github.com/hyperf/hyperf/pull/2693) Fixed unexpected behavior in retry budget for `hyperf/retry`.
- [#2695](https://github.com/hyperf/hyperf/pull/2695) Fixed method `Container::define()` does not works when the class has been resolved.
## Optimized
- [#2611](https://github.com/hyperf/hyperf/pull/2611) Optimized `FindDriver` for watcher, you can use it in alpine image.
- [#2662](https://github.com/hyperf/hyperf/pull/2662) Optimized amqp consumer which can stop safely.
- [#2690](https://github.com/hyperf/hyperf/pull/2690) Optimized `tracer` which ensure span finished and flushed.
# v2.0.15 - 2020-10-19
## Added
- [#2654](https://github.com/hyperf/hyperf/pull/2654) Added method `Hyperf\Utils\Resource::from` which can convert `string` to `resource`.
## Fixed
@ -6,6 +36,10 @@
- [#2639](https://github.com/hyperf/hyperf/pull/2639) Fixed exception will not be normalized for json-rpc.
- [#2643](https://github.com/hyperf/hyperf/pull/2643) Fixed undefined method unsearchable for `scout:flush`.
## Optimized
- [#2656](https://github.com/hyperf/hyperf/pull/2656) Optimized the response when parse parameters failed for json-rpc.
# v2.0.14 - 2020-10-12
## Added

View File

@ -68,7 +68,7 @@
"php-amqplib/php-amqplib": "^2.7",
"php-di/php-di": "^6.0",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan": "0.12.50",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^9.4",
"predis/predis": "^1.1",
"reactivex/rxphp": "^2.0",

BIN
docs/en/imgs/snowflake.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

157
docs/en/snowflake.md Normal file
View File

@ -0,0 +1,157 @@
# Snowflake
## Algorithm Introduction
`Snowflake` is a distributed global unique ID generation algorithm proposed by twitter. The result of the algorithm generating `ID` is a long integer with the size of `64bit` . Under the standard algorithm, its structure is shown in the figure below
![snowflake](imgs/snowflake.jpeg)
-`One bit`, unused.
- The highest bit in the binary system is the sign bit. The `ID` generated by us is generally a positive integer, so the highest bit is fixed to 0.
- `41 bits` to record the timestamp (MS).
- `41 bits` can represent `2^41 - 1` numbers.
- In other words, `41 bits` can represent the value of `2^41 - 1` milliseconds, and the unit year is `(2^41 - 1) / (1000 * 60 * 60 * 24 * 365)` about `69` years。
- `10 bits`, used to record the `ID` of the working machine.
- It can be deployed in `2^10` nodes, including `5` bits `DatacenterId` and `5` bits `WorkerId`.
- `12 bits`, serial number, used to record different `id` generated in the same millisecond.
- `12 bits` can represent the maximum number of positive integers `2^12 - 1` with a total of `4095` numbers, which represent the `4095` `ID` serial numbers generated by the same machine in the same time interval (MS).
`Snowflake` can guarantee that:
- All generated `ID` increase with time trend.
- No duplicate `ID` will be generated in the whole distributed system (Because there is a distinction between `DatacenterId (5 bits)` and `WorkerId (5 bits)`.
The [hyperf/snowflake](https://github.com/hyperf/snowflake) component provides good extensibility in design, allowing you to implement other variant algorithms based on snowflake with simple extension.
## Install
```
composer require hyperf/snowflake
```
## Use
The framework provides `MetaGeneratorInterface` and `IdGeneratorInterface`. `MetaGeneratorInterface` generates `Meta` files of `ID`, and `IdGeneratorInterface` generates `distributed ID` based on the corresponding `Meta` files.
The `MetaGeneratorInterface` used by the framework by default is a `millisecond level generator` based on `Redis`.
The configuration file is located in `config/autoload/snowflake.php` If the configuration file does not exist, you can execute `php bin/hyperf.php vendor:publish hyperf/snowflake` command to create a default configuration. The contents of the configuration file are as follows:
```php
<?php
declare(strict_types=1);
use Hyperf\Snowflake\MetaGenerator\RedisMilliSecondMetaGenerator;
use Hyperf\Snowflake\MetaGenerator\RedisSecondMetaGenerator;
use Hyperf\Snowflake\MetaGeneratorInterface;
return [
'begin_second' => MetaGeneratorInterface::DEFAULT_BEGIN_SECOND,
RedisMilliSecondMetaGenerator::class => [
// Redis Pool
'pool' => 'default',
// To calculate the Key of WorkerId
'key' => RedisMilliSecondMetaGenerator::DEFAULT_REDIS_KEY
],
RedisSecondMetaGenerator::class => [
// Redis Pool
'pool' => 'default',
// To calculate the Key of WorkerId
'key' => RedisMilliSecondMetaGenerator::DEFAULT_REDIS_KEY
],
];
```
Using `Snowflake` in the framework is very simple. You just need to take out the `IdGeneratorInterface` object from `DI`.
```php
<?php
use Hyperf\Snowflake\IdGeneratorInterface;
use Hyperf\Utils\ApplicationContext;
$container = ApplicationContext::getContainer();
$generator = $container->get(IdGeneratorInterface::class);
$id = $generator->generate();
```
When you know that the `ID` needs to reverse the corresponding `Meta`, you just need to call `generate`.
```php
<?php
use Hyperf\Snowflake\IdGeneratorInterface;
use Hyperf\Utils\ApplicationContext;
$container = ApplicationContext::getContainer();
$generator = $container->get(IdGeneratorInterface::class);
$meta = $generator->degenerate($id);
```
## Override 'Meta' generator
There are many ways to implement the `distributed global unique ID`, and there are also many variants based on the `Snowflake` algorithm. Although they are all `Snowflake` algorithms, they are not the same. For example, someone may generate a `Meta` based on `UserId` rather than `WorkerId`. Next, let's implement a simple `MetaGenerator`.
In short, the `UserId` will definitely exceed '10 bits'. Therefore, the default `DataCenterId` and `WorkerId` cannot be installed. Therefore, the `UserId` module needs to be taken.
```php
<?php
declare(strict_types=1);
use Hyperf\Snowflake\IdGenerator;
class UserDefinedIdGenerator
{
/**
* @var IdGenerator\SnowflakeIdGenerator
*/
protected $idGenerator;
public function __construct(IdGenerator\SnowflakeIdGenerator $idGenerator)
{
$this->idGenerator = $idGenerator;
}
public function generate(int $userId)
{
$meta = $this->idGenerator->getMetaGenerator()->generate();
return $this->idGenerator->generate($meta->setWorkerId($userId % 31));
}
public function degenerate(int $id)
{
return $this->idGenerator->degenerate($id);
}
}
use Hyperf\Utils\ApplicationContext;
$container = ApplicationContext::getContainer();
$generator = $container->get(UserDefinedIdGenerator::class);
$userId = 20190620;
$id = $generator->generate($userId);
```
## Application in database modelon in database model
After configuring `Snowflake`, we can make the database model directly use `Snowflake` `ID` as the primary key.
```php
<?php
class User extends \Hyperf\Database\Model\Model {
use \Hyperf\Snowflake\Concern\Snowflake;
}
```
When the user model is created, the `Snowflake` algorithm will be used to generate the primary key by default.

View File

@ -105,7 +105,9 @@ class AsyncQueueConsumer extends ConsumerProcess
这种模式会把对象直接序列化然后存到 `Redis` 等队列中,所以为了保证序列化后的体积,尽量不要将 `Container``Config` 等设置为成员变量。
比如以下 `Job` 的定义,是 **不可取**
比如以下 `Job` 的定义,是 **不可取** 的,同理 `@Inject` 也是如此。
> 因为 Job 会被序列化,所以成员变量不要包含 匿名函数 等 无法被序列化 的内容,如果不清楚哪些内容无法被序列化,尽量使用注解方式。
```php
<?php

View File

@ -1,5 +1,41 @@
# 版本更新记录
# v2.0.16 - 2020-10-26
## 新增
- [#2682](https://github.com/hyperf/hyperf/pull/2682) 为 `CacheableInterface` 新增方法 `getCacheTTL` 可根据不同模型设置不同的缓存时间。
- [#2696](https://github.com/hyperf/hyperf/pull/2696) 新增 Swoole Tracker 的内存检测工具。
## 修复
- [#2680](https://github.com/hyperf/hyperf/pull/2680) 修复 `CastsValue` 因为没有设置 `$isSynchronized` 默认值,导致的类型错误。
- [#2680](https://github.com/hyperf/hyperf/pull/2680) 修复 `CastsValue``$items` 默认值会被 `__construct` 覆盖的问题。
- [#2693](https://github.com/hyperf/hyperf/pull/2693) 修复 `hyperf/retry` 组件,`Budget` 表现不符合期望的问题。
- [#2695](https://github.com/hyperf/hyperf/pull/2695) 修复方法 `Container::define()` 因为容器中的对象已被实例化,而无法重定义的问题。
## 优化
- [#2611](https://github.com/hyperf/hyperf/pull/2611) 优化 `hyperf/watcher` 组件 `FindDriver` ,使其可以在 `Alpine` 镜像中使用。
- [#2662](https://github.com/hyperf/hyperf/pull/2662) 优化 `Amqp` 消费者进程,使其可以配合 `Signal` 组件安全停止。
- [#2690](https://github.com/hyperf/hyperf/pull/2690) 优化 `hyperf/tracer` 组件,确保其可以正常执行 `finish``flush` 方法。
# v2.0.15 - 2020-10-19
## 新增
- [#2654](https://github.com/hyperf/hyperf/pull/2654) 新增方法 `Hyperf\Utils\Resource::from`,可以方便的将 `string` 转化为 `resource`
## 修复
- [#2634](https://github.com/hyperf/hyperf/pull/2634) [#2640](https://github.com/hyperf/hyperf/pull/2640) 修复 `snowflake` 组件中,元数据生成器 `RedisSecondMetaGenerator` 会产生相同元数据的问题。
- [#2639](https://github.com/hyperf/hyperf/pull/2639) 修复 `json-rpc` 组件中,异常无法正常被序列化的问题。
- [#2643](https://github.com/hyperf/hyperf/pull/2643) 修复 `scout:flush` 执行失败的问题。
## 优化
- [#2656](https://github.com/hyperf/hyperf/pull/2656) 优化了 `json-rpc` 组件中,参数解析失败后,也可以返回对应的错误信息。
# v2.0.14 - 2020-10-12
## 新增

View File

@ -79,7 +79,7 @@ Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,
### 最大协程数限制
`Swoole Server` 通过 `set` 方法设置 `max_coroutine` 参数,用于配置一个 `Worker` 进程最多可存在的协程数量。因为随着 `Worker` 进程处理的协程数目的增加,其对应占用的内存也会随之增加,为了避免超出 `PHP``memory_limit` 限制,请根据实际业务的压测结果设置该值,`Swoole` 的默认值为 `3000`, 在 `hyperf-skeleton` 项目中默认设置为 `100000`
`Swoole Server` 通过 `set` 方法设置 `max_coroutine` 参数,用于配置一个 `Worker` 进程最多可存在的协程数量。因为随着 `Worker` 进程处理的协程数目的增加,其对应占用的内存也会随之增加,为了避免超出 `PHP``memory_limit` 限制,请根据实际业务的压测结果设置该值,`Swoole` 的默认值为 `100000` `Swoole` 版本小于 `v4.4.0-beta` 时默认值为 `3000` , 在 `hyperf-skeleton` 项目中默认设置为 `100000`
## 使用协程

View File

@ -147,6 +147,26 @@ User::query(true)->where('gender', '>', 1)->delete();
对于这种情况,我们可以修改 `use_default_value``true`,并添加 `Hyperf\DbConnection\Listener\InitTableCollectorListener``listener.php` 配置中,使 Hyperf 应用在启动时主动去获取数据库的字段信息,并在获取缓存数据时与之比较并进行缓存数据修正。
### 控制模型中缓存时间
除了 `database.php` 中配置的默认缓存时间 `ttl` 外,`Hyperf\ModelCache\Cacheable` 支持对模型配置更细的缓存时间:
```php
class User extends Model implements CacheableInterface
{
use Cacheable;
/**
* 缓存 10 分钟,返回 null 则使用配置文件中设置的超时时间
* @return int|null
*/
public function getCacheTTL(): ?int
{
return 600;
}
}
```
### EagerLoad
当我们使用模型关系时,可以通过 `load` 解决 `N+1` 的问题,但仍然需要查一次数据库。模型缓存通过重写了 `ModelBuilder`,可以让用户尽可能的从缓存中拿到对应的模型。

View File

@ -20,7 +20,7 @@ composer require hyperf/elasticsearch
Scout 安装完成后,使用 vendor:publish 命令来生成 Scout 配置文件。这个命令将在你的 config 目录下生成一个 scout.php 配置文件。
```bash
php bin/hyperf vendor:publish hyperf/scout
php bin/hyperf.php vendor:publish hyperf/scout
```
最后,在你要做搜索的模型中添加 Hyperf\Scout\Searchable trait。这个 trait 会注册一个模型观察者来保持模型和所有驱动的同步:

View File

@ -180,3 +180,67 @@ return [
Hyperf\SwooleTracker\Aspect\CoroutineHandlerAspect::class,
];
```
## 免费内存泄漏检测工具
Swoole Tracker 本是一款商业产品,拥有进行内存泄漏检测的能力,不过 Swoole Tracker 把内存泄漏检测的功能完全免费给 PHP 社区使用,完善 PHP 生态,回馈社区,下面将概述它的具体用法。
1. 前往 [Swoole Tracker 官网](https://business.swoole.com/SwooleTracker/download/) 下载最新的 Swoole Tracker 扩展;
2. 和上文添加扩展相同,再加入一行配置:
```ini
;Leak检测开关
apm.enable_malloc_hook=1
```
!> 注意不要在composer安装依赖时开启不要在生成代理类缓存时开启。
3. 根据自己的业务,在 Swoole 的 onReceive 或者 onRequest 事件开头加上 `trackerHookMalloc()` 调用:
```php
$http->on('request', function ($request, $response) {
trackerHookMalloc();
$response->end("<h1>Hello Swoole. #".rand(1000, 9999)."</h1>");
});
```
每次调用结束后(第一次调用不会被记录),都会生成一个泄漏的信息到 `/tmp/trackerleak` 日志中,我们可以在 Cli 命令行调用 `trackerAnalyzeLeak()` 函数即可分析泄漏日志,生成泄漏报告
```shell
php -r "trackerAnalyzeLeak();"
```
下面是泄漏报告的格式:
没有内存泄漏的情况:
```
[16916 (Loop 5)] ✅ Nice!! No Leak Were Detected In This Loop
```
其中`16916`表示进程 id`Loop 5`表示第 5 次调用主函数生成的泄漏信息
有确定的内存泄漏:
```
[24265 (Loop 8)] /tests/mem_leak/http_server.php:125 => [12928]
[24265 (Loop 8)] /tests/mem_leak/http_server.php:129 => [12928]
[24265 (Loop 8)] ❌ This Loop TotalLeak: [25216]
```
表示第 8 次调用`http_server.php`的 125 行和 129 行,分别泄漏了 12928 字节内存,总共泄漏了 25216 字节内存。
通过调用 `trackerCleanLeak()` 可以清除泄漏日志,重新开始。[了解更多内存检测工具使用细节](https://www.kancloud.cn/swoole-inc/ee-help-wiki/1941569)
在 Hyperf 中如果需要检测 HTTP Server 中的内存泄漏,可以在 `config/autoload/aspects.php` 配置以下 `Aspect`
```php
<?php
return [
Hyperf\SwooleTracker\Aspect\OnRequestAspect::class,
];
```
其他 Server 可以参照此 `Aspect` 进行重写使用。

View File

@ -47,3 +47,4 @@ parameters:
- '#Function getSwooleTracker.* not found#'
- '#InfluxDB\\Point constructor expects float\|null, string given#'
- '#Call to an undefined method Hyperf\\Database\\Model\\Builder::unsearchable#'
- '#trackerHookMalloc not found#'

View File

@ -21,6 +21,7 @@ use Hyperf\Amqp\Message\MessageInterface;
use Hyperf\Amqp\Pool\PoolFactory;
use Hyperf\Contract\ConfigInterface;
use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
use Hyperf\Process\ProcessManager;
use Hyperf\Utils\Coroutine\Concurrent;
use PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Message\AMQPMessage;
@ -92,7 +93,7 @@ class Consumer extends Builder
);
try {
while ($channel->is_consuming()) {
while ($channel->is_consuming() && ProcessManager::isRunning()) {
$channel->wait();
}
} catch (MaxConsumptionException $ex) {

View File

@ -24,30 +24,39 @@ abstract class CastsValue implements Synchronized, Arrayable
/**
* @var array
*/
protected $items;
protected $items = [];
/**
* @var bool
*/
protected $isSynchronized;
protected $isSynchronized = false;
public function __construct(Model $model, $itmes = [])
public function __construct(Model $model, $items = [])
{
$this->model = $model;
$this->items = $itmes;
$this->items = array_merge($this->items, $items);
}
public function __get($name)
{
return $this->items[$name];
return $this->items[$name] ?? null;
}
public function __set($name, $value)
{
$this->items[$name] = $value;
$this->isSynchronized = false;
$this->model->syncAttributes();
$this->isSynchronized = true;
$this->syncAttributes();
}
public function __isset($name)
{
return isset($this->items[$name]);
}
public function __unset($name)
{
unset($this->items[$name]);
$this->syncAttributes();
}
public function isSynchronized(): bool
@ -59,4 +68,11 @@ abstract class CastsValue implements Synchronized, Arrayable
{
return $this->items;
}
public function syncAttributes(): void
{
$this->isSynchronized = false;
$this->model->syncAttributes();
$this->isSynchronized = true;
}
}

View File

@ -43,7 +43,7 @@ class DatabaseMigratorIntegrationTest extends TestCase
$dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'hyperf',
'username' => 'root',
'password' => '',

View File

@ -28,6 +28,8 @@ class DatabaseModelCustomCastingTest extends TestCase
protected function tearDown(): void
{
\Mockery::close();
UserInfoCaster::$setCount = 0;
UserInfoCaster::$getCount = 0;
}
public function testBasicCustomCasting()
@ -215,16 +217,32 @@ class DatabaseModelCustomCastingTest extends TestCase
$model->syncOriginal();
$attributes = $model->getAttributes();
$this->assertSame(['name' => 'Hyperf', 'gender' => 1], $attributes);
$this->assertSame(['name' => 'Hyperf', 'gender' => 1], Arr::only($attributes, ['name', 'gender']));
$user->name = 'Nano';
$attributes = $model->getAttributes();
$this->assertSame(['name' => 'Nano', 'gender' => 1], $attributes);
$this->assertSame(['name' => 'Nano', 'gender' => 1], Arr::only($attributes, ['name', 'gender']));
$this->assertSame(['name' => 'Nano'], $model->getDirty());
$this->assertSame(2, UserInfoCaster::$setCount);
$this->assertSame(0, UserInfoCaster::$getCount);
}
public function testCastsValueSupportNull()
{
$model = new TestModelWithCustomCast();
$model->user = $user = new UserInfo($model, ['name' => 'Hyperf', 'gender' => 1]);
$attributes = $model->getAttributes();
$this->assertSame(['name' => 'Hyperf', 'gender' => 1, 'role_id' => 0], $attributes);
$this->assertSame(0, $user->role_id);
$user->role_id = 1;
$this->assertSame(['name' => 'Hyperf', 'gender' => 1, 'role_id' => 1], $model->getAttributes());
unset($user->role_id);
$this->assertSame(['name' => 'Hyperf', 'gender' => 1, 'role_id' => null], $model->getAttributes());
$this->assertSame(null, $user->role_id);
unset($user->not_found);
$this->assertSame(['name' => 'Hyperf', 'gender' => 1, 'role_id' => null], $model->getAttributes());
}
}
class TestModelWithCustomCast extends Model
@ -325,6 +343,7 @@ class UserInfoCaster implements CastsAttributes
return [
'name' => $value->name,
'gender' => $value->gender,
'role_id' => $value->role_id,
];
}
}
@ -416,7 +435,11 @@ class Address
/**
* @property string $name
* @property int $gender
* @property null|int $role_id
*/
class UserInfo extends CastsValue
{
protected $items = [
'role_id' => 0,
];
}

View File

@ -1882,7 +1882,7 @@ class ModelTest extends TestCase
$dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'hyperf',
'username' => 'root',
'password' => '',

View File

@ -47,6 +47,7 @@ abstract class AbstractTestCase extends TestCase
'db' => [
'default' => [
'driver' => $this->driver,
'host' => '127.0.0.1',
'password' => '',
'database' => 'hyperf',
'pool' => [
@ -56,6 +57,7 @@ abstract class AbstractTestCase extends TestCase
],
'pdo' => [
'driver' => 'pdo',
'host' => '127.0.0.1',
'password' => '',
'database' => 'hyperf',
'pool' => [

View File

@ -100,7 +100,7 @@ class Container implements HyperfContainerInterface
*/
public function define(string $name, $definition)
{
$this->definitionSource->addDefinition($name, $definition);
$this->setDefinition($name, $definition);
}
/**
@ -153,7 +153,10 @@ class Container implements HyperfContainerInterface
return $this->definitionSource;
}
protected function setDefinition(string $name, DefinitionInterface $definition): void
/**
* @param array|callable|string $definition
*/
private function setDefinition(string $name, $definition): void
{
// Clear existing entry if it exists
if (array_key_exists($name, $this->resolvedEntries)) {

View File

@ -13,8 +13,10 @@ namespace HyperfTest\Di;
use Hyperf\Di\Container;
use Hyperf\Di\Definition\DefinitionSource;
use HyperfTest\Di\Stub\Bar;
use HyperfTest\Di\Stub\Foo;
use HyperfTest\Di\Stub\FooInterface;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
@ -23,6 +25,11 @@ use PHPUnit\Framework\TestCase;
*/
class ContainerTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
}
public function testHas()
{
$container = new Container(new DefinitionSource([]));
@ -44,5 +51,10 @@ class ContainerTest extends TestCase
$container = new Container(new DefinitionSource([]));
$container->define(FooInterface::class, Foo::class);
$this->assertInstanceOf(Foo::class, $container->make(FooInterface::class));
$container->define(FooInterface::class, function () {
return Mockery::mock(Bar::class);
});
$this->assertInstanceOf(Bar::class, $foo = $container->make(FooInterface::class));
}
}

View File

@ -91,7 +91,7 @@ class InjectTest extends TestCase
public function testInjectEmptyVar()
{
$this->expectException(AnnotationException::class);
$this->expectExceptionMessage('The @Inject value is invalid for HyperfTest\Di\Stub\EmptyVarValue->demo. Because Argument 1 passed to Roave\BetterReflection\TypesFinder\FindPropertyType::Roave\BetterReflection\TypesFinder\{closure}() must be an instance of phpDocumentor\Reflection\DocBlock\Tags\Var_, instance of phpDocumentor\Reflection\DocBlock\Tags\InvalidTag given');
$this->expectExceptionMessage('The @Inject value is invalid for HyperfTest\Di\Stub\EmptyVarValue->demo');
BetterReflectionManager::initClassReflector([__DIR__ . '/Stub']);

View File

@ -14,6 +14,8 @@ namespace HyperfTest\GrpcClient;
use Grpc\Info;
use Hyperf\Grpc\Parser;
use Hyperf\GrpcClient\Request;
use Hyperf\Utils\Composer;
use Jean85\PrettyVersions;
use PHPUnit\Framework\TestCase;
/**
@ -22,6 +24,14 @@ use PHPUnit\Framework\TestCase;
*/
class RequestTest extends TestCase
{
protected function setUp(): void
{
$json = Composer::getLockContent();
if (version_compare($json['plugin-api-version'], '2.0.0', '>=')) {
$this->markTestSkipped(PrettyVersions::class . ' does not support composer v2.0');
}
}
public function testRequest()
{
$request = new Request($path = 'grpc.service/path', $info = new Info());

View File

@ -42,7 +42,13 @@ class CoreMiddleware extends \Hyperf\RpcServer\CoreMiddleware
// Route found, but the handler does not exist.
return $this->responseBuilder->buildErrorResponse($request, ResponseBuilder::INTERNAL_ERROR);
}
$parameters = $this->parseMethodParameters($controller, $action, $request->getParsedBody());
try {
$parameters = $this->parseMethodParameters($controller, $action, $request->getParsedBody());
} catch (\InvalidArgumentException $exception) {
return $this->responseBuilder->buildErrorResponse($request, ResponseBuilder::INVALID_PARAMS);
}
try {
$response = $controllerInstance->{$action}(...$parameters);
} catch (\Throwable $exception) {

View File

@ -60,6 +60,14 @@ trait Cacheable
return $manager->destroy([$this->getKey()], get_called_class());
}
/**
* Get the expire time for cache.
*/
public function getCacheTTL(): ?int
{
return null;
}
/**
* Increment a column's value by a given amount.
* @param string $column

View File

@ -21,4 +21,6 @@ interface CacheableInterface
public static function findManyFromCache(array $ids): Collection;
public function deleteCache(): bool;
public function getCacheTTL(): ?int;
}

View File

@ -90,7 +90,7 @@ class Manager
if (is_null($data)) {
$model = $instance->newQuery()->where($primaryKey, '=', $id)->first();
if ($model) {
$ttl = $handler->getConfig()->getTtl();
$ttl = $this->getCacheTTL($instance, $handler);
$handler->set($key, $this->formatModel($model), $ttl);
} else {
$ttl = $handler->getConfig()->getEmptyModelTtl();
@ -141,7 +141,7 @@ class Manager
$targetIds = array_diff($ids, $fetchIds);
if ($targetIds) {
$models = $instance->newQuery()->whereIn($primaryKey, $targetIds)->get();
$ttl = $handler->getConfig()->getTtl();
$ttl = $this->getCacheTTL($instance, $handler);
/** @var Model $model */
foreach ($models as $model) {
$id = $model->getKey();
@ -217,6 +217,17 @@ class Manager
return false;
}
/**
* @return \DateInterval|int
*/
protected function getCacheTTL(Model $instance, HandlerInterface $handler)
{
if ($instance instanceof CacheableInterface) {
return $instance->getCacheTTL() ?? $handler->getConfig()->getTtl();
}
return $handler->getConfig()->getTtl();
}
/**
* @param int|string $id
*/

View File

@ -15,6 +15,8 @@ use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\DbConnection\Collector\TableCollector;
use Hyperf\ModelCache;
use Hyperf\ModelCache\Handler\HandlerInterface;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\ModelCache\Stub\ManagerStub;
use HyperfTest\ModelCache\Stub\ModelStub;
@ -60,6 +62,37 @@ class ManagerTest extends TestCase
$this->assertSame(['id' => 1, 'json_data' => json_encode($json), 'str' => null, 'float_num' => 0.1], $res);
}
public function testGetCacheTTL()
{
$container = Mockery::mock(ContainerInterface::class);
$container->shouldReceive('get')->once()->with(StdoutLoggerInterface::class)->andReturn(new StdoutLogger());
$container->shouldReceive('get')->once()->with(ConfigInterface::class)->andReturn(new Config($this->getConfig()));
$container->shouldReceive('make')->with(ContainerInterface::class)->andReturn($container);
$container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn(null);
$container->shouldReceive('get')->with(TableCollector::class)->andReturn(new TableCollector());
ApplicationContext::setContainer($container);
$handler = Mockery::mock(HandlerInterface::class);
$handler->shouldReceive('getConfig')->andReturnUsing(function () {
return new ModelCache\Config([
'ttl' => 1000,
], 'default');
});
$manager = new ManagerStub($container);
$model = new ModelStub();
$this->assertSame(1000, $manager->getCacheTTL($model, $handler));
$model = new class() extends ModelStub implements ModelCache\CacheableInterface {
use ModelCache\Cacheable;
public function getCacheTTL(): ?int
{
return 100;
}
};
$this->assertSame(100, $manager->getCacheTTL($model, $handler));
}
protected function getConfig(): array
{
return [

View File

@ -368,4 +368,17 @@ class ModelCacheTest extends TestCase
$model->delete();
}
public function testModelCacheTTL()
{
$container = ContainerStub::mockContainer();
$model = new BookModel();
$this->assertSame(100, $model->getCacheTTL());
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$redis->del('{mc:default:m:book}:id:1');
BookModel::findFromCache(1);
$this->assertSame(100, $redis->ttl('{mc:default:m:book}:id:1'));
}
}

View File

@ -56,4 +56,9 @@ class BookModel extends Model implements CacheableInterface
{
return $this->morphOne(ImageModel::class, 'imageable');
}
public function getCacheTTL(): ?int
{
return 100;
}
}

View File

@ -103,7 +103,7 @@ class ContainerStub
],
'redis' => [
'default' => [
'host' => 'localhost',
'host' => '127.0.0.1',
'auth' => null,
'port' => 6379,
'db' => 0,

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\ModelCache\Stub;
use Hyperf\Database\Model\Model;
use Hyperf\ModelCache\Handler\HandlerInterface;
class ManagerStub extends \Hyperf\ModelCache\Manager
{
@ -19,4 +20,9 @@ class ManagerStub extends \Hyperf\ModelCache\Manager
{
return parent::formatModel($model);
}
public function getCacheTTL(Model $instance, HandlerInterface $handler)
{
return parent::getCacheTTL($instance, $handler);
}
}

View File

@ -33,7 +33,7 @@ class ContainerStub
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn(new Config([
'redis' => [
'default' => [
'host' => 'localhost',
'host' => '127.0.0.1',
'auth' => null,
'port' => 6379,
'db' => 0,

View File

@ -42,11 +42,17 @@ class RetryBudget implements RetryBudgetInterface
*/
private $timerId;
/**
* @var float|int
*/
private $maxToken;
public function __construct(int $ttl, int $minRetriesPerSec, float $percentCanRetry)
{
$this->ttl = $ttl;
$this->minRetriesPerSec = $minRetriesPerSec;
$this->percentCanRetry = $percentCanRetry;
$this->maxToken = ($this->minRetriesPerSec / $this->percentCanRetry) * $this->ttl;
$this->budget = new SplQueue();
}
@ -70,9 +76,7 @@ class RetryBudget implements RetryBudgetInterface
for ($i = 0; $i < $this->minRetriesPerSec / $this->percentCanRetry; ++$i) {
$this->produce();
}
while (! $this->budget->isEmpty()
&& $this->budget->top() <= microtime(true)
) {
while ($this->hasOverflown()) {
$this->budget->dequeue();
}
});
@ -98,4 +102,10 @@ class RetryBudget implements RetryBudgetInterface
$t = microtime(true) + $this->ttl;
$this->budget->push($t);
}
public function hasOverflown(): bool
{
return (! $this->budget->isEmpty() && $this->budget->bottom() <= microtime(true))
|| $this->budget->count() > $this->maxToken;
}
}

View File

@ -68,5 +68,20 @@ class RetryBudgetTest extends TestCase
$this->assertTrue($budget->consume());
$this->assertTrue($budget->consume());
$this->assertTrue(! $budget->consume());
// Retry budget should never have more than 1 token in this test
$budget = new RetryBudget(
1,
1,
1
);
$budget->init();
$ref = new \ReflectionClass(RetryBudget::class);
$prop = $ref->getProperty('budget');
$prop->setAccessible(true);
System::sleep(1.2);
$this->assertLessThanOrEqual(1, $prop->getValue($budget)->count());
System::sleep(1.2);
$this->assertLessThanOrEqual(1, $prop->getValue($budget)->count());
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\SwooleTracker\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\HttpServer\Server;
use Psr\Container\ContainerInterface;
use function trackerHookMalloc;
class OnRequestAspect extends AbstractAspect
{
public $classes = [
Server::class . '::onRequest',
];
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
if (function_exists('trackerHookMalloc')) {
trackerHookMalloc();
}
return $proceedingJoinPoint->process();
}
}

View File

@ -43,12 +43,15 @@ class TraceMiddleware implements MiddlewareInterface
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$span = $this->buildSpan($request);
$response = $handler->handle($request);
$span->finish();
defer(function () {
$this->tracer->flush();
});
try {
$response = $handler->handle($request);
} finally {
$span->finish();
}
return $response;
}

View File

@ -516,7 +516,7 @@ class Arr
if ($isAssoc) {
foreach ($array2 as $key => $value) {
if (is_array($value)) {
$array1[$key] = static::merge($array1[$key], $value, $unique);
$array1[$key] = static::merge($array1[$key] ?? [], $value, $unique);
} else {
$array1[$key] = $value;
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils;
class Resource
{
/**
* TODO: Swoole file hook does not support `php://temp` and `php://memory`.
* @return false|resource
*/
public static function from(string $body, string $filename = 'php://temp')
{
$resource = fopen($filename, 'r+');
if ($body !== '') {
fwrite($resource, $body);
fseek($resource, 0);
}
return $resource;
}
public static function fromMemory(string $body)
{
return static::from($body, 'php://memory');
}
}

View File

@ -73,6 +73,8 @@ class ArrTest extends TestCase
$this->assertSame(['id' => 1, 'ids' => [1, 2, 3], 'name' => 'Hyperf'], Arr::merge(['id' => 1, 'ids' => [1, 2]], ['name' => 'Hyperf', 'ids' => [1, 2, 3]]));
$this->assertSame(['id' => 1, 'ids' => [1, 2, 1, 2, 3], 'name' => 'Hyperf'], Arr::merge(['id' => 1, 'ids' => [1, 2]], ['name' => 'Hyperf', 'ids' => [1, 2, 3]], false));
$this->assertSame(['id' => 1, 'name' => ['Hyperf']], Arr::merge(['id' => 2], ['id' => 1, 'name' => ['Hyperf']]));
$array1 = [
'logger' => [
'default' => [

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Utils;
use Hyperf\Utils\Resource;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ResourceTest extends TestCase
{
public function testFrom()
{
$data = '123123';
$resource = Resource::from($data);
$this->assertSame('1', fread($resource, 1));
$this->assertSame('23', fread($resource, 2));
$this->assertSame('123', fread($resource, 10));
}
public function testFromMemoryLeak()
{
$data = str_repeat('1', 1024 * 1024);
$memory = memory_get_usage(true);
for ($i = 0; $i < 100; ++$i) {
Resource::fromMemory($data);
$current = memory_get_usage(true);
$leak = $current - $memory;
$memory = $current;
}
$this->assertSame(0, $leak);
}
}

View File

@ -48,7 +48,7 @@ class ValidationExistsRuleTest extends TestCase
$connector = new ConnectionFactory($container);
$dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'hyperf',
'username' => 'root',
'password' => '',

View File

@ -27,12 +27,26 @@ class FindDriver implements DriverInterface
/**
* @var bool
*/
protected $isDarwin;
protected $isDarwin = false;
/**
* @var bool
*/
protected $isSupportFloatMinutes = true;
/**
* @var int
*/
protected $startTime;
public function __construct(Option $option)
{
$this->option = $option;
$this->isDarwin = PHP_OS === 'Darwin';
if (PHP_OS === 'Darwin') {
$this->isDarwin = true;
} else {
$this->isDarwin = false;
}
if ($this->isDarwin) {
$ret = System::exec('which gfind');
if (empty($ret['output'])) {
@ -43,21 +57,27 @@ class FindDriver implements DriverInterface
if (empty($ret['output'])) {
throw new \InvalidArgumentException('find not exists.');
}
$ret = System::exec('find --help', true);
$this->isSupportFloatMinutes = (strpos($ret['output'] ?? '', 'BusyBox')) === false;
}
}
public function watch(Channel $channel): void
{
$this->startTime = time();
$ms = $this->option->getScanInterval();
Timer::tick($ms, function () use ($channel, $ms) {
$seconds = ceil(($ms + 1000) / 1000);
if ($this->isSupportFloatMinutes) {
$minutes = sprintf('-%.2f', $seconds / 60);
} else {
$minutes = sprintf('-%d', ceil($seconds / 60));
}
Timer::tick($ms, function () use ($channel, $minutes) {
global $fileModifyTimes;
if (is_null($fileModifyTimes)) {
$fileModifyTimes = [];
}
$seconds = ceil(($ms + 1000) / 1000);
$minutes = sprintf('-%.2f', $seconds / 60);
[$fileModifyTimes, $changedFiles] = $this->scan($fileModifyTimes, $minutes);
foreach ($changedFiles as $file) {
@ -70,27 +90,27 @@ class FindDriver implements DriverInterface
{
$changedFiles = [];
$dest = implode(' ', $targets);
$ret = System::exec($this->getBin() . ' ' . $dest . ' -mmin ' . $minutes . ' -type f -printf "%p %T+' . PHP_EOL . '"');
$ret = System::exec($this->getBin() . ' ' . $dest . ' -mmin ' . $minutes . ' -type f -print');
if ($ret['code'] === 0 && strlen($ret['output'])) {
$stdout = $ret['output'];
$lineArr = explode(PHP_EOL, $stdout);
foreach ($lineArr as $line) {
$fileArr = explode(' ', $line);
if (count($fileArr) == 2) {
$pathName = $fileArr[0];
$modifyTime = $fileArr[1];
if (! empty($ext) && ! Str::endsWith($pathName, $ext)) {
continue;
}
if (isset($fileModifyTimes[$pathName]) && $fileModifyTimes[$pathName] == $modifyTime) {
continue;
}
$fileModifyTimes[$pathName] = $modifyTime;
$changedFiles[] = $pathName;
$pathName = $line;
$modifyTime = fileatime($pathName);
// modifyTime less than or equal to startTime continue
if ($modifyTime <= $this->startTime) {
continue;
}
if (! empty($ext) && ! Str::endsWith($pathName, $ext)) {
continue;
}
if (isset($fileModifyTimes[$pathName]) && $fileModifyTimes[$pathName] == $modifyTime) {
continue;
}
$fileModifyTimes[$pathName] = $modifyTime;
$changedFiles[] = $pathName;
}
}