├── .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 | [![Build Status](https://travis-ci.org/hyperf/gotask.svg?branch=master)](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 | [![Build Status](https://travis-ci.org/hyperf/gotask.svg?branch=master)](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 | --------------------------------------------------------------------------------