├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .php-cs-fixer.php
├── .phpstorm.meta.php
├── .travis.yml
├── LICENSE
├── README-CN.md
├── README.md
├── bin
├── build.sh
└── mongo-proxy-darwin-arm64
├── cmd
└── app.go
├── composer.json
├── example
├── config
│ ├── main.php
│ └── sidecar.go
├── go2php
│ ├── main.php
│ └── sidecar.go
├── main.php
├── mongo_client
│ ├── main.php
│ └── sidecar.go
└── sidecar.go
├── go.mod
├── go.sum
├── phpunit.xml
├── pkg
├── config
│ ├── config.go
│ └── config_test.go
├── gotask
│ ├── client.go
│ ├── common.go
│ ├── common_test.go
│ ├── flag.go
│ ├── generator.go
│ ├── generator_test.go
│ ├── middleware.go
│ ├── server.go
│ └── server_test.go
├── log
│ ├── log.go
│ └── log_test.go
└── mongo_client
│ ├── bulk_write_model_factory.go
│ ├── config.go
│ ├── config_test.go
│ ├── flag.go
│ ├── middleware.go
│ ├── middleware_test.go
│ └── mongo_client.go
├── publish
├── go.mod
└── gotask.php
└── src
├── Config
└── DomainConfig.php
├── ConfigProvider.php
├── Exception
├── GoBuildException.php
└── InvalidGoTaskConnectionException.php
├── GoTask.php
├── GoTaskConnection.php
├── GoTaskConnectionPool.php
├── GoTaskFactory.php
├── GoTaskProxy.php
├── IPC
├── IPCReceiverInterface.php
├── IPCSenderInterface.php
├── PipeIPCSender.php
├── SocketIPCReceiver.php
└── SocketIPCSender.php
├── Listener
├── CommandListener.php
├── Go2PhpListener.php
├── LogRedirectListener.php
└── PipeLockListener.php
├── MongoClient
├── Collection.php
├── Database.php
├── MongoClient.php
├── MongoProxy.php
├── MongoTrait.php
└── Type
│ ├── BulkWriteResult.php
│ ├── DeleteResult.php
│ ├── IndexInfo.php
│ ├── InsertManyResult.php
│ ├── InsertOneResult.php
│ └── UpdateResult.php
├── PipeGoTask.php
├── Process
└── GoTaskProcess.php
├── Relay
├── ConnectionRelay.php
├── CoroutineSocketRelay.php
├── ProcessPipeRelay.php
├── RelayInterface.php
└── SocketTransporter.php
├── SocketGoTask.php
├── SocketIPCFactory.php
├── WithGoTask.php
└── Wrapper
├── ByteWrapper.php
├── ConfigWrapper.php
└── LoggerWrapper.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests export-ignore
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | # Sequence of patterns matched against refs/tags
4 | tags:
5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
6 |
7 | name: Release
8 |
9 | jobs:
10 | release:
11 | name: Release
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Create Release
17 | id: create_release
18 | uses: actions/create-release@v1
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 | with:
22 | tag_name: ${{ github.ref }}
23 | release_name: Release ${{ github.ref }}
24 | draft: false
25 | prerelease: false
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | *.cache
4 | *.log
5 | .idea/
6 | /app
7 | /mongo
8 | /example/app
9 | /example/go2php/app
10 | /example/config/app
11 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
14 | ->setRules([
15 | '@PSR2' => true,
16 | '@Symfony' => true,
17 | '@DoctrineAnnotation' => true,
18 | '@PhpCsFixer' => true,
19 | 'header_comment' => [
20 | 'comment_type' => 'PHPDoc',
21 | 'header' => $header,
22 | 'separate' => 'none',
23 | 'location' => 'after_declare_strict',
24 | ],
25 | 'array_syntax' => [
26 | 'syntax' => 'short'
27 | ],
28 | 'list_syntax' => [
29 | 'syntax' => 'short'
30 | ],
31 | 'concat_space' => [
32 | 'spacing' => 'one'
33 | ],
34 | 'blank_line_before_statement' => [
35 | 'statements' => [
36 | 'declare',
37 | ],
38 | ],
39 | 'general_phpdoc_annotation_remove' => [
40 | 'annotations' => [
41 | 'author'
42 | ],
43 | ],
44 | 'ordered_imports' => [
45 | 'imports_order' => [
46 | 'class', 'function', 'const',
47 | ],
48 | 'sort_algorithm' => 'alpha',
49 | ],
50 | 'single_line_comment_style' => [
51 | 'comment_types' => [
52 | ],
53 | ],
54 | 'yoda_style' => [
55 | 'always_move_variable' => false,
56 | 'equal' => false,
57 | 'identical' => false,
58 | ],
59 | 'phpdoc_align' => [
60 | 'align' => 'left',
61 | ],
62 | 'multiline_whitespace_before_semicolons' => [
63 | 'strategy' => 'no_multi_line',
64 | ],
65 | 'constant_case' => [
66 | 'case' => 'lower',
67 | ],
68 | 'global_namespace_import' => [
69 | 'import_classes' => true,
70 | 'import_constants' => true,
71 | 'import_functions' => true,
72 | ],
73 | 'phpdoc_to_comment' => false,
74 | 'class_attributes_separation' => true,
75 | 'combine_consecutive_unsets' => true,
76 | 'declare_strict_types' => true,
77 | 'linebreak_after_opening_tag' => true,
78 | 'lowercase_static_reference' => true,
79 | 'no_useless_else' => true,
80 | 'no_unused_imports' => true,
81 | 'not_operator_with_successor_space' => true,
82 | 'not_operator_with_space' => false,
83 | 'ordered_class_elements' => true,
84 | 'php_unit_strict' => false,
85 | 'phpdoc_separation' => false,
86 | 'single_quote' => true,
87 | 'standardize_not_equals' => true,
88 | 'multiline_comment_opening_closing' => true,
89 | 'single_line_empty_body' => false,
90 | ])
91 | ->setFinder(
92 | PhpCsFixer\Finder::create()
93 | ->exclude('vendor')
94 | ->in(__DIR__)
95 | )
96 | ->setUsingCache(false);
97 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 | > `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`
28 | - phpenv config-rm xdebug.ini || echo "xdebug not available"
29 | - phpenv config-add ./tests/ci.ini
30 |
31 |
32 | before_script:
33 | - cd $TRAVIS_BUILD_DIR
34 | - composer config -g process-timeout 900 && composer update
35 |
36 | script:
37 | - composer analyse
38 | - composer test
39 | - composer test-go
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Hyperf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-CN.md:
--------------------------------------------------------------------------------
1 | # GoTask
2 |
3 | [English](./README.md) | 中文
4 |
5 | [](https://travis-ci.org/hyperf/gotask)
6 |
7 | GoTask通过[Swoole进程管理功能](https://wiki.swoole.com/#/process)启动Go进程作为Swoole主进程边车(Sidecar),利用[进程通讯](https://wiki.swoole.com/#/learn?id=%e4%bb%80%e4%b9%88%e6%98%afipc)将任务投递给边车处理并接收返回值。可以理解为Go版的Swoole TaskWorker。
8 |
9 | ```bash
10 | composer require hyperf/gotask
11 | ```
12 |
13 | ## 特性
14 |
15 | * [超高速低消耗](https://github.com/reasno/gotask-benchmark)
16 | * Co/Socket实现,100%协程化
17 | * 支持Unix Socket、TCP、stdin/stdout管道
18 | * PHP与Go双向通讯
19 | * 边车自动启停
20 | * 支持远程异常捕获
21 | * 支持结构化数据、二进制数据投递
22 | * go边车兼容[net/rpc](https://cloud.tencent.com/developer/section/1143675)
23 | * 自带连接池支持
24 | * 可独立使用,也可深度融合Hyperf
25 |
26 | ## 使用场景
27 | * 执行阻塞函数,如MongoDB查询
28 | * 执行CPU密集操作,如编码解码
29 | * 接入Go语言生态,如Kubernetes
30 |
31 | ## 使用要求
32 |
33 | * PHP 7.2+
34 | * Go 1.13+
35 | * Swoole 4.4LTS+
36 | * Hyperf 1.1+ (optional)
37 |
38 | ## 示例
39 |
40 | ```go
41 | package main
42 |
43 | import (
44 | "github.com/hyperf/gotask/v2/pkg/gotask"
45 | )
46 |
47 | type App struct{}
48 |
49 | func (a *App) Hi(name string, r *interface{}) error {
50 | *r = map[string]string{
51 | "hello": name,
52 | }
53 | return nil
54 | }
55 |
56 | func main() {
57 | gotask.SetAddress("127.0.0.1:6001")
58 | gotask.Register(new(App))
59 | gotask.Run()
60 | }
61 | ```
62 |
63 | ```php
64 | call("App.Hi", "Hyperf"));
74 | // 打印 [ "hello" => "Hyperf" ]
75 | });
76 |
77 | ```
78 |
79 | ## 文档
80 | * [安装与配置](https://github.com/Hyperf/gotask/wiki/Installation-&-Configuration)
81 | * [文档](https://github.com/Hyperf/gotask/wiki/Documentation)
82 | * [FAQ](https://github.com/Hyperf/gotask/wiki/FAQ)
83 | * [示例](https://github.com/Hyperf/gotask/tree/master/example)
84 | * [Hyperf示例](https://github.com/Hyperf/gotask-benchmark/blob/master/app/Controller/IndexController.php)
85 |
86 | ## Benchmark
87 |
88 | https://github.com/reasno/gotask-benchmark
89 |
90 | ## 鸣谢
91 | * https://github.com/spiral/goridge 提供了IPC通讯的编码和解码。
92 | * https://github.com/twose 提供了人肉答疑支持。
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GoTask
2 |
3 | English | [中文](./README-CN.md)
4 |
5 | [](https://travis-ci.org/hyperf/gotask)
6 |
7 | GoTask spawns a go process as a Swoole sidecar and establishes a bi-directional IPC to offload heavy-duties to Go. Think of it as a Swoole Taskworker in Go.
8 |
9 | ```bash
10 | composer require hyperf/gotask
11 | ```
12 |
13 | ## Feature
14 |
15 | * [High performance with low footprint.](https://github.com/reasno/gotask-benchmark)
16 | * Based on Swoole 4 coroutine socket API.
17 | * Support Unix Socket, TCP and stdin/stdout pipes.
18 | * Support both PHP-to-Go and Go-to-PHP calls.
19 | * Automatic sidecar lifecycle management.
20 | * Correctly handle remote error.
21 | * Support both structural payload and binary payload.
22 | * Sidecar API compatible with net/rpc.
23 | * Baked-in connection pool.
24 | * Optionally integrated with Hyperf framework.
25 |
26 | ## Perfect For
27 | * Blocking operations in Swoole, such as MongoDB queries.
28 | * CPU Intensive operations, such as encoding and decoding.
29 | * Leveraging Go eco-system, such as Kubernetes clients.
30 |
31 | ## Requirement
32 |
33 | * PHP 7.2+
34 | * Go 1.13+
35 | * Swoole 4.4LTS+
36 | * Hyperf 1.1+ (optional)
37 |
38 | ## Task Delivery Demo
39 |
40 | ```go
41 | package main
42 |
43 | import (
44 | "github.com/hyperf/gotask/v2/pkg/gotask"
45 | )
46 |
47 | type App struct{}
48 |
49 | func (a *App) Hi(name string, r *interface{}) error {
50 | *r = map[string]string{
51 | "hello": name,
52 | }
53 | return nil
54 | }
55 |
56 | func main() {
57 | gotask.SetAddress("127.0.0.1:6001")
58 | gotask.Register(new(App))
59 | gotask.Run()
60 | }
61 | ```
62 |
63 | ```php
64 | call("App.Hi", "Hyperf"));
74 | // [ "hello" => "Hyperf" ]
75 | });
76 |
77 | ```
78 |
79 | ## Resources
80 | > English documentation is not yet complete! Please see examples first.
81 |
82 | * [Installation](https://github.com/Hyperf/gotask/wiki/Installation-&-Configuration)
83 | * [Document](https://github.com/Hyperf/gotask/wiki/Document)
84 | * [FAQ](https://github.com/Hyperf/gotask/wiki/FAQ)
85 | * [Example](https://github.com/Hyperf/gotask/tree/master/example)
86 | * [Hyperf Example](https://github.com/reasno/gotask-benchmark/blob/master/app/Controller/IndexController.php)
87 |
88 | ## Benchmark
89 |
90 | https://github.com/reasno/gotask-benchmark
91 |
92 | ## Credit
93 | * https://github.com/spiral/goridge provides the IPC protocol.
94 | * https://github.com/twose helps the creation of this project.
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/bin/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | package=../example/mongo_client/sidecar.go
3 | package_name=mongo-proxy
4 |
5 | #the full list of the platforms: https://golang.org/doc/install/source#environment
6 | platforms=(
7 | "darwin/amd64"
8 | "linux/amd64"
9 | "darwin/arm64"
10 | )
11 |
12 | for platform in "${platforms[@]}"
13 | do
14 | platform_split=(${platform//\// })
15 | GOOS=${platform_split[0]}
16 | GOARCH=${platform_split[1]}
17 | output_name=$package_name'-'$GOOS'-'$GOARCH
18 | if [ $GOOS = "windows" ]; then
19 | output_name+='.exe'
20 | fi
21 | echo GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="-s -w" -o $output_name $package
22 | GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="-s -w" -o $output_name $package
23 | if [ $? -ne 0 ]; then
24 | echo 'An error has occurred! Aborting the script execution...'
25 | exit 1
26 | fi
27 | done
28 |
--------------------------------------------------------------------------------
/bin/mongo-proxy-darwin-arm64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyperf/gotask/fc4cc32c4f2e6414b2decb5a7c1ffe359f553599/bin/mongo-proxy-darwin-arm64
--------------------------------------------------------------------------------
/cmd/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/hyperf/gotask/v2/pkg/gotask"
7 | )
8 |
9 | // App sample
10 | type App struct{}
11 |
12 | // Hi returns greeting message.
13 | func (a *App) Hi(name interface{}, r *interface{}) error {
14 | *r = map[string]interface{}{
15 | "hello": name,
16 | }
17 | return nil
18 | }
19 |
20 | func main() {
21 | if err := gotask.Register(new(App)); err != nil {
22 | log.Fatalln(err)
23 | }
24 | if err := gotask.Run(); err != nil {
25 | log.Fatalln(err)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hyperf/gotask",
3 | "type": "library",
4 | "license": "MIT",
5 | "keywords": [
6 | "php",
7 | "hyperf"
8 | ],
9 | "description": "A replacement for Swoole TaskWorker in Go",
10 | "autoload": {
11 | "psr-4": {
12 | "Hyperf\\GoTask\\": "src/"
13 | }
14 | },
15 | "autoload-dev": {
16 | "psr-4": {
17 | "HyperfTest\\": "tests"
18 | }
19 | },
20 | "require": {
21 | "php": ">=8.1",
22 | "ext-swoole": ">=5.0",
23 | "hyperf/pool": "^3.0",
24 | "hyperf/process": "^3.0",
25 | "spiral/goridge": "^2.4",
26 | "symfony/event-dispatcher": "^6.3"
27 | },
28 | "require-dev": {
29 | "friendsofphp/php-cs-fixer": "^3.21",
30 | "hyperf/command": "^3.0",
31 | "hyperf/config": "^3.0",
32 | "hyperf/di": "^3.0",
33 | "hyperf/framework": "^3.0",
34 | "hyperf/testing": "^3.0",
35 | "mockery/mockery": "^1.6",
36 | "phpstan/phpstan": "^1.10",
37 | "swoole/ide-helper": "^5.0"
38 | },
39 | "config": {
40 | "sort-packages": true
41 | },
42 | "scripts": {
43 | "test": "go build -o app example/*.go && go build -o mongo example/mongo_client/*.go && phpunit -c phpunit.xml --colors=always",
44 | "start-test-server": "php tests/TestServer.php",
45 | "test-go": "/bin/bash -c 'php tests/TestServer.php & sleep 5 && go test ./...'",
46 | "analyse": "phpstan analyse --memory-limit 300M -l 0 ./src",
47 | "cs-fix": "php-cs-fixer fix $1",
48 | "binary": "go build -o mongo example/mongo_client/*.go"
49 | },
50 | "extra": {
51 | "branch-alias": {
52 | "dev-master": "3.0-dev"
53 | },
54 | "hyperf": {
55 | "config": "Hyperf\\GoTask\\ConfigProvider"
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/example/config/main.php:
--------------------------------------------------------------------------------
1 | set(ConfigInterface::class, new Config([
31 | 'gotask' => [
32 | 'enable' => true,
33 | 'socket_address' => ADDR,
34 | 'pool' => [
35 | 'min_connections' => 1,
36 | 'max_connections' => 100,
37 | 'connect_timeout' => 10.0,
38 | 'wait_timeout' => 3.0,
39 | 'heartbeat' => -1,
40 | 'max_idle_time' => (float) env('GOTASK_MAX_IDLE_TIME', 60),
41 | ],
42 | ],
43 | ]));
44 | $container->define(
45 | StdoutLoggerInterface::class,
46 | StdoutLogger::class
47 | );
48 | ApplicationContext::setContainer($container);
49 | exec('go build -o ' . __DIR__ . '/app ' . __DIR__ . '/sidecar.go');
50 | $process = new Process(function (Process $process) {
51 | sleep(1);
52 | $process->exec(__DIR__ . '/app', ['-go2php-address', ADDR]);
53 | }, false, 0, true);
54 | $process->start();
55 |
56 | run(function () {
57 | $server = new SocketIPCReceiver(ADDR);
58 | $server->start();
59 | });
60 |
--------------------------------------------------------------------------------
/example/config/sidecar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/hyperf/gotask/v2/pkg/config"
7 | )
8 |
9 | func main() {
10 | addr, err := config.Get("gotask.socket_address", "default")
11 | if err != nil {
12 | log.Fatalln(err)
13 | }
14 | log.Println(addr)
15 | addr, err = config.Get("gotask.non_exist", "default")
16 | if err != nil {
17 | log.Fatalln(err)
18 | }
19 | log.Println(addr)
20 | err = config.Set("gotask.non_exist", "exist")
21 | if err != nil {
22 | log.Fatalln(err)
23 | }
24 | addr, err = config.Get("gotask.non_exist", "")
25 | if err != nil {
26 | log.Fatalln(err)
27 | }
28 | log.Println(addr)
29 | has, err := config.Has("gotask.non_exist")
30 | if err != nil {
31 | log.Fatalln(err)
32 | }
33 | log.Println(has)
34 | }
35 |
--------------------------------------------------------------------------------
/example/go2php/main.php:
--------------------------------------------------------------------------------
1 | exec(__DIR__ . '/app', ['-go2php-address', ADDR]);
25 | }, false, 0, true);
26 | $process->start();
27 |
28 | run(function () {
29 | $server = new SocketIPCReceiver(ADDR);
30 | $server->start();
31 | });
32 |
33 | class Example
34 | {
35 | public function HelloString(string $payload)
36 | {
37 | return "Hello, {$payload}!";
38 | }
39 |
40 | public function HelloInterface(array $payload)
41 | {
42 | return ['hello' => $payload];
43 | }
44 |
45 | public function HelloStruct(array $payload)
46 | {
47 | return ['hello' => $payload];
48 | }
49 |
50 | public function HelloBytes(string $payload)
51 | {
52 | return new \Hyperf\GoTask\Wrapper\ByteWrapper(base64_encode($payload));
53 | }
54 |
55 | public function HelloError(array $payload)
56 | {
57 | throw new \Exception('err');
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/go2php/sidecar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/hyperf/gotask/v2/pkg/gotask"
9 | )
10 |
11 | func main() {
12 | client, err := gotask.NewAutoClient()
13 | if err != nil {
14 | log.Fatalln(err)
15 | }
16 | defer client.Close()
17 |
18 | {
19 | var res []byte
20 | err = client.Call("Example::HelloString", "Hyperf", &res)
21 | if err != nil {
22 | log.Fatalln(err)
23 | }
24 | fmt.Println(string(res))
25 | }
26 |
27 | {
28 | var p interface{}
29 | p = []string{"jack", "jill"}
30 | var res interface{}
31 | err = client.Call("Example::HelloInterface", p, &res)
32 | if err != nil {
33 | log.Fatalln(err)
34 | }
35 | fmt.Printf("%+v\n", res)
36 | }
37 |
38 | {
39 | type Name struct {
40 | Id int `json:"id"`
41 | FirstName string `json:"firstName"`
42 | LastName string `json:"lastName"`
43 | }
44 | var res struct {
45 | Hello interface{} `json:"hello"`
46 | }
47 | err = client.Call("Example::HelloStruct", Name{Id: 23, FirstName: "LeBron", LastName: "James"}, &res)
48 | if err != nil {
49 | log.Fatalln(err)
50 | }
51 | fmt.Printf("%+v\n", res)
52 | }
53 |
54 | {
55 | var p []byte
56 | var res []byte
57 | p = make([]byte, 100)
58 | base64.StdEncoding.Encode(p, []byte("My Bytes"))
59 | err = client.Call("Example::HelloBytes", p, &res)
60 | if err != nil {
61 | log.Fatalln(err)
62 | }
63 | fmt.Printf("%+v\n", string(res))
64 | }
65 |
66 | {
67 | var res interface{}
68 | err = client.Call("Example::HelloError", "Hyperf", &res)
69 | if err != nil {
70 | log.Fatalln(err)
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/example/main.php:
--------------------------------------------------------------------------------
1 | exec(__DIR__ . '/app', ['-address', ADDR]);
25 | });
26 | $process->start();
27 |
28 | sleep(1);
29 |
30 | run(function () {
31 | $task = new SocketIPCSender(ADDR);
32 | var_dump($task->call('App.HelloString', 'Hyperf'));
33 | var_dump($task->call('App.HelloInterface', ['jack', 'jill']));
34 | var_dump($task->call('App.HelloStruct', [
35 | 'firstName' => 'LeBron',
36 | 'lastName' => 'James',
37 | 'id' => 23,
38 | ]));
39 | var_dump($task->call('App.HelloBytes', base64_encode('My Bytes'), GoTask::PAYLOAD_RAW));
40 | try {
41 | $task->call('App.HelloError', 'Hyperf');
42 | } catch (\Throwable $e) {
43 | var_dump($e);
44 | }
45 | try {
46 | $task->call('App.HelloPanic', '');
47 | } catch (\Throwable $e) {
48 | var_dump($e);
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/example/mongo_client/main.php:
--------------------------------------------------------------------------------
1 | exec(__DIR__ . '/app', ['-address', ADDR]);
27 | });
28 | $process->start();
29 |
30 | sleep(1);
31 |
32 | run(function () {
33 | $task = new SocketIPCSender(ADDR);
34 | $client = new MongoClient(new MongoProxy($task), new Config([]));
35 | $collection = $client->database('testing')->collection('unit');
36 | $collection->insertOne(['foo' => 'bar', 'tid' => 0]);
37 | });
38 |
--------------------------------------------------------------------------------
/example/mongo_client/sidecar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 |
7 | "github.com/hyperf/gotask/v2/pkg/gotask"
8 | "github.com/hyperf/gotask/v2/pkg/mongo_client"
9 | "go.mongodb.org/mongo-driver/mongo"
10 | "go.mongodb.org/mongo-driver/mongo/options"
11 | )
12 |
13 | func main() {
14 | mongoConfig := mongo_client.LoadConfig()
15 | ctx, cancel := context.WithTimeout(context.Background(), mongoConfig.ConnectTimeout)
16 | defer cancel()
17 |
18 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoConfig.Uri))
19 | if err != nil {
20 | log.Fatalln(err)
21 | }
22 |
23 | if err := gotask.Register(mongo_client.NewMongoProxyWithTimeout(client, mongoConfig.ReadWriteTimeout)); err != nil {
24 | log.Fatalln(err)
25 | }
26 |
27 | if err := gotask.Run(); err != nil {
28 | log.Fatalln(err)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/sidecar.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "github.com/hyperf/gotask/v2/pkg/gotask"
8 | "io/ioutil"
9 | "log"
10 | )
11 |
12 | // App sample
13 | type App struct{}
14 |
15 | func (a *App) HelloString(name string, r *interface{}) error {
16 | *r = fmt.Sprintf("Hello, %s!", name)
17 | return nil
18 | }
19 |
20 | // Hello returns greeting message.
21 | func (a *App) HelloInterface(name interface{}, r *interface{}) error {
22 | *r = map[string]interface{}{
23 | "hello": name,
24 | }
25 | return nil
26 | }
27 |
28 | type Name struct {
29 | Id int `json:"id"`
30 | FirstName string `json:"firstName"`
31 | LastName string `json:"lastName"`
32 | }
33 |
34 | func (a *App) HelloStruct(name Name, r *interface{}) error {
35 | *r = map[string]Name{
36 | "hello": name,
37 | }
38 | return nil
39 | }
40 |
41 | func (a *App) HelloBytes(name []byte, r *[]byte) error {
42 | reader := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(name))
43 | *r, _ = ioutil.ReadAll(reader)
44 | return nil
45 | }
46 |
47 | func (a *App) HelloError(name interface{}, r *interface{}) error {
48 | return fmt.Errorf("%s, it is possible to return error", name)
49 | }
50 |
51 | func (a *App) HelloPanic(name interface{}, r *interface{}) (e error) {
52 | defer func() {
53 | if p := recover(); p != nil {
54 | // Recovering from panic
55 | e = fmt.Errorf("panic in go: %v", p)
56 | }
57 | }()
58 | panic("Test if we can handle panic")
59 | }
60 |
61 | func main() {
62 | if err := gotask.Register(new(App)); err != nil {
63 | log.Fatalln(err)
64 | }
65 | if err := gotask.Run(); err != nil {
66 | log.Fatalln(err)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hyperf/gotask/v2
2 |
3 | go 1.13
4 |
5 | require (
6 | github.com/fatih/pool v3.0.0+incompatible
7 | github.com/oklog/run v1.1.0
8 | github.com/pkg/errors v0.9.1
9 | github.com/spiral/goridge/v2 v2.4.4
10 | go.mongodb.org/mongo-driver v1.3.3
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/fatih/pool v3.0.0+incompatible h1:3xXzI/t5o6aEU/R+xe7ed44CTw41lV3oB0gB5pNXS5U=
6 | github.com/fatih/pool v3.0.0+incompatible/go.mod h1:v+kkrv3f2oJ1P9NHaKArMYdTVtNCwfR0DlXwnhA2L4k=
7 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
8 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
9 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
10 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
11 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
12 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
13 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
14 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
15 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
16 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
17 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
18 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
19 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
20 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
21 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
22 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
23 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
24 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
25 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
26 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
27 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
28 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
29 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
30 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
31 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
32 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
33 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
34 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
35 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
38 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
39 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
40 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
41 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
42 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
43 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
44 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
45 | github.com/klauspost/compress v1.9.5 h1:U+CaK85mrNNb4k8BNOfgJtJ/gr6kswUCFj6miSzVC6M=
46 | github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
47 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
48 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
49 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
52 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
53 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
54 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
55 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
59 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
60 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
61 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
62 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
63 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
64 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
65 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
66 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
67 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
68 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
69 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
72 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
73 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
74 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
75 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
76 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
77 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
78 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
79 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
80 | github.com/spiral/goridge/v2 v2.4.4 h1:9wV6YtvwIj8mbCLadJ1g4/Zh3YQ29alvxL5cuPWY90I=
81 | github.com/spiral/goridge/v2 v2.4.4/go.mod h1:C/EZKFPON9lypi8QO7I5ObgVmrIzTmhZqFz/tmypcGc=
82 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
83 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
84 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
85 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
86 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
87 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
88 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
89 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
90 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
91 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
92 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc h1:n+nNi93yXLkJvKwXNP9d55HC7lGK4H/SRcwB5IaUZLo=
93 | github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
94 | go.mongodb.org/mongo-driver v1.3.3 h1:9kX7WY6sU/5qBuhm5mdnNWdqaDAQKB2qSZOd5wMEPGQ=
95 | go.mongodb.org/mongo-driver v1.3.3/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
96 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
97 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
98 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
99 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
100 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
101 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
102 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
103 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
104 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
105 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
106 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
107 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
109 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
110 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
111 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
112 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
113 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
115 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
116 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
117 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
118 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
119 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
120 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
121 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
122 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
124 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
125 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
126 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
127 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
128 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
129 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 | ./tests/
14 |
15 |
--------------------------------------------------------------------------------
/pkg/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/hyperf/gotask/v2/pkg/gotask"
7 | )
8 |
9 | const phpGet = "Hyperf\\GoTask\\Wrapper\\ConfigWrapper::get"
10 | const phpSet = "Hyperf\\GoTask\\Wrapper\\ConfigWrapper::set"
11 | const phpHas = "Hyperf\\GoTask\\Wrapper\\ConfigWrapper::has"
12 |
13 | // Get retrieves a configuration from PHP, and fallback to the second parameter
14 | // if a config is missing at PHP's end.
15 | func Get(key string, fallback interface{}) (value interface{}, err error) {
16 | client, err := gotask.NewAutoClient()
17 | if err != nil {
18 | return fallback, err
19 | }
20 | err = client.Call(phpGet, key, &value)
21 | if err != nil {
22 | return value, err
23 | }
24 | if value == nil {
25 | return fallback, nil
26 | }
27 | return value, nil
28 | }
29 |
30 | // GetString returns a string config
31 | func GetString(key string, fallback string) (value string, err error) {
32 | untyped, err := Get(key, fallback)
33 | if err != nil {
34 | return fallback, err
35 | }
36 | if typed, ok := untyped.(string); ok {
37 | return typed, nil
38 | }
39 | return fallback, fmt.Errorf("config %s expected to be string, got %+v instead", key, untyped)
40 | }
41 |
42 | // GetInt returns a int config
43 | func GetInt(key string, fallback int) (value int, err error) {
44 | untyped, err := Get(key, fallback)
45 | if err != nil {
46 | return fallback, err
47 | }
48 | if typed, ok := untyped.(int); ok {
49 | return typed, nil
50 | }
51 | return fallback, fmt.Errorf("config %s expected to be int, got %+v instead", key, untyped)
52 | }
53 |
54 | // GetFloat returns a float64 config
55 | func GetFloat(key string, fallback float64) (value float64, err error) {
56 | untyped, err := Get(key, fallback)
57 | if err != nil {
58 | return fallback, err
59 | }
60 | if typed, ok := untyped.(float64); ok {
61 | return typed, nil
62 | }
63 | return fallback, fmt.Errorf("config %s expected to be float64, got %+v instead", key, untyped)
64 | }
65 |
66 | // GetBool returns a boolean config
67 | func GetBool(key string, fallback bool) (value bool, err error) {
68 | untyped, err := Get(key, fallback)
69 | if err != nil {
70 | return fallback, err
71 | }
72 | if typed, ok := untyped.(bool); ok {
73 | return typed, nil
74 | }
75 | return fallback, fmt.Errorf("config %s expected to be bool, got %+v instead", key, untyped)
76 | }
77 |
78 | // Has checks if a configuration exists in PHP
79 | func Has(key string) (value bool, err error) {
80 | client, err := gotask.NewAutoClient()
81 | if err != nil {
82 | return false, err
83 | }
84 | err = client.Call(phpHas, key, &value)
85 | if err != nil {
86 | return value, err
87 | }
88 | return value, nil
89 | }
90 |
91 | // Set sets a configuration in PHP
92 | func Set(key string, val interface{}) (err error) {
93 | client, err := gotask.NewAutoClient()
94 | if err != nil {
95 | return nil
96 | }
97 | payload := map[string]interface{}{
98 | "key": key,
99 | "value": val,
100 | }
101 | err = client.Call(phpSet, payload, nil)
102 | if err != nil {
103 | return err
104 | }
105 | return nil
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hyperf/gotask/v2/pkg/gotask"
7 | )
8 |
9 | func testGet(t *testing.T) {
10 | testing.Init()
11 | enable, err := Get("gotask.php2go.enable", true)
12 | if err != nil {
13 | t.Errorf("Get returns err %e", err)
14 | }
15 | if enable != true {
16 | t.Errorf("enable should be true")
17 | }
18 | }
19 |
20 | func testSet(t *testing.T) {
21 | testing.Init()
22 | err := Set("gotask.non_exist", []string{"some", "value"})
23 | if err != nil {
24 | t.Errorf("Set returns err %+v", err)
25 | }
26 | val, err := Get("gotask.non_exist", []string{"other", "value"})
27 | if err != nil {
28 | t.Errorf("Get returns err %+v", err)
29 | }
30 | if _, ok := val.([]interface{}); !ok {
31 | t.Errorf("val should be slice, but got %+v", val)
32 | }
33 | s, _ := val.([]interface{})
34 | if s[0] != "some" {
35 | t.Errorf("key 1 should be some, but got %s", s[0])
36 | }
37 | if s[1] != "value" {
38 | t.Errorf("key 2 should be value, but got %s", s[1])
39 | }
40 | }
41 |
42 | func testHas(t *testing.T) {
43 | testing.Init()
44 | has, err := Has("gotask.socket_address")
45 | if err != nil {
46 | t.Errorf("Set returns err %e", err)
47 | }
48 | if has != true {
49 | t.Errorf("expect true, got %v", has)
50 | }
51 | val, err := Has("gotask.no_no")
52 | if val == true {
53 | t.Errorf("expect false, got %v", has)
54 | }
55 | }
56 |
57 | func TestAll(t *testing.T) {
58 | gotask.SetGo2PHPAddress("../../tests/test.sock")
59 | for i := 0; i < 50; i++ {
60 | t.Run("testAll", func(t *testing.T) {
61 | t.Parallel()
62 | testHas(t)
63 | testGet(t)
64 | testSet(t)
65 | })
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/gotask/client.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "github.com/fatih/pool"
5 | "github.com/pkg/errors"
6 | "github.com/spiral/goridge/v2"
7 | "net"
8 | "net/rpc"
9 | "strings"
10 | )
11 |
12 | type Pool struct {
13 | pool.Pool
14 | }
15 |
16 | var globalPool *Pool
17 |
18 | //SetGo2PHPAddress sets the go2php server socket address
19 | func SetGo2PHPAddress(address string) {
20 | *go2phpAddress = address
21 | }
22 |
23 | //GetGo2PHPAddress retrieves the go2php server socket address
24 | func GetGo2PHPAddress() string {
25 | return *go2phpAddress
26 | }
27 |
28 | //NewAutoPool creates a connection pool using pre-defined addresses
29 | func NewAutoPool() (*Pool, error) {
30 | addresses := strings.Split(*go2phpAddress, ",")
31 | return NewPool(addresses)
32 | }
33 |
34 | //NewPool creates a connection pool
35 | func NewPool(addresses []string) (*Pool, error) {
36 | index := 0
37 | factory := func() (net.Conn, error) {
38 | return net.Dial(parseAddr(addresses[index%len(addresses)]))
39 | }
40 | p, err := pool.NewChannelPool(5, 30, factory)
41 | if err != nil {
42 | return nil, errors.Wrap(err, "Failed to create connection pool")
43 | }
44 | return &Pool{
45 | Pool: p,
46 | }, nil
47 | }
48 |
49 | // Client represents a client for go2php IPC.
50 | type Client struct {
51 | *rpc.Client
52 | }
53 |
54 | // NewAutoClient creates a client connected to predefined connection pool.
55 | func NewAutoClient() (c *Client, err error) {
56 | if globalPool == nil {
57 | globalPool, err = NewAutoPool()
58 | if err != nil {
59 | return nil, errors.Wrap(err, "Connection pool not available")
60 | }
61 | }
62 | conn, err := globalPool.Get()
63 | if err != nil {
64 | return nil, errors.Wrap(err, "Failed to get a connection from connection pool")
65 | }
66 | c = NewClient(conn)
67 | return c, nil
68 | }
69 |
70 | // NewClient returns a new Client using the connection provided.
71 | func NewClient(conn net.Conn) *Client {
72 | return &Client{
73 | Client: rpc.NewClientWithCodec(goridge.NewClientCodec(conn)),
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/gotask/common.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | )
7 |
8 | type Function struct {
9 | Name string
10 | Raw bool
11 | ParamModifier string
12 | ResultModifier string
13 | }
14 | type Class struct {
15 | Name string
16 | Functions []Function
17 | }
18 |
19 | func parseAddr(addr string) (string, string) {
20 | var network string
21 | if strings.Contains(addr, ":") {
22 | network = "tcp"
23 | } else {
24 | network = "unix"
25 | }
26 | return network, addr
27 | }
28 |
29 | func reflectStruct(i interface{}) *Class {
30 | var val reflect.Type
31 | if reflect.TypeOf(i).Kind() != reflect.Ptr {
32 | val = reflect.PtrTo(reflect.TypeOf(i))
33 | } else {
34 | val = reflect.TypeOf(i)
35 | }
36 | functions := make([]Function, 0)
37 | for i := 0; i < val.NumMethod(); i++ {
38 | f := Function{
39 | Name: val.Method(i).Name,
40 | Raw: val.Method(i).Type.In(1) == reflect.TypeOf([]byte{}),
41 | ParamModifier: getModifier(val.Method(i).Type.In(1)),
42 | ResultModifier: getModifier(val.Method(i).Type.In(2).Elem()),
43 | }
44 | functions = append(functions, f)
45 | }
46 | return &Class{
47 | Name: val.Elem().Name(),
48 | Functions: functions,
49 | }
50 | }
51 |
52 | func getModifier(t reflect.Type) string {
53 | if t == reflect.TypeOf([]byte{}) {
54 | return "string"
55 | }
56 | if t.Kind() == reflect.Int {
57 | return "int"
58 | }
59 | if t.Kind() == reflect.Float64 {
60 | return "float"
61 | }
62 | if t.Kind() == reflect.Float32 {
63 | return "float"
64 | }
65 | if t.Kind() == reflect.Bool {
66 | return "bool"
67 | }
68 | return ""
69 | }
70 |
71 | // Reflect if an interface is either a struct or a pointer to a struct
72 | // and has the defined member field, if error is nil, the given
73 | // FieldName exists and is accessible with reflect.
74 | func property(i interface{}, fieldName string, fallback string) string {
75 | ValueIface := reflect.ValueOf(i)
76 |
77 | // Check if the passed interface is a pointer
78 | if ValueIface.Type().Kind() != reflect.Ptr {
79 | // Create a new type of Iface's Type, so we have a pointer to work with
80 | ValueIface = reflect.New(reflect.TypeOf(i))
81 | }
82 |
83 | // 'dereference' with Elem() and get the field by name
84 | Field := ValueIface.Elem().FieldByName(fieldName)
85 | if !Field.IsValid() || !(Field.Kind() == reflect.String) {
86 | return fallback
87 | }
88 | return Field.String()
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/gotask/common_test.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | type Mock struct{}
8 |
9 | func (m Mock) MockMethod(arg interface{}, r *interface{}) error {
10 | return nil
11 | }
12 | func (m Mock) MockMethodBytes(arg []byte, r *interface{}) error {
13 | return nil
14 | }
15 | func (m *Mock) Pointer(arg []byte, r *interface{}) error {
16 | return nil
17 | }
18 | func TestReflectStruct(t *testing.T) {
19 | m := Mock{}
20 | out := reflectStruct(m)
21 | if out.Name != "Mock" {
22 | t.Errorf("Name must be Mock, got %s", out.Name)
23 | }
24 | if out.Functions[0].Name != "MockMethod" {
25 | t.Errorf("Name must be MockMethod, got %s", out.Functions[0].Name)
26 | }
27 | if out.Functions[0].Raw != false {
28 | t.Errorf("Raw must be false, got %+v", out.Functions[0].Raw)
29 | }
30 | if out.Functions[1].Name != "MockMethodBytes" {
31 | t.Errorf("Name must be MockMethodBytes, got %s", out.Functions[1].Name)
32 | }
33 | if out.Functions[1].Raw != true {
34 | t.Errorf("Name must be true, got %+v", out.Functions[1].Raw)
35 | }
36 | if out.Functions[2].Name != "Pointer" {
37 | t.Errorf("Name must be MockMethodBytes, got %s", out.Functions[2].Name)
38 | }
39 | if out.Functions[2].Raw != true {
40 | t.Errorf("Name must be true, got %+v", out.Functions[2].Raw)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/pkg/gotask/flag.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "flag"
5 | )
6 |
7 | var (
8 | address *string
9 | standalone *bool
10 | listenOnPipe *bool
11 | go2phpAddress *string
12 | reflection *bool
13 | )
14 |
15 | func init() {
16 | standalone = flag.Bool("standalone", false, "if set, ignore parent process status")
17 | address = flag.String("address", "127.0.0.1:6001", "must be a unix socket or tcp address:port like 127.0.0.1:6001")
18 | listenOnPipe = flag.Bool("listen-on-pipe", false, "listen on stdin/stdout pipe")
19 | go2phpAddress = flag.String("go2php-address", "127.0.0.1:6002", "must be a unix socket or tcp address:port like 127.0.0.1:6002")
20 | reflection = flag.Bool("reflect", false, "instead of running the service, provide a service definition to os.stdout using reflection")
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/gotask/generator.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "path/filepath"
9 | "text/template"
10 | "unicode"
11 |
12 | "github.com/pkg/errors"
13 | )
14 |
15 | const phpBody = `= 0; i-- { // reverse
14 | next = others[i](next)
15 | }
16 | return outer(next)
17 | }
18 | }
19 |
20 | func PanicRecover() Middleware {
21 | return func(next Handler) Handler {
22 | return func(cmd interface{}, r *interface{}) (e error) {
23 | defer func() {
24 | if rec := recover(); rec != nil {
25 | e = fmt.Errorf("panic: %s", rec)
26 | }
27 | }()
28 | return next(cmd, r)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/gotask/server.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "net"
8 | "net/rpc"
9 | "os"
10 | "os/signal"
11 | "path"
12 | "syscall"
13 | "time"
14 |
15 | "github.com/oklog/run"
16 | "github.com/pkg/errors"
17 | "github.com/spiral/goridge/v2"
18 | )
19 |
20 | var g run.Group
21 |
22 | func checkProcess(pid int, quit chan bool) {
23 | if *standalone {
24 | return
25 | }
26 | process, err := os.FindProcess(int(pid))
27 | if err != nil {
28 | close(quit)
29 | return
30 | }
31 | err = process.Signal(syscall.Signal(0))
32 | if err != nil {
33 | close(quit)
34 | }
35 | }
36 |
37 | // Register a net/rpc compatible service
38 | func Register(receiver interface{}) error {
39 | if !flag.Parsed() {
40 | flag.Parse()
41 | }
42 | if !*reflection {
43 | return rpc.Register(receiver)
44 | }
45 | return generatePHP(receiver)
46 | }
47 |
48 | // Set the address of socket
49 | func SetAddress(addr string) {
50 | *address = addr
51 | }
52 |
53 | // Get the address of the socket
54 | func GetAddress() string {
55 | return *address
56 | }
57 |
58 | // Run the sidecar, receive any fatal errors.
59 | func Run() error {
60 | if !flag.Parsed() {
61 | flag.Parse()
62 | }
63 |
64 | if *reflection {
65 | return nil
66 | }
67 |
68 | if *listenOnPipe {
69 | relay := goridge.NewPipeRelay(os.Stdin, os.Stdout)
70 | codec := goridge.NewCodecWithRelay(relay)
71 | g.Add(func() error {
72 | rpc.ServeCodec(codec)
73 | return fmt.Errorf("pipe is closed")
74 | }, func(err error) {
75 | _ = os.Stdin.Close()
76 | _ = os.Stdout.Close()
77 | _ = codec.Close()
78 | })
79 | }
80 |
81 | if *address != "" {
82 | network, addr := parseAddr(*address)
83 | cleanup, err := checkAddr(network, addr)
84 | if err != nil {
85 | return errors.Wrap(err, "cannot remove existing unix socket")
86 | }
87 | defer cleanup()
88 |
89 | ln, err := net.Listen(network, addr)
90 | if err != nil {
91 | return errors.Wrap(err, "unable to listen")
92 | }
93 |
94 | g.Add(func() error {
95 | for {
96 | conn, err := ln.Accept()
97 | if err != nil {
98 | return err
99 | }
100 | go rpc.ServeCodec(goridge.NewCodec(conn))
101 | }
102 | }, func(err error) {
103 | _ = ln.Close()
104 | })
105 | }
106 |
107 | {
108 | var (
109 | termChan chan os.Signal
110 | ppid int
111 | pdeadChan chan bool
112 | ticker *time.Ticker
113 | )
114 | termChan = make(chan os.Signal)
115 | signal.Notify(termChan, os.Interrupt, os.Kill)
116 | ppid = os.Getppid()
117 | pdeadChan = make(chan bool)
118 | ticker = time.NewTicker(500 * time.Millisecond)
119 | ctx, cancel := context.WithCancel(context.Background())
120 | g.Add(func() error {
121 | for {
122 | select {
123 | case sig := <-termChan:
124 | return fmt.Errorf("received system call:%+v, shutting down\n", sig)
125 | case <-pdeadChan:
126 | return nil
127 | case <-ticker.C:
128 | checkProcess(ppid, pdeadChan)
129 | case <-ctx.Done():
130 | return ctx.Err()
131 | }
132 | }
133 | }, func(err error) {
134 | cancel()
135 | })
136 | }
137 |
138 | return g.Run()
139 | }
140 |
141 | // Add an actor (function) to the group. Each actor must be pre-emptable by an
142 | // interrupt function. That is, if interrupt is invoked, execute should return.
143 | // Also, it must be safe to call interrupt even after execute has returned.
144 | //
145 | // The first actor (function) to return interrupts all running actors.
146 | // The error is passed to the interrupt functions, and is returned by Run.
147 | func Add(execute func() error, interrupt func(error)) {
148 | g.Add(execute, interrupt)
149 | }
150 |
151 | func checkAddr(network, addr string) (func(), error) {
152 | if network != "unix" {
153 | return func() {}, nil
154 | }
155 | if _, err := os.Stat(addr); !os.IsNotExist(err) {
156 | return func() {}, os.Remove(addr)
157 | }
158 | if err := os.MkdirAll(path.Dir(addr), os.ModePerm); err != nil {
159 | return func() {}, err
160 | }
161 | if ok, err := isWritable(path.Dir(addr)); err != nil || !ok {
162 | return func() {}, errors.Wrap(err, "socket directory is not writable")
163 | }
164 | return func() { os.Remove(addr) }, nil
165 | }
166 |
167 | func isWritable(path string) (isWritable bool, err error) {
168 | info, err := os.Stat(path)
169 | if err != nil {
170 | return false, err
171 | }
172 |
173 | if !info.IsDir() {
174 | return false, fmt.Errorf("%s isn't a directory", path)
175 | }
176 |
177 | // Check if the user bit is enabled in file permission
178 | if info.Mode().Perm()&(1<<(uint(7))) == 0 {
179 | return false, fmt.Errorf("write permission bit is not set on this %s for user", path)
180 | }
181 |
182 | var stat syscall.Stat_t
183 | if err = syscall.Stat(path, &stat); err != nil {
184 | return false, err
185 | }
186 |
187 | err = nil
188 | if uint32(os.Geteuid()) != stat.Uid {
189 | return false, errors.Errorf("user doesn't have permission to write to %s", path)
190 | }
191 |
192 | return true, nil
193 | }
194 |
--------------------------------------------------------------------------------
/pkg/gotask/server_test.go:
--------------------------------------------------------------------------------
1 | package gotask
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 | "testing"
8 | )
9 |
10 | func TestClearAddr(t *testing.T) {
11 | dir, _ := ioutil.TempDir("", "")
12 | defer os.Remove(dir)
13 |
14 | if _, err := checkAddr("unix", dir+"/non-exist.sock"); err != nil {
15 | t.Errorf("checkAddr should not return error for non-exist files")
16 | }
17 | if _, err := checkAddr("tcp", "127.0.0.1:6000"); err != nil {
18 | t.Errorf("checkAddr should not return error for tcp ports")
19 | }
20 | file, err := os.Create("/tmp/temp.sock")
21 | if err != nil {
22 | log.Fatal(err)
23 | }
24 | defer file.Close()
25 | if _, err := checkAddr("unix", "/tmp/temp.sock"); err != nil {
26 | t.Errorf("checkAddr should be able to clear unix socket")
27 | }
28 | _, err = os.Stat("/tmp/temp.sock")
29 | if !os.IsNotExist(err) {
30 | t.Errorf("unix socket are not cleared, %v", err)
31 | }
32 |
33 | if _, err := checkAddr("unix", dir+"/path/to/dir/temp.sock"); err != nil {
34 | t.Errorf("checkAddr should be able to create directory if not exist")
35 | }
36 |
37 | if _, err := checkAddr("unix", "/private/temp.sock"); err == nil {
38 | t.Error("unix socket shouldn't be created")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "github.com/hyperf/gotask/v2/pkg/gotask"
5 | )
6 |
7 | const phpLog = "Hyperf\\GoTask\\Wrapper\\LoggerWrapper::log"
8 |
9 | // C is a type for passing PSR context.
10 | type C map[string]interface{}
11 |
12 | // Map returns itself
13 | func (c C) Map() map[string]interface{} {
14 | return c
15 | }
16 |
17 | // Mapper is an interface for PSR Context
18 | type Mapper interface {
19 | Map() map[string]interface{}
20 | }
21 |
22 | // Emergency logger
23 | func Emergency(message string, context Mapper) error {
24 | client, err := gotask.NewAutoClient()
25 | if err != nil {
26 | return err
27 | }
28 | payload := map[string]interface{}{
29 | "level": "emergency",
30 | "context": context.Map(),
31 | "message": message,
32 | }
33 | err = client.Call(phpLog, payload, nil)
34 | return err
35 | }
36 |
37 | // Alert logger
38 | func Alert(message string, context Mapper) error {
39 | client, err := gotask.NewAutoClient()
40 | if err != nil {
41 | return err
42 | }
43 | payload := map[string]interface{}{
44 | "level": "alert",
45 | "context": context.Map(),
46 | "message": message,
47 | }
48 | err = client.Call(phpLog, payload, nil)
49 | return err
50 | }
51 |
52 | // Critical logger
53 | func Critical(message string, context Mapper) error {
54 | client, err := gotask.NewAutoClient()
55 | if err != nil {
56 | return err
57 | }
58 | payload := map[string]interface{}{
59 | "level": "critical",
60 | "context": context.Map(),
61 | "message": message,
62 | }
63 | err = client.Call(phpLog, payload, nil)
64 | return err
65 | }
66 |
67 | // Error logger
68 | func Error(message string, context Mapper) error {
69 | client, err := gotask.NewAutoClient()
70 | if err != nil {
71 | return err
72 | }
73 | payload := map[string]interface{}{
74 | "level": "error",
75 | "context": context.Map(),
76 | "message": message,
77 | }
78 | err = client.Call(phpLog, payload, nil)
79 | return err
80 | }
81 |
82 | // Warning logger
83 | func Warning(message string, context Mapper) error {
84 | client, err := gotask.NewAutoClient()
85 | if err != nil {
86 | return err
87 | }
88 | payload := map[string]interface{}{
89 | "level": "warning",
90 | "context": context.Map(),
91 | "message": message,
92 | }
93 | err = client.Call(phpLog, payload, nil)
94 | return err
95 | }
96 |
97 | // Notice logger
98 | func Notice(message string, context Mapper) error {
99 | client, err := gotask.NewAutoClient()
100 | if err != nil {
101 | return err
102 | }
103 | payload := map[string]interface{}{
104 | "level": "notice",
105 | "context": context.Map(),
106 | "message": message,
107 | }
108 | err = client.Call(phpLog, payload, nil)
109 | return err
110 | }
111 |
112 | // Info logger
113 | func Info(message string, context Mapper) error {
114 | client, err := gotask.NewAutoClient()
115 | if err != nil {
116 | return err
117 | }
118 | payload := map[string]interface{}{
119 | "level": "info",
120 | "context": context.Map(),
121 | "message": message,
122 | }
123 | err = client.Call(phpLog, payload, nil)
124 | return err
125 | }
126 |
127 | // Debug logger
128 | func Debug(message string, context Mapper) error {
129 | client, err := gotask.NewAutoClient()
130 | if err != nil {
131 | return err
132 | }
133 | payload := map[string]interface{}{
134 | "level": "debug",
135 | "context": context.Map(),
136 | "message": message,
137 | }
138 | err = client.Call(phpLog, payload, nil)
139 | return err
140 | }
141 |
142 | // Log logs a message at any given level
143 | func Log(level string, message string, context Mapper) error {
144 | client, err := gotask.NewAutoClient()
145 | if err != nil {
146 | return err
147 | }
148 | payload := map[string]interface{}{
149 | "level": level,
150 | "context": context.Map(),
151 | "message": message,
152 | }
153 | err = client.Call(phpLog, payload, nil)
154 | return err
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/log/log_test.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/hyperf/gotask/v2/pkg/gotask"
7 | )
8 |
9 | func testInfo(t *testing.T) {
10 | err := Info("hello", C{
11 | "Some": "Value",
12 | })
13 | if err != nil {
14 | t.Errorf("level Info log should be successful, got %+v", err)
15 | }
16 | }
17 |
18 | func TestAll(t *testing.T) {
19 | t.Parallel()
20 | gotask.SetGo2PHPAddress("../../tests/test.sock")
21 | for i := 0; i < 50; i++ {
22 | t.Run("testAll", func(t *testing.T) {
23 | t.Parallel()
24 | testInfo(t)
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/mongo_client/bulk_write_model_factory.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "go.mongodb.org/mongo-driver/bson"
5 | "go.mongodb.org/mongo-driver/mongo"
6 | "go.mongodb.org/mongo-driver/mongo/options"
7 | )
8 |
9 | func parseModels(arg []map[string][]bson.Raw) []mongo.WriteModel {
10 | var models = make([]mongo.WriteModel, 0, len(arg))
11 | for _, v := range arg {
12 | for kk, vv := range v {
13 | models = append(models, makeModel(kk, vv))
14 | }
15 | }
16 | return models
17 | }
18 |
19 | func makeModel(k string, v []bson.Raw) mongo.WriteModel {
20 | switch k {
21 | case "insertOne":
22 | m := mongo.NewInsertOneModel()
23 | if len(v) == 0 {
24 | return m
25 | }
26 | m.SetDocument(v[0])
27 | return m
28 | case "updateOne":
29 | m := mongo.NewUpdateOneModel()
30 | if len(v) == 0 {
31 | return m
32 | }
33 | m.SetFilter(v[0])
34 | if len(v) == 1 {
35 | return m
36 | }
37 | m.SetUpdate(v[1])
38 | if len(v) == 2 {
39 | return m
40 | }
41 | o := getOptions(v[2])
42 | m.SetUpsert(o.Upsert)
43 | if o.Collation == nil {
44 | return m
45 | }
46 | m.SetCollation(o.Collation)
47 | return m
48 | case "updateMany":
49 | m := mongo.NewUpdateManyModel()
50 | if len(v) == 0 {
51 | return m
52 | }
53 | m.SetFilter(v[0])
54 | if len(v) == 1 {
55 | return m
56 | }
57 | m.SetUpdate(v[1])
58 | if len(v) == 2 {
59 | return m
60 | }
61 | o := getOptions(v[2])
62 | m.SetUpsert(o.Upsert)
63 | if o.Collation == nil {
64 | return m
65 | }
66 | m.SetCollation(o.Collation)
67 | return m
68 | case "replaceOne":
69 | m := mongo.NewReplaceOneModel()
70 | if len(v) == 0 {
71 | return m
72 | }
73 | m.SetFilter(v[0])
74 | if len(v) == 1 {
75 | return m
76 | }
77 | m.SetReplacement(v[1])
78 | if len(v) == 2 {
79 | return m
80 | }
81 | o := getOptions(v[2])
82 | m.SetUpsert(o.Upsert)
83 | if o.Collation == nil {
84 | return m
85 | }
86 | m.SetCollation(o.Collation)
87 | return m
88 | case "deleteOne":
89 | m := mongo.NewDeleteOneModel()
90 | if len(v) == 0 {
91 | return m
92 | }
93 | m.SetFilter(v[0])
94 | if len(v) == 1 {
95 | return m
96 | }
97 | o := getOptions(v[1])
98 | if o.Collation == nil {
99 | return m
100 | }
101 | m.SetCollation(o.Collation)
102 | return m
103 | case "deleteMany":
104 | m := mongo.NewDeleteManyModel()
105 | if len(v) == 0 {
106 | return m
107 | }
108 | m.SetFilter(v[0])
109 | if len(v) == 1 {
110 | return m
111 | }
112 | o := getOptions(v[1])
113 | if o.Collation == nil {
114 | return m
115 | }
116 | m.SetCollation(o.Collation)
117 | return m
118 | default:
119 | return nil
120 | }
121 | }
122 |
123 | type option struct {
124 | Collation *options.Collation `bson:"collation"`
125 | Upsert bool `bson:"upsert"`
126 | ArrayFilters options.ArrayFilters `bson:"arrayFilters"`
127 | }
128 |
129 | func getOptions(v bson.Raw) *option {
130 | var o option
131 | err := bson.Unmarshal(v, &o)
132 | if err != nil {
133 | panic(err)
134 | }
135 | return &o
136 | }
137 |
--------------------------------------------------------------------------------
/pkg/mongo_client/config.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "time"
7 | )
8 |
9 | type Config struct {
10 | Uri string
11 | ConnectTimeout time.Duration
12 | ReadWriteTimeout time.Duration
13 | }
14 |
15 | var (
16 | globalMongoUri *string
17 | globalMongoConnectTimeout *time.Duration
18 | globalMongoReadWriteTimeout *time.Duration
19 | )
20 |
21 | func getTimeout(env string, fallback time.Duration) (result time.Duration) {
22 | env, ok := os.LookupEnv(env)
23 | if !ok {
24 | return fallback
25 | }
26 | result, err := time.ParseDuration(env)
27 | if err != nil {
28 | return fallback
29 | }
30 | return result
31 | }
32 |
33 | // LoadConfig loads Configurations from environmental variables or config file in PHP.
34 | // Environmental variables takes priority.
35 | func LoadConfig() Config {
36 | if !flag.Parsed() {
37 | flag.Parse()
38 | }
39 | return Config{
40 | *globalMongoUri,
41 | *globalMongoConnectTimeout,
42 | *globalMongoReadWriteTimeout,
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pkg/mongo_client/config_test.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "os"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestGetTimeout(t *testing.T) {
10 | t.Parallel()
11 | _ = os.Setenv("SOMEENV", "20s")
12 |
13 | cases := [][]interface{}{
14 | {"SOMEENV", 20 * time.Second},
15 | {"NONEXIST", time.Second},
16 | }
17 |
18 | for _, tt := range cases {
19 | tt := tt
20 | t.Run(tt[0].(string), func(t *testing.T) {
21 | t.Parallel()
22 | s := getTimeout(tt[0].(string), time.Second)
23 | if s != tt[1] {
24 | t.Errorf("got %q, want %q", s, tt[1])
25 | }
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/mongo_client/flag.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "time"
7 | )
8 |
9 | func init() {
10 | parseConfig()
11 | }
12 |
13 | func parseConfig() {
14 | uri, ok := os.LookupEnv("MONGODB_URI")
15 | if !ok {
16 | uri = "mongodb://127.0.0.1:27017"
17 | }
18 | ct := getTimeout("MONGODB_CONNECT_TIMEOUT", 3*time.Second)
19 | rwt := getTimeout("MONGODB_READ_WRITE_TIMEOUT", time.Minute)
20 |
21 | globalMongoUri = flag.String("mongodb-uri", uri, "the default mongodb uri")
22 | globalMongoConnectTimeout = flag.Duration("mongodb-connect-timeout", ct, "mongodb connect timeout")
23 | globalMongoReadWriteTimeout = flag.Duration("mongodb-read-write-timeout", rwt, "mongodb read write timeout")
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/mongo_client/middleware.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "github.com/hyperf/gotask/v2/pkg/gotask"
7 | "github.com/pkg/errors"
8 | "go.mongodb.org/mongo-driver/bson"
9 | "go.mongodb.org/mongo-driver/mongo"
10 | )
11 |
12 | // BsonDeserialize deserializes bson cmd into a struct cmd
13 | func BsonDeserialize(ex interface{}) gotask.Middleware {
14 | return func(next gotask.Handler) gotask.Handler {
15 | return func(cmd interface{}, r *interface{}) error {
16 | b, ok := cmd.([]byte)
17 | if !ok {
18 | return fmt.Errorf("bsonDeserialize only accepts []byte")
19 | }
20 | e := bson.Unmarshal(b, ex)
21 | if e != nil {
22 | return errors.Wrap(e, "fails to unmarshal bson")
23 | }
24 | return next(ex, r)
25 | }
26 | }
27 | }
28 |
29 | // BsonSerialize serializes any result into a bson encoded result
30 | func BsonSerialize() gotask.Middleware {
31 | return func(next gotask.Handler) gotask.Handler {
32 | return func(cmd interface{}, r *interface{}) (e error) {
33 | defer func() {
34 | if e != nil {
35 | *r = []byte{}
36 | return
37 | }
38 | if *r == nil {
39 | *r = []byte{}
40 | return
41 | }
42 | switch (*r).(type) {
43 | case int64:
44 | b := make([]byte, 8)
45 | binary.LittleEndian.PutUint64(b, uint64((*r).(int64)))
46 | *r = b
47 | return
48 | case string:
49 | *r = []byte((*r).(string))
50 | return
51 | default:
52 | _, *r, e = bson.MarshalValue(r)
53 | if e != nil {
54 | e = errors.Wrap(e, "unable to serialize bson")
55 | }
56 | }
57 |
58 | }()
59 | return next(cmd, r)
60 | }
61 | }
62 | }
63 |
64 | func ErrorFilter() gotask.Middleware {
65 | return func(next gotask.Handler) gotask.Handler {
66 | return func(cmd interface{}, r *interface{}) (e error) {
67 | defer func() {
68 | if e == mongo.ErrNilCursor || e == mongo.ErrNilDocument {
69 | e = nil
70 | }
71 | e = errors.Wrap(e, "error while executing mongo command")
72 | }()
73 | return next(cmd, r)
74 | }
75 | }
76 | }
77 |
78 | func stackMiddleware(ex interface{}) gotask.Middleware {
79 | return gotask.Chain(
80 | gotask.PanicRecover(),
81 | BsonDeserialize(ex),
82 | BsonSerialize(),
83 | ErrorFilter(),
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/mongo_client/middleware_test.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "encoding/json"
5 | "go.mongodb.org/mongo-driver/bson"
6 | "testing"
7 | )
8 |
9 | func TestBsonDeserialize(t *testing.T) {
10 | cases := []struct {
11 | name string
12 | cases bson.D
13 | }{
14 | {"kv", bson.D{{"hello", "world"}}},
15 | {"number", bson.D{{"bar", 1}}},
16 | {"slice", bson.D{{"hello", []string{"value", "test"}}}},
17 | {"nil", bson.D{{"foo", nil}}},
18 | {"multiple", bson.D{
19 | {"hello", "world"},
20 | {"hello2", "world2"},
21 | }},
22 | }
23 |
24 | for _, c := range cases {
25 | c := c
26 | t.Run(c.name, func(t *testing.T) {
27 | t.Parallel()
28 | b, _ := bson.Marshal(c.cases)
29 | p := &bson.D{}
30 | m := BsonDeserialize(p)
31 | h := m(func(cmd interface{}, result *interface{}) error {
32 | a, _ := json.Marshal((*p)[0])
33 | b, _ := json.Marshal(c.cases[0])
34 | if string(a) != string(b) {
35 | t.Errorf("cmd is not equal, want %q, got %q", c.cases, cmd)
36 | }
37 | return nil
38 | })
39 | _ = h(b, nil)
40 | })
41 | }
42 | }
43 |
44 | func TestBsonSerialize(t *testing.T) {
45 | cases := []struct {
46 | name string
47 | cases bson.D
48 | }{
49 | {"kv", bson.D{{"hello", "world"}}},
50 | {"number", bson.D{{"bar", 1}}},
51 | {"slice", bson.D{{"hello", []string{"value", "test"}}}},
52 | {"nil", bson.D{{"foo", nil}}},
53 | {"multiple", bson.D{
54 | {"hello", "world"},
55 | {"hello2", "world2"},
56 | }},
57 | }
58 |
59 | for _, c := range cases {
60 | c := c
61 | t.Run(c.name, func(t *testing.T) {
62 | t.Parallel()
63 | b, _ := bson.Marshal(c.cases)
64 | m := BsonSerialize()
65 | h := m(func(cmd interface{}, result *interface{}) error {
66 | *result = c.cases
67 | return nil
68 | })
69 | var result interface{}
70 | _ = h(nil, &result)
71 | b, ok := result.([]byte)
72 | if !ok {
73 | t.Errorf("result should be of type byte")
74 | }
75 | by, _ := bson.Marshal(c.cases)
76 | if string(by) != string(b) {
77 | t.Errorf("want %b, got %b", b, by)
78 | }
79 |
80 | })
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/mongo_client/mongo_client.go:
--------------------------------------------------------------------------------
1 | package mongo_client
2 |
3 | import (
4 | "context"
5 | "github.com/hyperf/gotask/v2/pkg/gotask"
6 | "time"
7 |
8 | "go.mongodb.org/mongo-driver/bson"
9 | "go.mongodb.org/mongo-driver/mongo"
10 | "go.mongodb.org/mongo-driver/mongo/options"
11 | )
12 |
13 | type MongoProxy struct {
14 | timeout time.Duration
15 | client *mongo.Client
16 | }
17 |
18 | // NewMongoProxy creates a new Mongo Proxy
19 | func NewMongoProxy(client *mongo.Client) *MongoProxy {
20 | return &MongoProxy{
21 | 5 * time.Second,
22 | client,
23 | }
24 | }
25 |
26 | // NewMongoProxyWithTimeout creates a new Mongo Proxy, with a read write timeout.
27 | func NewMongoProxyWithTimeout(client *mongo.Client, timeout time.Duration) *MongoProxy {
28 | return &MongoProxy{
29 | timeout,
30 | client,
31 | }
32 | }
33 |
34 | func (m *MongoProxy) getHandler(i interface{}, f coreExecutor) gotask.Handler {
35 | mw := stackMiddleware(i)
36 | return mw(func(cmd interface{}, result *interface{}) error {
37 | ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
38 | defer cancel()
39 | return f(ctx, result)
40 | })
41 | }
42 |
43 | func (m *MongoProxy) exec(i interface{}, payload []byte, result *[]byte, f coreExecutor) error {
44 | var r interface{}
45 | defer func() {
46 | if r == nil {
47 | *result = nil
48 | } else {
49 | *result = r.([]byte)
50 | }
51 | }()
52 | return m.getHandler(i, f)(payload, &r)
53 | }
54 |
55 | type coreExecutor func(ctx context.Context, r *interface{}) error
56 |
57 | type InsertOneCmd struct {
58 | Database string
59 | Collection string
60 | Record bson.Raw
61 | Opts *options.InsertOneOptions
62 | }
63 |
64 | // InsertOne executes an insert command to insert a single document into the collection.
65 | func (m *MongoProxy) InsertOne(payload []byte, result *[]byte) (err error) {
66 | cmd := &InsertOneCmd{}
67 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) error {
68 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
69 | *r, err = collection.InsertOne(ctx, cmd.Record, cmd.Opts)
70 | return err
71 | })
72 | }
73 |
74 | type InsertManyCmd struct {
75 | Database string
76 | Collection string
77 | Records []interface{}
78 | Opts *options.InsertManyOptions
79 | }
80 |
81 | // InsertMany executes an insert command to insert multiple documents into the collection. If write errors occur
82 | // during the operation (e.g. duplicate key error), this method returns a BulkWriteException error.
83 | func (m *MongoProxy) InsertMany(payload []byte, result *[]byte) (err error) {
84 | cmd := &InsertManyCmd{}
85 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) error {
86 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
87 | *r, err = collection.InsertMany(ctx, cmd.Records, cmd.Opts)
88 | return err
89 | })
90 | }
91 |
92 | type FindOneCmd struct {
93 | Database string
94 | Collection string
95 | Filter bson.Raw
96 | Opts *options.FindOneOptions
97 | }
98 |
99 | // FindOne executes a find command and returns one document in the collection.
100 | func (m *MongoProxy) FindOne(payload []byte, result *[]byte) error {
101 | cmd := &FindOneCmd{}
102 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
103 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
104 | err = collection.FindOne(ctx, cmd.Filter, cmd.Opts).Decode(r)
105 | return err
106 | })
107 | }
108 |
109 | type FindOneAndDeleteCmd struct {
110 | Database string
111 | Collection string
112 | Filter bson.Raw
113 | Opts *options.FindOneAndDeleteOptions
114 | }
115 |
116 | // FindOneAndDelete executes a findAndModify command to delete at most one document in the collection. and returns the
117 | // document as it appeared before deletion.
118 | func (m *MongoProxy) FindOneAndDelete(payload []byte, result *[]byte) error {
119 | cmd := &FindOneAndDeleteCmd{}
120 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
121 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
122 | err = collection.FindOneAndDelete(ctx, cmd.Filter, cmd.Opts).Decode(r)
123 | return err
124 | })
125 | }
126 |
127 | type FindOneAndUpdateCmd struct {
128 | Database string
129 | Collection string
130 | Filter bson.Raw
131 | Update bson.Raw
132 | Opts *options.FindOneAndUpdateOptions
133 | }
134 |
135 | // FindOneAndUpdate executes a findAndModify command to update at most one document in the collection and returns the
136 | // document as it appeared before updating.
137 | func (m *MongoProxy) FindOneAndUpdate(payload []byte, result *[]byte) error {
138 | cmd := &FindOneAndUpdateCmd{}
139 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
140 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
141 | err = collection.FindOneAndUpdate(ctx, cmd.Filter, cmd.Update, cmd.Opts).Decode(r)
142 | return err
143 | })
144 | }
145 |
146 | type FindOneAndReplaceCmd struct {
147 | Database string
148 | Collection string
149 | Filter bson.Raw
150 | Replace bson.Raw
151 | Opts *options.FindOneAndReplaceOptions
152 | }
153 |
154 | // FindOneAndReplace executes a findAndModify command to replace at most one document in the collection
155 | // and returns the document as it appeared before replacement.
156 | func (m *MongoProxy) FindOneAndReplace(payload []byte, result *[]byte) error {
157 | cmd := &FindOneAndReplaceCmd{}
158 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
159 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
160 | err = collection.FindOneAndReplace(ctx, cmd.Filter, cmd.Replace, cmd.Opts).Decode(r)
161 | return err
162 | })
163 | }
164 |
165 | type FindCmd struct {
166 | Database string
167 | Collection string
168 | Filter bson.Raw
169 | Opts *options.FindOptions
170 | }
171 |
172 | // Find executes a find command and returns all the matching documents in the collection.
173 | func (m *MongoProxy) Find(payload []byte, result *[]byte) error {
174 | cmd := &FindCmd{}
175 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) error {
176 | var rr []interface{}
177 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
178 | cursor, err := collection.Find(ctx, cmd.Filter, cmd.Opts)
179 | if cursor != nil {
180 | defer cursor.Close(ctx)
181 | err = cursor.All(ctx, &rr)
182 | }
183 | *r = rr
184 | return err
185 | })
186 | }
187 |
188 | type UpdateOneCmd struct {
189 | Database string
190 | Collection string
191 | Filter bson.Raw
192 | Update bson.Raw
193 | Opts *options.UpdateOptions
194 | }
195 |
196 | // UpdateOne executes an update command to update at most one document in the collection.
197 | func (m *MongoProxy) UpdateOne(payload []byte, result *[]byte) error {
198 | cmd := &UpdateOneCmd{}
199 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
200 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
201 | *r, err = collection.UpdateOne(ctx, cmd.Filter, cmd.Update, cmd.Opts)
202 | return err
203 | })
204 | }
205 |
206 | type UpdateManyCmd struct {
207 | Database string
208 | Collection string
209 | Filter bson.Raw
210 | Update bson.Raw
211 | Opts *options.UpdateOptions
212 | }
213 |
214 | // UpdateMany executes an update command to update documents in the collection.
215 | func (m *MongoProxy) UpdateMany(payload []byte, result *[]byte) error {
216 | cmd := &UpdateManyCmd{}
217 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
218 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
219 | *r, err = collection.UpdateMany(ctx, cmd.Filter, cmd.Update, cmd.Opts)
220 | return err
221 | })
222 | }
223 |
224 | type ReplaceOneCmd struct {
225 | Database string
226 | Collection string
227 | Filter bson.Raw
228 | Replace bson.Raw
229 | Opts *options.ReplaceOptions
230 | }
231 |
232 | // ReplaceOne executes an update command to replace at most one document in the collection.
233 | func (m *MongoProxy) ReplaceOne(payload []byte, result *[]byte) error {
234 | cmd := &ReplaceOneCmd{}
235 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
236 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
237 | *r, err = collection.ReplaceOne(ctx, cmd.Filter, cmd.Replace, cmd.Opts)
238 | return err
239 | })
240 | }
241 |
242 | type CountDocumentsCmd struct {
243 | Database string
244 | Collection string
245 | Filter bson.Raw
246 | Opts *options.CountOptions
247 | }
248 |
249 | // CountDocuments returns the number of documents in the collection.
250 | func (m *MongoProxy) CountDocuments(payload []byte, result *[]byte) error {
251 | cmd := &CountDocumentsCmd{}
252 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
253 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
254 | *r, err = collection.CountDocuments(ctx, cmd.Filter, cmd.Opts)
255 | return err
256 | })
257 | }
258 |
259 | type DeleteOneCmd struct {
260 | Database string
261 | Collection string
262 | Filter bson.Raw
263 | Opts *options.DeleteOptions
264 | }
265 |
266 | // DeleteOne executes a delete command to delete at most one document from the collection.
267 | func (m *MongoProxy) DeleteOne(payload []byte, result *[]byte) error {
268 | cmd := &DeleteOneCmd{}
269 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
270 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
271 | *r, err = collection.DeleteOne(ctx, cmd.Filter, cmd.Opts)
272 | return err
273 | })
274 | }
275 |
276 | type DeleteManyCmd struct {
277 | Database string
278 | Collection string
279 | Filter bson.Raw
280 | Opts *options.DeleteOptions
281 | }
282 |
283 | // DeleteMany executes a delete command to delete documents from the collection.
284 | func (m *MongoProxy) DeleteMany(payload []byte, result *[]byte) error {
285 | cmd := &DeleteManyCmd{}
286 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
287 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
288 | *r, err = collection.DeleteMany(ctx, cmd.Filter, cmd.Opts)
289 | return err
290 | })
291 | }
292 |
293 | type AggregateCmd struct {
294 | Database string
295 | Collection string
296 | Pipeline mongo.Pipeline
297 | Opts *options.AggregateOptions
298 | }
299 |
300 | // Aggregate executes an aggregate command against the collection and returns all the resulting documents.
301 | func (m *MongoProxy) Aggregate(payload []byte, result *[]byte) error {
302 | cmd := &AggregateCmd{}
303 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
304 | var rr []interface{}
305 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
306 | cursor, err := collection.Aggregate(ctx, cmd.Pipeline, cmd.Opts)
307 | if cursor != nil {
308 | defer cursor.Close(ctx)
309 | err = cursor.All(ctx, &rr)
310 | }
311 | *r = rr
312 | return err
313 | })
314 | }
315 |
316 | type BulkWriteCmd struct {
317 | Database string
318 | Collection string
319 | Operations []map[string][]bson.Raw
320 | Opts *options.BulkWriteOptions
321 | }
322 |
323 | func (m *MongoProxy) BulkWrite(payload []byte, result *[]byte) error {
324 | cmd := &BulkWriteCmd{}
325 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
326 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
327 | models := parseModels(cmd.Operations)
328 | *r, err = collection.BulkWrite(ctx, models, cmd.Opts)
329 | return err
330 | })
331 | }
332 |
333 | type DistinctCmd struct {
334 | Database string
335 | Collection string
336 | FieldName string
337 | Filter bson.Raw
338 | Opts *options.DistinctOptions
339 | }
340 |
341 | // Distinct executes a distinct command to find the unique values for a specified field in the collection.
342 | func (m *MongoProxy) Distinct(payload []byte, result *[]byte) error {
343 | cmd := &DistinctCmd{}
344 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
345 | var rr []interface{}
346 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
347 | rr, err = collection.Distinct(ctx, cmd.FieldName, cmd.Filter, cmd.Opts)
348 | *r = rr
349 | return err
350 | })
351 | }
352 |
353 | type CreateIndexCmd struct {
354 | Database string
355 | Collection string
356 | IndexKeys bson.Raw
357 | Opts *options.IndexOptions
358 | CreateOpts *options.CreateIndexesOptions
359 | }
360 |
361 | func (m *MongoProxy) CreateIndex(payload []byte, result *[]byte) error {
362 | cmd := &CreateIndexCmd{}
363 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
364 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
365 | model := mongo.IndexModel{
366 | Keys: cmd.IndexKeys,
367 | Options: cmd.Opts,
368 | }
369 | *r, err = collection.Indexes().CreateOne(ctx, model, cmd.CreateOpts)
370 | return err
371 | })
372 | }
373 |
374 | type CreateIndexesCmd struct {
375 | Database string
376 | Collection string
377 | Models []mongo.IndexModel
378 | Opts *options.IndexOptions
379 | CreateOpts *options.CreateIndexesOptions
380 | }
381 |
382 | func (m *MongoProxy) CreateIndexes(payload []byte, result *[]byte) error {
383 | cmd := &CreateIndexesCmd{}
384 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
385 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
386 | *r, err = collection.Indexes().CreateMany(ctx, cmd.Models, cmd.CreateOpts)
387 | return err
388 | })
389 | }
390 |
391 | type DropIndexCmd struct {
392 | Database string
393 | Collection string
394 | Name string
395 | Opts *options.DropIndexesOptions
396 | }
397 |
398 | func (m *MongoProxy) DropIndex(payload []byte, result *[]byte) error {
399 | cmd := &DropIndexCmd{}
400 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
401 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
402 | *r, err = collection.Indexes().DropOne(ctx, cmd.Name, cmd.Opts)
403 | return err
404 | })
405 | }
406 |
407 | type DropIndexesCmd struct {
408 | Database string
409 | Collection string
410 | Opts *options.DropIndexesOptions
411 | }
412 |
413 | func (m *MongoProxy) DropIndexes(payload []byte, result *[]byte) error {
414 | cmd := &DropIndexesCmd{}
415 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
416 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
417 | *r, err = collection.Indexes().DropAll(ctx, cmd.Opts)
418 | return err
419 | })
420 | }
421 |
422 | type ListIndexesCmd struct {
423 | Database string
424 | Collection string
425 | Opts *options.ListIndexesOptions
426 | }
427 |
428 | func (m *MongoProxy) ListIndexes(payload []byte, result *[]byte) error {
429 | cmd := &ListIndexesCmd{}
430 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
431 | var rr []interface{}
432 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
433 | cursor, err := collection.Indexes().List(ctx, cmd.Opts)
434 | if cursor != nil {
435 | defer cursor.Close(ctx)
436 | err = cursor.All(ctx, &rr)
437 | *r = rr
438 | }
439 | return err
440 | })
441 | }
442 |
443 | type DropCmd struct {
444 | Database string
445 | Collection string
446 | }
447 |
448 | // Drop drops the collection on the server. This method ignores "namespace not found" errors so it is safe to drop
449 | // a collection that does not exist on the server.
450 | func (m *MongoProxy) Drop(payload []byte, result *[]byte) error {
451 | cmd := &DropCmd{}
452 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
453 | collection := m.client.Database(cmd.Database).Collection(cmd.Collection)
454 | return collection.Drop(ctx)
455 | })
456 | }
457 |
458 | type Cmd struct {
459 | Database string
460 | Command bson.D
461 | Opts *options.RunCmdOptions
462 | }
463 |
464 | // RunCommand executes the given command against the database.
465 | func (m *MongoProxy) RunCommand(payload []byte, result *[]byte) error {
466 | cmd := &Cmd{}
467 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
468 | database := m.client.Database(cmd.Database)
469 | return database.RunCommand(ctx, cmd.Command, cmd.Opts).Decode(r)
470 | })
471 | }
472 |
473 | // RunCommandCursor executes the given command against the database and parses the response as a slice. If the command
474 | // being executed does not return a slice, the command will be executed on the server and an error
475 | // will be returned because the server response cannot be parsed as a slice.
476 | func (m *MongoProxy) RunCommandCursor(payload []byte, result *[]byte) error {
477 | cmd := &Cmd{}
478 | return m.exec(cmd, payload, result, func(ctx context.Context, r *interface{}) (err error) {
479 | var rr []interface{}
480 | database := m.client.Database(cmd.Database)
481 | cursor, err := database.RunCommandCursor(ctx, cmd.Command, cmd.Opts)
482 | if cursor != nil {
483 | defer cursor.Close(ctx)
484 | err = cursor.All(ctx, &rr)
485 | }
486 | *r = rr
487 | return err
488 | })
489 | }
490 |
--------------------------------------------------------------------------------
/publish/go.mod:
--------------------------------------------------------------------------------
1 | module example.com/server
2 |
3 | go 1.14
4 |
--------------------------------------------------------------------------------
/publish/gotask.php:
--------------------------------------------------------------------------------
1 | true,
14 | 'executable' => BASE_PATH . '/bin/app',
15 | 'socket_address' => \Hyperf\GoTask\ConfigProvider::address(),
16 | 'go2php' => [
17 | 'enable' => false,
18 | 'address' => \Hyperf\GoTask\ConfigProvider::address(),
19 | ],
20 | 'go_build' => [
21 | 'enable' => false,
22 | 'workdir' => BASE_PATH . '/gotask',
23 | 'command' => 'go build -o ../bin/app cmd/app.go',
24 | ],
25 | 'go_log' => [
26 | 'redirect' => true,
27 | 'level' => 'info',
28 | ],
29 | 'pool' => [
30 | 'min_connections' => 1,
31 | 'max_connections' => 30,
32 | 'connect_timeout' => 10.0,
33 | 'wait_timeout' => 30.0,
34 | 'heartbeat' => -1,
35 | 'max_idle_time' => (float) env('GOTASK_MAX_IDLE_TIME', 60),
36 | ],
37 | ];
38 |
--------------------------------------------------------------------------------
/src/Config/DomainConfig.php:
--------------------------------------------------------------------------------
1 | config->get('gotask.executable', BASE_PATH . '/app');
32 | }
33 |
34 | public function isEnabled(): bool
35 | {
36 | return $this->config->get('gotask.enable', false) || $this->config->get('gotask.enabled', false);
37 | }
38 |
39 | public function getAddress(): string
40 | {
41 | return $this->config->get('gotask.socket_address', '127.0.0.1:6001');
42 | }
43 |
44 | public function getArgs(): array
45 | {
46 | $args = $this->config->get('gotask.args', []);
47 | $argArr = ['-address', $this->getAddress()];
48 | if ($this->shouldEnableGo2Php()) {
49 | $argArr[] = '-go2php-address';
50 | $argArr[] = $this->getGo2PhpAddress();
51 | }
52 | return array_merge($argArr, $args);
53 | }
54 |
55 | public function shouldBuild(): bool
56 | {
57 | return $this->config->get('gotask.go_build.enable', false);
58 | }
59 |
60 | public function getBuildWorkdir(): string
61 | {
62 | return $this->config->get('gotask.go_build.workdir', BASE_PATH . '/gotask');
63 | }
64 |
65 | public function getBuildCommand(): string
66 | {
67 | return $this->config->get('gotask.go_build.command');
68 | }
69 |
70 | public function shouldLogRedirect(): bool
71 | {
72 | return $this->config->get('gotask.go_log.redirect', true);
73 | }
74 |
75 | public function getLogLevel(): string
76 | {
77 | return $this->config->get('gotask.go_log.level', 'info');
78 | }
79 |
80 | public function shouldEnableGo2Php(): bool
81 | {
82 | return $this->config->get('gotask.go2php.enable', false);
83 | }
84 |
85 | public function getGo2PhpAddress(): string
86 | {
87 | return $this->config->get('gotask.go2php.address', '127.0.0.1:6002');
88 | }
89 |
90 | public function getPoolOptions(): array
91 | {
92 | return $this->config->get('gotask.pool', []);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/ConfigProvider.php:
--------------------------------------------------------------------------------
1 | [
29 | GoTask::class => GoTaskFactory::class,
30 | ],
31 | 'commands' => [
32 | ],
33 | 'processes' => [
34 | GoTaskProcess::class,
35 | ],
36 | 'listeners' => [
37 | CommandListener::class,
38 | PipeLockListener::class,
39 | LogRedirectListener::class,
40 | Go2PhpListener::class,
41 | ],
42 | 'annotations' => [
43 | 'scan' => [
44 | 'paths' => [
45 | __DIR__,
46 | ],
47 | 'ignore_annotations' => [
48 | 'mixin',
49 | ],
50 | ],
51 | ],
52 | 'publish' => [
53 | [
54 | 'id' => 'config',
55 | 'description' => 'The config for gotask.',
56 | 'source' => __DIR__ . '/../publish/gotask.php',
57 | 'destination' => BASE_PATH . '/config/autoload/gotask.php',
58 | ],
59 | [
60 | 'id' => 'app',
61 | 'description' => 'The go main package template for gotask.',
62 | 'source' => __DIR__ . '/../cmd/app.go',
63 | 'destination' => BASE_PATH . '/gotask/cmd/app.go',
64 | ],
65 | [
66 | 'id' => 'gomod',
67 | 'description' => 'The go.mod for gotask.',
68 | 'source' => __DIR__ . '/../publish/go.mod',
69 | 'destination' => BASE_PATH . '/gotask/go.mod',
70 | ],
71 | ],
72 | ];
73 | }
74 |
75 | public static function address(): string
76 | {
77 | if (defined('BASE_PATH')) {
78 | $root = BASE_PATH . '/runtime';
79 | } else {
80 | $root = '/tmp';
81 | }
82 |
83 | $appName = env('APP_NAME');
84 | $socketName = $appName . '_' . uniqid();
85 | return $root . "/{$socketName}.sock";
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Exception/GoBuildException.php:
--------------------------------------------------------------------------------
1 | reconnect();
37 | }
38 |
39 | public function __call(string $name, array $arguments): mixed
40 | {
41 | try {
42 | $result = $this->connection->{$name}(...$arguments);
43 | } catch (Throwable $exception) {
44 | $result = $this->retry($name, $arguments, $exception);
45 | }
46 |
47 | return $result;
48 | }
49 |
50 | public function close(): bool
51 | {
52 | unset($this->connection);
53 | return true;
54 | }
55 |
56 | public function reconnect(): bool
57 | {
58 | $this->connection = $this->factory->make();
59 | $this->lastUseTime = microtime(true);
60 | return true;
61 | }
62 |
63 | public function getActiveConnection(): self
64 | {
65 | if ($this->check()) {
66 | return $this;
67 | }
68 |
69 | if (! $this->reconnect()) {
70 | throw new ConnectionException('Connection reconnect failed.');
71 | }
72 |
73 | return $this;
74 | }
75 |
76 | protected function retry($name, $arguments, Throwable $exception): mixed
77 | {
78 | $logger = $this->container->get(StdoutLoggerInterface::class);
79 | $logger->warning(sprintf('RemoteGoTask::__call failed, because ' . $exception->getMessage()));
80 |
81 | try {
82 | $this->reconnect();
83 | $result = $this->connection->{$name}(...$arguments);
84 | } catch (Throwable $exception) {
85 | $this->lastUseTime = 0.0;
86 | throw $exception;
87 | }
88 |
89 | return $result;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/GoTaskConnectionPool.php:
--------------------------------------------------------------------------------
1 | getPoolOptions();
28 | $this->frequency = make(Frequency::class);
29 | parent::__construct($container, $options);
30 | }
31 |
32 | public function createConnection(): ConnectionInterface
33 | {
34 | return make(GoTaskConnection::class, ['pool' => $this]);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/GoTaskFactory.php:
--------------------------------------------------------------------------------
1 | get(DomainConfig::class);
23 | if ($config->getAddress()) {
24 | return $container->get(SocketGoTask::class);
25 | }
26 | return $container->get(PipeGoTask::class);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GoTaskProxy.php:
--------------------------------------------------------------------------------
1 | call($class . '.' . $method, ...$arguments);
28 | }
29 |
30 | public function call(string $method, mixed $payload, int $flags = 0): mixed
31 | {
32 | return $this->goTask->call($method, $payload, $flags);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/IPC/IPCReceiverInterface.php:
--------------------------------------------------------------------------------
1 | handler = new RPC(
35 | new ProcessPipeRelay($process)
36 | );
37 | }
38 |
39 | public function __call(string $name, array $arguments): void
40 | {
41 | $this->handler->{$name}(...$arguments);
42 | }
43 |
44 | public function call(string $method, $payload, int $flags = 0): mixed
45 | {
46 | return $this->handler->call($method, $payload, $flags);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/IPC/SocketIPCReceiver.php:
--------------------------------------------------------------------------------
1 | address = 'unix:' . $address;
43 | $this->port = 0;
44 | } else {
45 | $this->address = $split[0];
46 | $this->port = (int) $split[1];
47 | }
48 | }
49 |
50 | public function start(): bool
51 | {
52 | if ($this->isStarted()) {
53 | return true;
54 | }
55 | $this->server = new \Swoole\Coroutine\Server($this->address, $this->port, false, true);
56 | $this->quit = false;
57 | $this->server->handle(function (Connection $conn) {
58 | $relay = new ConnectionRelay($conn);
59 | while ($this->quit !== true) {
60 | try {
61 | $body = $relay->receiveSync($headerFlags);
62 | } catch (PrefixException $e) {
63 | $relay->close();
64 | break;
65 | }
66 | if (! ($headerFlags & Relay::PAYLOAD_CONTROL)) {
67 | throw new TransportException('rpc response header is missing');
68 | }
69 |
70 | $seq = unpack('P', substr($body, -8));
71 | $method = substr($body, 0, -8);
72 | // wait for the response
73 | $body = $relay->receiveSync($bodyFlags);
74 | $payload = $this->handleBody($body, $bodyFlags);
75 | try {
76 | $response = $this->dispatch($method, $payload);
77 | $error = null;
78 | } catch (Throwable $e) {
79 | $response = null;
80 | $error = $e;
81 | }
82 | $relay->send(
83 | $method . pack('P', $seq[1]),
84 | Relay::PAYLOAD_CONTROL | Relay::PAYLOAD_RAW
85 | );
86 |
87 | if ($error !== null) {
88 | $error = $this->formatError($error);
89 | $relay->send($error, Relay::PAYLOAD_ERROR | Relay::PAYLOAD_RAW);
90 | continue;
91 | }
92 | if ($response instanceof ByteWrapper) {
93 | $relay->send($response->byte, Relay::PAYLOAD_RAW);
94 | continue;
95 | }
96 | if (is_null($response)) {
97 | $relay->send($response, Relay::PAYLOAD_NONE);
98 | continue;
99 | }
100 | $relay->send(json_encode($response), 0);
101 | }
102 | });
103 | $this->server->start();
104 | return true;
105 | }
106 |
107 | public function close(): void
108 | {
109 | if ($this->server !== null) {
110 | $this->quit = true;
111 | $this->server->shutdown();
112 | }
113 | $this->server = null;
114 | }
115 |
116 | protected function dispatch(string $method, mixed $payload): mixed
117 | {
118 | [$class, $handler] = explode('::', $method);
119 | if (ApplicationContext::hasContainer()) {
120 | $container = ApplicationContext::getContainer();
121 | $instance = $container->get($class);
122 | } else {
123 | $instance = new $class();
124 | }
125 | return $instance->{$handler}($payload);
126 | }
127 |
128 | protected function isStarted(): bool
129 | {
130 | return $this->server !== null;
131 | }
132 |
133 | /**
134 | * Handle response body.
135 | *
136 | * @throws ServiceException
137 | */
138 | protected function handleBody(string $body, int $flags): mixed
139 | {
140 | if ($flags & GoTask::PAYLOAD_ERROR && $flags & GoTask::PAYLOAD_RAW) {
141 | throw new ServiceException("error '{$body}' on '{$this->server}'");
142 | }
143 |
144 | if ($flags & GoTask::PAYLOAD_RAW) {
145 | return $body;
146 | }
147 |
148 | return json_decode($body, true);
149 | }
150 |
151 | private function formatError(Throwable $error): string
152 | {
153 | $simpleFormat = $error->getMessage() . ':' . $error->getTraceAsString();
154 | if (! ApplicationContext::hasContainer()) {
155 | return $simpleFormat;
156 | }
157 | $container = ApplicationContext::getContainer();
158 | if (! $container->has(FormatterInterface::class)) {
159 | return $simpleFormat;
160 | }
161 | return $container->get(FormatterInterface::class)->format($error);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/IPC/SocketIPCSender.php:
--------------------------------------------------------------------------------
1 | handler = new RPC(
36 | new CoroutineSocketRelay($split[0], 0, CoroutineSocketRelay::SOCK_UNIX)
37 | );
38 | return;
39 | }
40 | [$host, $port] = $split;
41 | $this->handler = new RPC(
42 | new CoroutineSocketRelay($host, (int) $port, CoroutineSocketRelay::SOCK_TCP)
43 | );
44 | }
45 |
46 | public function __call($name, $arguments): void
47 | {
48 | $this->handler->{$name}(...$arguments);
49 | }
50 |
51 | public function call(string $method, $payload, int $flags = 0): mixed
52 | {
53 | return $this->handler->call($method, $payload, $flags);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Listener/CommandListener.php:
--------------------------------------------------------------------------------
1 | config->isEnabled()) {
40 | return;
41 | }
42 | if (($event instanceof ConsoleCommandEvent) && ($event->getCommand() instanceof WithGoTask)) {
43 | $this->process = new Process(function (Process $process) {
44 | $executable = $this->config->getExecutable();
45 | $args = $this->config->getArgs();
46 | $process->exec($executable, $args);
47 | });
48 | $this->process->start();
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Listener/Go2PhpListener.php:
--------------------------------------------------------------------------------
1 | config->shouldEnableGo2Php()) {
40 | return;
41 | }
42 |
43 | if ($event instanceof BeforeHandle && ! ($event->getCommand() instanceof WithGoTask)) {
44 | return;
45 | }
46 |
47 | $addr = $this->config->getGo2PhpAddress();
48 | if ($this->isUnix($addr)) {
49 | $addrArr = explode(',', $addr);
50 | if (count($addrArr) <= $event->workerId) {
51 | return;
52 | }
53 | $addr = $addrArr[$event->workerId];
54 | }
55 |
56 | go(function () use ($addr) {
57 | $server = make(SocketIPCReceiver::class, [$addr]);
58 | $server->start();
59 | });
60 | }
61 |
62 | private function isUnix(string $addr): bool
63 | {
64 | return strpos($addr, ':') === false;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Listener/LogRedirectListener.php:
--------------------------------------------------------------------------------
1 | config = $container->get(DomainConfig::class);
34 | }
35 |
36 | public function listen(): array
37 | {
38 | return [MainWorkerStart::class];
39 | }
40 |
41 | public function process(object $event): void
42 | {
43 | if (! $this->config->shouldLogRedirect()) {
44 | return;
45 | }
46 | Coroutine::create(function () {
47 | $processes = ProcessCollector::get('gotask');
48 | if (empty($processes)) {
49 | return;
50 | }
51 | $sock = $processes[0]->exportSocket();
52 | while (true) {
53 | try {
54 | /* @var \Swoole\Coroutine\Socket $sock */
55 | $recv = $sock->recv();
56 | if ($recv === '') {
57 | throw new SocketAcceptException('Socket is closed', $sock->errCode);
58 | }
59 | if ($recv === false && $sock->errCode !== SOCKET_ETIMEDOUT) {
60 | throw new SocketAcceptException('Socket is closed', $sock->errCode);
61 | }
62 | if ($recv !== false) {
63 | $this->logOutput((string) $recv);
64 | }
65 | } catch (Throwable $exception) {
66 | $this->logThrowable($exception);
67 | if ($exception instanceof SocketAcceptException) {
68 | break;
69 | }
70 | }
71 | }
72 | });
73 | }
74 |
75 | protected function logThrowable(Throwable $throwable): void
76 | {
77 | if ($this->container->has(StdoutLoggerInterface::class) && $this->container->has(FormatterInterface::class)) {
78 | $logger = $this->container->get(StdoutLoggerInterface::class);
79 | $formatter = $this->container->get(FormatterInterface::class);
80 | $logger->error($formatter->format($throwable));
81 |
82 | if ($throwable instanceof SocketAcceptException) {
83 | $logger->critical('Socket of process is unavailable, please restart the server');
84 | }
85 | }
86 | }
87 |
88 | protected function logOutput(string $output): void
89 | {
90 | if ($this->container->has(StdoutLoggerInterface::class)) {
91 | $logger = $this->container->get(StdoutLoggerInterface::class);
92 | $level = $this->config->getLogLevel();
93 | $logger->{$level}($output);
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Listener/PipeLockListener.php:
--------------------------------------------------------------------------------
1 | container->get(PipeGoTask::class);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/MongoClient/Collection.php:
--------------------------------------------------------------------------------
1 | sanitize($document);
42 | $data = $this->mongo->insertOne($this->makePayload([
43 | 'Record' => $document,
44 | ], $opts));
45 | return toPHP($data, ['root' => InsertOneResult::class]);
46 | }
47 |
48 | public function insertMany($documents = [], array $opts = []): InsertManyResult
49 | {
50 | $documents = $this->sanitize($documents);
51 | $data = $this->mongo->insertMany($this->makePayload([
52 | 'Records' => $documents,
53 | ], $opts));
54 | return toPHP($data, ['root' => InsertManyResult::class]);
55 | }
56 |
57 | public function find($filter = [], array $opts = []): array|object
58 | {
59 | $filter = $this->sanitize($filter);
60 | $data = $this->mongo->find($this->makePayload([
61 | 'Filter' => $filter,
62 | ], $opts));
63 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
64 | return $data !== '' ? toPHP($data, $typeMap) : [];
65 | }
66 |
67 | public function findOne($filter = [], array $opts = []): array|object
68 | {
69 | $filter = $this->sanitize($filter);
70 | $data = $this->mongo->findOne($this->makePayload([
71 | 'Filter' => $filter,
72 | ], $opts));
73 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
74 | return $data !== '' ? toPHP($data, $typeMap) : [];
75 | }
76 |
77 | public function findOneAndDelete($filter = [], array $opts = []): array|object
78 | {
79 | $filter = $this->sanitize($filter);
80 | $data = $this->mongo->findOneAndDelete($this->makePayload([
81 | 'Filter' => $filter,
82 | ], $opts));
83 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
84 | return $data !== '' ? toPHP($data, $typeMap) : [];
85 | }
86 |
87 | public function findOneAndUpdate($filter = [], $update = [], array $opts = []): array|object
88 | {
89 | $filter = $this->sanitize($filter);
90 | $data = $this->mongo->findOneAndUpdate($this->makePayload([
91 | 'Filter' => $filter,
92 | 'Update' => $update,
93 | ], $opts));
94 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
95 | return $data !== '' ? toPHP($data, $typeMap) : [];
96 | }
97 |
98 | public function findOneAndReplace($filter = [], $replace = [], array $opts = []): array|object
99 | {
100 | $filter = $this->sanitize($filter);
101 | $data = $this->mongo->findOneAndReplace($this->makePayload([
102 | 'Filter' => $filter,
103 | 'Replace' => $replace,
104 | ], $opts));
105 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
106 | return $data !== '' ? toPHP($data, $typeMap) : [];
107 | }
108 |
109 | public function updateOne($filter = [], $update = [], array $opts = []): UpdateResult
110 | {
111 | $filter = $this->sanitize($filter);
112 | $update = $this->sanitize($update);
113 | $data = $this->mongo->updateOne($this->makePayload([
114 | 'Filter' => $filter,
115 | 'Update' => $update,
116 | ], $opts));
117 | return toPHP($data, ['root' => UpdateResult::class]);
118 | }
119 |
120 | public function updateMany($filter = [], $update = [], array $opts = []): UpdateResult
121 | {
122 | $filter = $this->sanitize($filter);
123 | $update = $this->sanitize($update);
124 | $data = $this->mongo->updateMany($this->makePayload([
125 | 'Filter' => $filter,
126 | 'Update' => $update,
127 | ], $opts));
128 | return toPHP($data, ['root' => UpdateResult::class]);
129 | }
130 |
131 | public function replaceOne($filter = [], $replace = [], array $opts = []): UpdateResult
132 | {
133 | $filter = $this->sanitize($filter);
134 | $replace = $this->sanitize($replace);
135 | $data = $this->mongo->replaceOne($this->makePayload([
136 | 'Filter' => $filter,
137 | 'Replace' => $replace,
138 | ], $opts));
139 | return toPHP($data, ['root' => UpdateResult::class]);
140 | }
141 |
142 | public function countDocuments($filter = [], array $opts = []): int
143 | {
144 | $filter = $this->sanitize($filter);
145 | $data = $this->mongo->countDocuments($this->makePayload([
146 | 'Filter' => $filter,
147 | ], $opts));
148 | return unpack('P', $data)[1];
149 | }
150 |
151 | public function deleteOne($filter = [], array $opts = []): DeleteResult
152 | {
153 | $filter = $this->sanitize($filter);
154 | $data = $this->mongo->deleteOne($this->makePayload([
155 | 'Filter' => $filter,
156 | ], $opts));
157 | return toPHP($data, ['root' => DeleteResult::class]);
158 | }
159 |
160 | public function deleteMany($filter = [], array $opts = []): DeleteResult
161 | {
162 | $filter = $this->sanitize($filter);
163 | $data = $this->mongo->deleteMany($this->makePayload([
164 | 'Filter' => $filter,
165 | ], $opts));
166 | return toPHP($data, ['root' => DeleteResult::class]);
167 | }
168 |
169 | public function aggregate($pipeline = [], array $opts = []): array|object
170 | {
171 | $pipeline = $this->sanitize($pipeline);
172 | $data = $this->mongo->aggregate($this->makePayload([
173 | 'Pipeline' => $pipeline,
174 | ], $opts));
175 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
176 | return $data !== '' ? toPHP($data, $typeMap) : [];
177 | }
178 |
179 | public function bulkWrite($operations = [], array $opts = []): BulkWriteResult
180 | {
181 | $operations = $this->sanitize($operations);
182 | $data = $this->mongo->bulkWrite($this->makePayload([
183 | 'Operations' => $operations,
184 | ], $opts));
185 | return toPHP($data, ['root' => BulkWriteResult::class]);
186 | }
187 |
188 | public function distinct(string $fieldName, $filter = [], array $opts = []): array|object
189 | {
190 | $filter = $this->sanitize($filter);
191 | $data = $this->mongo->distinct($this->makePayload([
192 | 'FieldName' => $fieldName,
193 | 'Filter' => $filter,
194 | ], $opts));
195 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
196 | return $data !== '' ? toPHP($data, $typeMap) : [];
197 | }
198 |
199 | public function createIndex($index = [], array $opts = []): string
200 | {
201 | $index = $this->sanitize($index);
202 | return $this->mongo->createIndex($this->makePayload([
203 | 'IndexKeys' => $index,
204 | ], $opts));
205 | }
206 |
207 | public function createIndexes($indexes = [], array $opts = []): array|object
208 | {
209 | $indexes = $this->sanitize($indexes);
210 | $data = $this->mongo->createIndexes($this->makePayload([
211 | 'Models' => $indexes,
212 | ], $opts));
213 | return $data === '' ? [] : toPHP($data, ['root' => 'array']);
214 | }
215 |
216 | public function listIndexes($indexes = [], array $opts = []): array|object
217 | {
218 | $data = $this->mongo->listIndexes($this->makePayload([], $opts));
219 | return $data === '' ? [] : toPHP($data, ['root' => 'array', 'document' => IndexInfo::class, 'fieldPaths' => ['$.key' => 'array']]);
220 | }
221 |
222 | public function dropIndex(string $name, array $opts = []): array|object
223 | {
224 | $data = $this->mongo->dropIndex($this->makePayload([
225 | 'Name' => $name,
226 | ], $opts));
227 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
228 | return $data === '' ? [] : toPHP($data, $typeMap);
229 | }
230 |
231 | public function dropIndexes(array $opts = []): array|object
232 | {
233 | $data = $this->mongo->dropIndexes($this->makePayload([
234 | ], $opts));
235 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
236 | return $data === '' ? [] : toPHP($data, $typeMap);
237 | }
238 |
239 | public function drop(): string
240 | {
241 | return $this->mongo->drop(fromPHP([
242 | 'Database' => $this->database,
243 | 'Collection' => $this->collection,
244 | ]));
245 | }
246 |
247 | private function makePayload(array $partial, array $opts): string
248 | {
249 | return fromPHP(array_merge($partial, [
250 | 'Database' => $this->database,
251 | 'Collection' => $this->collection,
252 | 'Opts' => $this->sanitizeOpts($opts),
253 | ]));
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/MongoClient/Database.php:
--------------------------------------------------------------------------------
1 | mongo, $this->config, $this->database, $collName, $this->typeMap);
35 | }
36 |
37 | public function collection(string $collName): Collection
38 | {
39 | return new Collection($this->mongo, $this->config, $this->database, $collName, $this->typeMap);
40 | }
41 |
42 | public function runCommand(array $command = [], array $opts = []): array|object|string
43 | {
44 | $payload = [
45 | 'Database' => $this->database,
46 | 'Command' => $this->sanitize($command),
47 | 'Opts' => $this->sanitizeOpts($opts),
48 | ];
49 | $result = $this->mongo->runCommand(fromPHP($payload));
50 | if ($result !== '') {
51 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
52 | return toPHP($result, $typeMap);
53 | }
54 | return '';
55 | }
56 |
57 | public function runCommandCursor(array $command = [], array $opts = []): array|object|string
58 | {
59 | $payload = [
60 | 'Database' => $this->database,
61 | 'Command' => $this->sanitize($command),
62 | 'Opts' => $this->sanitizeOpts($opts),
63 | ];
64 | $result = $this->mongo->runCommandCursor(fromPHP($payload));
65 | if ($result !== '') {
66 | $typeMap = $opts['typeMap'] ?? $this->typeMap;
67 | return toPHP($result, $typeMap);
68 | }
69 | return '';
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/MongoClient/MongoClient.php:
--------------------------------------------------------------------------------
1 | typeMap = $this->config->get('mongodb.type_map', ['document' => 'array', 'root' => 'array']);
28 | }
29 |
30 | public function __get(string $dbName): Database
31 | {
32 | return new Database($this->mongo, $this->config, $dbName, $this->typeMap);
33 | }
34 |
35 | public function database(string $dbName): Database
36 | {
37 | return new Database($this->mongo, $this->config, $dbName, $this->typeMap);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/MongoClient/MongoProxy.php:
--------------------------------------------------------------------------------
1 | sanitize($opts);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/BulkWriteResult.php:
--------------------------------------------------------------------------------
1 |
32 | */
33 | private array $upsertedIds;
34 |
35 | public function bsonUnserialize(array $data): void
36 | {
37 | $this->matchedCount = $data['matchedcount'];
38 | $this->modifiedCount = $data['modifiedcount'];
39 | $this->upsertedCount = $data['upsertedcount'];
40 | $this->deletedCount = $data['deletedcount'];
41 | $this->insertedCount = $data['insertedcount'];
42 | $this->upsertedIds = (array) $data['upsertedids'];
43 | }
44 |
45 | public function getMatchedCount(): int
46 | {
47 | return $this->matchedCount;
48 | }
49 |
50 | public function getModifiedCount(): int
51 | {
52 | return $this->modifiedCount;
53 | }
54 |
55 | public function getUpsertedCount(): int
56 | {
57 | return $this->upsertedCount;
58 | }
59 |
60 | public function getDeletedCount(): int
61 | {
62 | return $this->deletedCount;
63 | }
64 |
65 | public function getUpsertedIds(): array
66 | {
67 | return (array) $this->upsertedIds;
68 | }
69 |
70 | public function getinsertedCount(): int
71 | {
72 | return $this->insertedCount;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/DeleteResult.php:
--------------------------------------------------------------------------------
1 | n = $data['n'];
24 | }
25 |
26 | public function getDeletedCount(): int
27 | {
28 | return $this->n;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/IndexInfo.php:
--------------------------------------------------------------------------------
1 | v = $data['v'] ?? 0;
36 | $this->key = $data['key'] ?? [];
37 | $this->name = $data['name'] ?? '';
38 | $this->ns = $data['ns'] ?? '';
39 | $this->sparse = $data['sparse'] ?? false;
40 | $this->unique = $data['unique'] ?? false;
41 | $this->ttl = $data['ttl'] ?? false;
42 | }
43 |
44 | public function getKey(): array
45 | {
46 | return $this->key;
47 | }
48 |
49 | public function getVersion(): int
50 | {
51 | return $this->v;
52 | }
53 |
54 | public function getName(): string
55 | {
56 | return $this->name;
57 | }
58 |
59 | public function getNamespace(): string
60 | {
61 | return $this->ns;
62 | }
63 |
64 | public function isSparse(): bool
65 | {
66 | return $this->sparse;
67 | }
68 |
69 | public function isUnique(): bool
70 | {
71 | return $this->unique;
72 | }
73 |
74 | public function isTtl(): bool
75 | {
76 | return $this->ttl;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/InsertManyResult.php:
--------------------------------------------------------------------------------
1 |
22 | */
23 | private array $insertedIDs;
24 |
25 | public function bsonUnserialize(array $data): void
26 | {
27 | $this->insertedIDs = $data['insertedids'];
28 | }
29 |
30 | /**
31 | * @return array
32 | */
33 | public function getInsertedIDs(): array
34 | {
35 | return $this->insertedIDs;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/InsertOneResult.php:
--------------------------------------------------------------------------------
1 | insertedId = $data['insertedid'];
24 | }
25 |
26 | public function getInsertedId(): ?ObjectId
27 | {
28 | return $this->insertedId;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/MongoClient/Type/UpdateResult.php:
--------------------------------------------------------------------------------
1 | matchedCount = $data['matchedcount'];
31 | $this->modifiedCount = $data['modifiedcount'];
32 | $this->upsertedCount = $data['upsertedcount'];
33 | $this->upsertedId = $data['upsertedid'];
34 | }
35 |
36 | public function getUpsertedId(): ObjectId|string|null
37 | {
38 | return $this->upsertedId;
39 | }
40 |
41 | public function getUpsertedCount(): int
42 | {
43 | return $this->upsertedCount;
44 | }
45 |
46 | public function getModifiedCount(): int
47 | {
48 | return $this->modifiedCount;
49 | }
50 |
51 | public function getMatchedCount(): int
52 | {
53 | return $this->matchedCount;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/PipeGoTask.php:
--------------------------------------------------------------------------------
1 | lock = new Lock();
42 | }
43 |
44 | public function call(string $method, mixed $payload, int $flags = 0): mixed
45 | {
46 | if ($this->taskChannel == null) {
47 | $this->taskChannel = new Channel(100);
48 | go(function () {
49 | $this->start();
50 | });
51 | }
52 | $returnChannel = new Channel(1);
53 | $this->taskChannel->push([$method, $payload, $flags, $returnChannel]);
54 | $result = $returnChannel->pop();
55 | if ($result instanceof Throwable) {
56 | throw $result;
57 | }
58 | return $result;
59 | }
60 |
61 | private function start(): void
62 | {
63 | if ($this->process == null) {
64 | $processName = $this->config->getProcessName();
65 | $this->process = ProcessCollector::get($processName)[0];
66 | }
67 | $task = make(PipeIPCSender::class, ['process' => $this->process]);
68 | while (true) {
69 | [$method, $payload, $flag, $returnChannel] = $this->taskChannel->pop();
70 | // check if channel is closed
71 | if ($method === null) {
72 | break;
73 | }
74 | $this->lock->lock();
75 | try {
76 | $result = $task->call($method, $payload, $flag);
77 | $returnChannel->push($result);
78 | } catch (Throwable $e) {
79 | if (! $returnChannel instanceof Channel) {
80 | throw $e;
81 | }
82 | $returnChannel->push($e);
83 | } finally {
84 | $this->lock->unlock();
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Process/GoTaskProcess.php:
--------------------------------------------------------------------------------
1 | config = $container->get(DomainConfig::class);
33 | $this->redirectStdinStdout = $this->config->shouldLogRedirect();
34 | $this->name = $this->config->getProcessName();
35 | }
36 |
37 | public function isEnable($server): bool
38 | {
39 | return $this->config->isEnabled();
40 | }
41 |
42 | public function bind($server): void
43 | {
44 | if ($this->config->shouldBuild()) {
45 | chdir($this->config->getBuildWorkdir());
46 | exec($this->config->getBuildCommand(), $output, $rev);
47 | if ($rev !== 0) {
48 | throw new GoBuildException(sprintf(
49 | 'Cannot build go files with command %s: %s',
50 | $this->config->getBuildCommand(),
51 | implode(PHP_EOL, $output)
52 | ));
53 | }
54 | }
55 | parent::bind($server);
56 | }
57 |
58 | public function handle(): void
59 | {
60 | $executable = $this->config->getExecutable();
61 | $args = $this->config->getArgs();
62 | $this->process->exec($executable, $args);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Relay/ConnectionRelay.php:
--------------------------------------------------------------------------------
1 | isConnected()) {
41 | return true;
42 | }
43 |
44 | $this->socket = $this->conn->exportSocket();
45 | return true;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Relay/CoroutineSocketRelay.php:
--------------------------------------------------------------------------------
1 | type == self::SOCK_TCP) {
85 | return "tcp://{$this->address}:{$this->port}";
86 | }
87 |
88 | return "unix://{$this->address}";
89 | }
90 |
91 | public function getAddress(): string
92 | {
93 | return $this->address;
94 | }
95 |
96 | public function getPort(): ?int
97 | {
98 | return $this->port;
99 | }
100 |
101 | public function getType(): int
102 | {
103 | return $this->type;
104 | }
105 |
106 | /**
107 | * Ensure socket connection. Returns true if socket successfully connected
108 | * or have already been connected.
109 | *
110 | * @throws RelayException
111 | * @throws Error when sockets are used in unsupported environment
112 | */
113 | public function connect(): bool
114 | {
115 | if ($this->isConnected()) {
116 | return true;
117 | }
118 |
119 | $this->socket = $this->createSocket();
120 | try {
121 | \Hyperf\Support\retry(20, function(): void {
122 | // Port type needs to be int, so we convert null to 0
123 | if ($this->socket->connect($this->address, $this->port ?? 0) === false) {
124 | throw new RelayException(sprintf('%s (%s)', $this->socket->errMsg, $this->socket->errCode));
125 | }
126 | }, 100);
127 | } catch (Exception $e) {
128 | throw new RelayException("unable to establish connection (20x) {$this}: {$e->getMessage()}", 0, $e);
129 | }
130 |
131 | return true;
132 | }
133 |
134 | /**
135 | * @throws GoridgeException
136 | */
137 | private function createSocket(): Socket
138 | {
139 | if ($this->type === self::SOCK_UNIX) {
140 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
141 | throw new GoridgeException("socket {$this} unavailable on Windows");
142 | }
143 | return new Socket(AF_UNIX, SOCK_STREAM, 0);
144 | }
145 |
146 | return new Socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Relay/ProcessPipeRelay.php:
--------------------------------------------------------------------------------
1 | socket->fd;
47 | }
48 |
49 | /**
50 | * Ensure socket connection. Returns true if socket successfully connected
51 | * or have already been connected.
52 | *
53 | * @throws RelayException
54 | * @throws Error when sockets are used in unsupported environment
55 | */
56 | public function connect(): bool
57 | {
58 | if ($this->isConnected()) {
59 | return true;
60 | }
61 |
62 | $this->socket = $this->createSocket();
63 | return true;
64 | }
65 |
66 | /**
67 | * @throws GoridgeException
68 | */
69 | private function createSocket(): Socket
70 | {
71 | return $this->process->exportSocket();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Relay/RelayInterface.php:
--------------------------------------------------------------------------------
1 | isConnected()) {
27 | $this->close();
28 | }
29 | }
30 |
31 | public function send(?string $payload, int $flags = null)
32 | {
33 | $this->connect();
34 |
35 | $size = $payload === null ? 0 : strlen($payload);
36 | if ($flags & self::PAYLOAD_NONE && $size != 0) {
37 | throw new TransportException('unable to send payload with PAYLOAD_NONE flag');
38 | }
39 |
40 | $body = pack('CPJ', $flags, $size, $size);
41 |
42 | if (! ($flags & self::PAYLOAD_NONE)) {
43 | $body .= $payload;
44 | }
45 |
46 | $this->socket->send($body);
47 | return $this;
48 | }
49 |
50 | public function receiveSync(int &$flags = null): ?string
51 | {
52 | $this->connect();
53 |
54 | $prefix = $this->fetchPrefix();
55 | $flags = $prefix['flags'];
56 | $result = null;
57 |
58 | if ($prefix['size'] !== 0) {
59 | $readBytes = $prefix['size'];
60 | $buffer = null;
61 |
62 | // Add ability to write to stream in a future
63 | while ($readBytes > 0) {
64 | $buffer = $this->socket->recv(min(self::BUFFER_SIZE, $readBytes));
65 | $result .= $buffer;
66 | $readBytes -= strlen($buffer);
67 | }
68 | }
69 | return $result;
70 | }
71 |
72 | public function isConnected(): bool
73 | {
74 | return $this->socket != null;
75 | }
76 |
77 | /**
78 | * Close connection.
79 | *
80 | * @throws RelayException
81 | */
82 | public function close(): void
83 | {
84 | if (! $this->isConnected()) {
85 | throw new RelayException("unable to close socket '{$this}', socket already closed");
86 | }
87 |
88 | $this->socket->close();
89 | $this->socket = null;
90 | }
91 |
92 | /**
93 | * @return array Prefix [flag, length]
94 | * @throws PrefixException
95 | */
96 | private function fetchPrefix(): array
97 | {
98 | $prefixBody = $this->socket->recv(17);
99 | if ($prefixBody === false || strlen($prefixBody) !== 17) {
100 | throw new PrefixException(sprintf(
101 | 'unable to read prefix from socket: %s (%s)',
102 | $this->socket->errMsg,
103 | $this->socket->errCode
104 | ));
105 | }
106 |
107 | $result = unpack('Cflags/Psize/Jrevs', $prefixBody);
108 | if (! is_array($result)) {
109 | throw new PrefixException('invalid prefix');
110 | }
111 |
112 | if ($result['size'] != $result['revs']) {
113 | throw new PrefixException('invalid prefix (checksum)');
114 | }
115 |
116 | return $result;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/SocketGoTask.php:
--------------------------------------------------------------------------------
1 | getContextKey());
35 | $connection = $this->getConnection($hasContextConnection);
36 | try {
37 | $connection = $connection->getConnection();
38 | // Execute the command with the arguments.
39 | $result = $connection->call($method, $payload, $flags);
40 | } finally {
41 | // Release connection.
42 | if (! $hasContextConnection) {
43 | Context::set($this->getContextKey(), $connection);
44 | defer(function () use ($connection) {
45 | $connection->release();
46 | });
47 | }
48 | }
49 | return $result;
50 | }
51 |
52 | /**
53 | * Get a connection from coroutine context, or from redis connectio pool.
54 | */
55 | private function getConnection(mixed $hasContextConnection): GoTaskConnection
56 | {
57 | $connection = null;
58 | if ($hasContextConnection) {
59 | $connection = Context::get($this->getContextKey());
60 | }
61 | if (! $connection instanceof GoTaskConnection) {
62 | $pool = $this->pool;
63 | $connection = $pool->get();
64 | }
65 | if (! $connection instanceof GoTaskConnection) {
66 | throw new InvalidGoTaskConnectionException('The connection is not a valid RedisConnection.');
67 | }
68 | return $connection;
69 | }
70 |
71 | /**
72 | * The key to identify the connection object in coroutine context.
73 | */
74 | private function getContextKey(): string
75 | {
76 | return 'gotask.connection';
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/SocketIPCFactory.php:
--------------------------------------------------------------------------------
1 | config->getAddress();
30 | return make(SocketIPCSender::class, ['address' => $address]);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/WithGoTask.php:
--------------------------------------------------------------------------------
1 | config->get($payload, null);
27 | }
28 |
29 | public function has(string $payload): bool
30 | {
31 | return $this->config->has($payload);
32 | }
33 |
34 | public function set(string $payload): mixed
35 | {
36 | $this->config->set($payload['key'], $payload['value']);
37 | return null;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Wrapper/LoggerWrapper.php:
--------------------------------------------------------------------------------
1 | logger->log($payload['level'], $payload['message'], $payload['context']);
27 | return null;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------