├── .github └── workflows │ ├── cloud_code_scan.yml │ └── unittest.yml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── README.en.md ├── README.md ├── anthunder ├── __init__.py ├── client │ ├── __init__.py │ ├── aio_client.py │ ├── base.py │ └── client.py ├── command │ ├── __init__.py │ ├── fail_response.py │ └── heartbeat.py ├── discovery │ ├── __init__.py │ ├── local.py │ └── mosn │ │ └── __init__.py ├── exceptions.py ├── helpers │ ├── __init__.py │ ├── immutable_dict.py │ ├── request_id.py │ └── singleton.py ├── listener │ ├── __init__.py │ ├── aio_listener.py │ ├── base_listener.py │ └── sock_listener.py ├── model │ ├── __init__.py │ ├── request.py │ └── service.py └── protocol │ ├── __init__.py │ ├── _package_base.py │ ├── _request_pkg.py │ ├── _response_pkg.py │ ├── _rpc_trace_context.py │ ├── _sofa_header.py │ ├── constants.py │ └── exceptions.py ├── install-protobuf.sh ├── mysockpool ├── __init__.py ├── _wait.py ├── connection.py ├── connection_pool.py ├── exceptions.py ├── origin-license.txt ├── pool_manager.py ├── recently_used_container.py └── utils.py ├── mytracer ├── __init__.py ├── _rpc_id.py ├── _trace_id.py ├── helpers.py ├── span.py ├── span_context.py └── tracer.py ├── performance_aio.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── sync_call_demo.py └── tests ├── __init__.py ├── mysockpool_test ├── __init__.py └── test_poolman.py ├── mytracer_test ├── __init__.py ├── test_helpers.py ├── test_trace_id.py └── test_tracer.py ├── proto ├── ComplexServicePbRequest.proto ├── ComplexServicePbResult.proto ├── SampleServicePbRequest.proto ├── SampleServicePbResult.proto ├── SubServicePbRequest.proto ├── SubServicePbResult.proto ├── __init__.py └── python │ ├── SampleService.py │ └── __init__.py ├── test_aio_listener_client.py ├── test_bolt_package.py ├── test_helpers.py └── test_mesh_client.py /.github/workflows/cloud_code_scan.yml: -------------------------------------------------------------------------------- 1 | name: Alipay Cloud Devops Codescan 2 | on: 3 | pull_request_target: 4 | jobs: 5 | stc: # Code security scanning 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: codeScan 9 | uses: layotto/alipay-cloud-devops-codescan@main 10 | with: 11 | parent_uid: ${{ secrets.ALI_PID }} 12 | private_key: ${{ secrets.ALI_PK }} 13 | scan_type: stc 14 | sca: # Open source compliance scanning 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: codeScan 18 | uses: layotto/alipay-cloud-devops-codescan@main 19 | with: 20 | parent_uid: ${{ secrets.ALI_PID }} 21 | private_key: ${{ secrets.ALI_PK }} 22 | scan_type: sca 23 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: unittest 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10'] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: arduino/setup-protoc@v1 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 pytest coverage 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | pushd tests/proto && protoc --python_out=./python *.proto && popd 30 | - name: Test with unittest 31 | run: | 32 | coverage run -m unittest discover . 33 | - uses: codecov/codecov-action@v1 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.java 3 | *.pyc 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | ## 0.9 (2021-06-25) 3 | ### Feature 4 | - obtain service metadata from subscriber 5 | - support serialize type (other than protobuf) set in service metadata 6 | 7 | ## 0.8.1 (2021-05-31) 8 | ### Feature 9 | - drop support for python 2.7 10 | - support extra sofaheader in block Client class 11 | ### Bugfixes 12 | - fix compatible issues with python 3.9 13 | 14 | ## 0.7 (2020-04-10) 15 | ### Feature 16 | - migrate to modern coroutine syntax, drop support for python 3.4 17 | 18 | ## 0.6.3 (2019-10-10) 19 | ### Bugfixes 20 | - fix client socket failure 21 | 22 | ## 0.6.2 (2019-06-05) 23 | ### Bugfixes 24 | - fix infinite loop on losted connection 25 | 26 | ## 0.6.1 (2019-06-05) 27 | ### Bugfixes 28 | - fix heartbeat with mosn 29 | 30 | ## 0.6 (2019-05-31) 31 | ### Bugfixes 32 | - fix service publish with mosn integrate 33 | 34 | ## 0.5.6 (2019-03-15) 35 | ### Bugfixes 36 | - fix a infinite loop bug when parsing protocol 37 | 38 | ## 0.5.4 (2018-11-09) 39 | ### Bugfixes 40 | - fix server errors under python2.7 41 | 42 | ## 0.5.3 (2018-08-27) 43 | ### Feature 44 | - support antsharecloud parameters. 45 | 46 | ## 0.5.2 (2018-09-03) 47 | ### Bugfixes 48 | - fix various errors under python2.7 49 | 50 | ## 0.5.1 (2018-08-31) 51 | ### Bugfixes 52 | - sofa trace rpc id may contains str. 53 | 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Ant Small and Micro Financial Services Group Co., Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md README.en.md LICENSE HISTORY.md requirements.txt mysockpool/origin-license.txt 2 | 3 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # anthunder(a.k.a. sofa-bolt-python) 2 | 3 | anthunder(ant thunder) is a sofa-bolt library written in python. 4 | It supports RPC calling via 'sofa-bolt + protobuf' protocol. 5 | 6 | ## requirements 7 | 8 | - python3 >= 3.5 (aio classes needs asyncio support) 9 | - mosn >= 1.3 (to use with version >= 0.6) 10 | - mosn < 1.3 (to use with version < 0.6) 11 | 12 | ## roadmap 13 | 14 | - [x] bolt client(protobuf serialization) 15 | - [x] service discover via mosn (sofa servicemesh sidecar) 16 | - [x] bolt server(protobuf serialization) 17 | - [x] sofa-hessian(and other) serialization protocol support 18 | 19 | ## Tutorial 20 | 21 | ### As client (caller) 22 | 0. Acquire `.proto` file 23 | 1. Execute `protoc --python_out=. *.proto` to compile protobuf file, and get `_pb2.py` file. 24 | 2. Import protobuf classes (postfixed with `_pb2`) 25 | 26 | ```python 27 | from SampleServicePbResult_pb2 import SampleServicePbResult 28 | from SampleServicePbRequest_pb2 import SampleServicePbRequest 29 | 30 | from anthunder import AioClient 31 | from anthunder.discovery.mosn import MosnClient, ApplicationInfo 32 | 33 | 34 | spanctx = SpanContext() # generate a new context, an object of mytracer.SpanContext, stores rpc_trace_context. 35 | # spanctx = ctx # or transfered from upstream rpc 36 | service_reg = MosnClient() # using mosn for service discovery, see https://mosn.io for detail 37 | service_reg.startup(ApplicationInfo(YOUR_APP_NAME)) 38 | # service_reg = LocalRegistry({interface: (inf_ip, inf_port)}) # or a service-address dict as service discovery 39 | 40 | # Subscribe interface before client requests 41 | service_reg.subscribe(interface) 42 | 43 | client = AioClient(YOUR_APP_NAME, service_register=service_reg) 44 | # will create a thread, and send heartbeat to remote every 30s 45 | 46 | interface = 'com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0' 47 | 48 | 49 | # Call synchronously 50 | content = client.invoke_sync(interface, "hello", 51 | SampleServicePbRequest(name=some_name).SerializeToString(), 52 | timeout_ms=500, spanctx=spanctx) 53 | result = SampleServicePbResult() 54 | result.ParseFromString(content) 55 | 56 | # Call asynchronously 57 | 58 | def client_callback(resp): 59 | # callback function, accepts bytes as the only argument, 60 | # then do deserialize and further processes 61 | result = SampleServicePbResult() 62 | result.ParseFromString(content) 63 | # do something 64 | 65 | future = client.invoke_async(interface, "hello", 66 | SampleServicePbRequest(name=some_name).SerializeToString(), 67 | spanctx=spanctx, callback=client_callback) 68 | ) 69 | 70 | ``` 71 | 72 | See project's unittest for runnable demo 73 | 74 | ### As server 75 | 76 | ```python 77 | from anthunder.listener import aio_listener 78 | from anthunder.discovery.mosn import MosnClient, ApplicationInfo 79 | 80 | 81 | class SampleService(object): 82 | def __init__(self, ctx): 83 | # service must accept one param as spanctx for rpc tracing support 84 | self.ctx = ctx 85 | 86 | def hello(self, bs: bytes): 87 | obj = SampleServicePbRequest() 88 | obj.ParseFromString(bs) 89 | print("Processing Request", obj) 90 | return SampleServicePbResult(result=obj.name).SerializeToString() 91 | 92 | 93 | interface = 'com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0' 94 | 95 | service_reg = MosnClient() # using mosn for service discovery, see https://mosn.io for detail 96 | service_reg.startup(ApplicationInfo(YOUR_APP_NAME)) 97 | listener = aio_listener.AioListener(('127.0.0.1', 12200), YOUR_APP_NAME, service_register=service_reg) 98 | # register interface and its function, plus its protobuf definition class 99 | listener.register_interface(interface, service_cls=SampleService, provider_meta=ProviderMetaInfo(appName="test_app")) 100 | # start server in a standalone thread 101 | listener.run_threading() 102 | # or start in current thread 103 | listener.run_forever() 104 | 105 | # publish interfaces, MUST after listener start. 106 | listener.publish() 107 | 108 | # shutdown the server 109 | listener.shutdown() 110 | 111 | ``` 112 | 113 | ## License 114 | 115 | Copyright (c) 2018-present, Ant Financial Service Group 116 | 117 | Apache License 2.0 118 | 119 | See LICENSE file. 120 | 121 | ## Thirdparty 122 | 123 | Part of the mysockpool package uses codes from [urllib3](https://github.com/urllib3/urllib3) project 124 | under the term of MIT License. See origin-license.txt under the mysockpool package. 125 | 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anthunder(a.k.a. sofa-bolt-python) 2 | 3 | [![License](https://img.shields.io/pypi/l/anthunder.svg)](https://pypi.org/project/anthunder/) 4 | [![Version](https://img.shields.io/pypi/v/anthunder.svg)](https://pypi.org/project/anthunder/) 5 | [![Wheel](https://img.shields.io/pypi/wheel/anthunder.svg)](https://pypi.org/project/anthunder/) 6 | [![Python](https://img.shields.io/pypi/pyversions/anthunder.svg)](https://pypi.org/project/anthunder/) 7 | [![devstatus](https://img.shields.io/pypi/status/anthunder.svg)](https://pypi.org/project/anthunder/) 8 | [![Build Status](https://github.com/sofastack/sofa-bolt-python/actions/workflows/unittest.yml/badge.svg)](https://github.com/sofastack/sofa-bolt-python/actions) 9 | [![codecov](https://img.shields.io/codecov/c/gh/sofastack/sofa-bolt-python/master.svg)](https://codecov.io/gh/sofastack/sofa-bolt-python) 10 | [![codebeat](https://codebeat.co/badges/59c6418c-72a1-4229-b363-686a2640e9d5)](https://codebeat.co/projects/github-com-alipay-sofa-bolt-python-master) 11 | 12 | See [English README](https://github.com/sofastack/sofa-bolt-python/blob/master/README.en.md) 13 | 14 | anthunder是一个python实现的BOLT协议库,提供BOLT client和server功能,支持使用BOLT + Protobuf方式的RPC调用。 15 | 16 | ## requirements 17 | 18 | - python3 >= 3.5 (aio classes needs asyncio support) 19 | - mosn >= 1.3 (to use with version >= 0.6) 20 | - mosn < 1.3 (to use with version < 0.6) 21 | 22 | ## roadmap 23 | 24 | - [x] 支持Bolt+pb调用服务端(client端) 25 | - [x] 支持通过servicemesh的服务发现与服务发布 26 | - [x] 支持使用Bolt+pb提供服务(server端) 27 | - [x] 支持其它序列化协议 28 | 29 | ## Tutorial 30 | 以下示例以使用protobuf序列化为例。其它序列化协议请参考demo。 31 | 32 | ### 做为调用方 33 | 0. 获取服务方提供的 `.proto` 文件 34 | 1. 执行`protoc --python_out=. *.proto`命令,编译protobuf文件获得`_pb2.py`文件 35 | 2. 导入pb类并调用接口 36 | 37 | ```python 38 | from SampleServicePbResult_pb2 import SampleServicePbResult 39 | from SampleServicePbRequest_pb2 import SampleServicePbRequest 40 | 41 | from anthunder import AioClient 42 | from anthunder.discovery.mosn import MosnClient, ApplicationInfo 43 | 44 | 45 | spanctx = ctx # ctx is transfered from upstream rpc, which is an object of mytracer.SpanContext, stores rpc_trace_context 46 | # spanctx = SpanContext() # or generate a new context 47 | service_reg = MosnClient() # using mosn for service discovery, see https://mosn.io for detail 48 | service_reg.startup(ApplicationInfo(YOUR_APP_NAME)) 49 | # service_reg = LocalRegistry({interface: (inf_ip, inf_port)}) # or a service-address dict as service discovery 50 | 51 | # 订阅服务, subscribe before client's requests 52 | service_reg.subscribe(interface) 53 | 54 | client = AioClient(YOUR_APP_NAME, service_register=service_reg) # will create a thread, and send heartbeat to remote every 30s 55 | 56 | interface = 'com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0' 57 | 58 | 59 | # 同步调用 60 | content = client.invoke_sync(interface, "hello", 61 | SampleServicePbRequest(name=some_name).SerializeToString(), 62 | timeout_ms=500, spanctx=spanctx) 63 | result = SampleServicePbResult() 64 | result.ParseFromString(content) 65 | 66 | # 异步调用 67 | 68 | def client_callback(resp): 69 | # callback function, accepts bytes as the only argument, 70 | # then do deserialize and further processes 71 | result = SampleServicePbResult() 72 | result.ParseFromString(content) 73 | # do something 74 | 75 | future = client.invoke_async(interface, "hello", 76 | SampleServicePbRequest(name=some_name).SerializeToString(), 77 | spanctx=spanctx, callback=client_callback) 78 | ) 79 | 80 | ``` 81 | 82 | 参考unittest 83 | 84 | ### 做为服务方 85 | 86 | ```python 87 | from anthunder import AioListener 88 | from anthunder.discovery.mosn import MosnClient, ApplicationInfo 89 | 90 | 91 | class SampleService(object): 92 | def __init__(self, ctx): 93 | # service must accept one param as spanctx for rpc tracing support 94 | self.ctx = ctx 95 | 96 | def hello(self, bs: bytes): 97 | obj = SampleServicePbRequest() 98 | obj.ParseFromString(bs) 99 | print("Processing Request", obj) 100 | return SampleServicePbResult(result=obj.name).SerializeToString() 101 | 102 | 103 | interface = 'com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0' 104 | 105 | service_reg = MosnClient() # using mosn for service discovery, see https://mosn.io for detail 106 | service_reg.startup(ApplicationInfo(YOUR_APP_NAME)) 107 | listener = AioListener(('127.0.0.1', 12199), YOUR_APP_NAME, service_register=service_reg) 108 | # register interface and its function, plus its protobuf definition class 109 | listener.register_interface(interface, service_cls=SampleService, provider_meta=ProviderMetaInfo(appName="test_app")) 110 | # start server in a standalone thread 111 | listener.run_threading() 112 | # or start in current thread 113 | listener.run_forever() 114 | 115 | # publish interfaces, MUST after listener start. 116 | listener.publish() 117 | 118 | # shutdown the server 119 | listener.shutdown() 120 | 121 | ``` 122 | 123 | ## License 124 | 125 | Copyright (c) 2018-present, Ant Financial Service Group 126 | 127 | Apache License 2.0 128 | 129 | See LICENSE file. 130 | 131 | ## Thirdparty 132 | 133 | Part of the mysockpool package uses codes from [urllib3](https://github.com/urllib3/urllib3) project 134 | under the term of MIT License. See origin-license.txt under the mysockpool package. 135 | 136 | -------------------------------------------------------------------------------- /anthunder/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__ 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:36 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:36 by jiaqi.hjq init 23 | """ 24 | 25 | __all__ = [ 26 | 'Client', 'SockListener', 'BaseService', 'Request', 'AioListener', 27 | 'AioClient', 'ProviderMetaInfo' 28 | ] 29 | 30 | from anthunder.client import Client, AioClient 31 | from anthunder.listener import SockListener, AioListener 32 | from anthunder.model import BaseService, ProviderMetaInfo, Request -------------------------------------------------------------------------------- /anthunder/client/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ 20 | __all__ = ["Client", "AioClient",] 21 | 22 | from .client import Client 23 | from .aio_client import AioClient -------------------------------------------------------------------------------- /anthunder/client/aio_client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : aio_client 18 | Author : jiaqi.hjq 19 | """ 20 | # Needs python >= 3.4 21 | import asyncio 22 | import functools 23 | import logging 24 | import threading 25 | import traceback 26 | from contextlib import suppress 27 | 28 | try: 29 | import uvloop 30 | 31 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 32 | except ImportError: 33 | pass 34 | from concurrent.futures import TimeoutError, CancelledError 35 | 36 | from anthunder.command.heartbeat import HeartbeatRequest 37 | from anthunder.exceptions import PyboltError, ServerError 38 | from anthunder.helpers.singleton import Singleton 39 | from anthunder.protocol import SofaHeader, BoltRequest, BoltResponse 40 | from anthunder.protocol.constants import PTYPE, CMDCODE, RESPSTATUS 41 | 42 | from .base import _BaseClient 43 | 44 | logger = logging.getLogger(__name__) 45 | 46 | 47 | class AioClient(_BaseClient): 48 | __metaclass__ = Singleton 49 | """bolt client implemented with asyncio""" 50 | def __init__(self, app_name, **kwargs): 51 | super(AioClient, self).__init__(app_name, **kwargs) 52 | self._loop = asyncio.new_event_loop() 53 | self.request_mapping = dict() # request_id: event 54 | self.response_mapping = dict() # request_id: response_pkg 55 | self.connection_mapping = dict() # address: (reader_coro, writer) 56 | self.loop_thread = self._init() 57 | self._pending_dial = dict() # address: (asyncio.Lock) 58 | 59 | def _init(self): 60 | def _t(): 61 | asyncio.set_event_loop(self._loop) 62 | self._loop.run_forever() 63 | 64 | t = threading.Thread(target=_t, daemon=True) 65 | t.start() 66 | if self._service_register and getattr(self._service_register, 67 | "keep_alive", False): 68 | logger.debug("has mesh client, start a heartbeat thread") 69 | asyncio.run_coroutine_threadsafe( 70 | self._heartbeat_timer(self._get_address(None)), self._loop) 71 | logger.debug("client coro thread started") 72 | return t 73 | 74 | def invoke_oneway(self, interface, method, content, *, spanctx, **headers): 75 | header = SofaHeader.build_header(spanctx, interface, method, **headers) 76 | pkg = BoltRequest.new_request(header, content, timeout_ms=-1, codec=self._get_serialize_protocol(interface)) 77 | asyncio.run_coroutine_threadsafe(self.invoke(pkg), loop=self._loop) 78 | 79 | def invoke_sync(self, interface, method, content, *, spanctx, timeout_ms, 80 | **headers): 81 | """blocking call to interface, returns responsepkg.content(as bytes)""" 82 | assert isinstance(timeout_ms, (int, float)) 83 | header = SofaHeader.build_header(spanctx, interface, method, **headers) 84 | pkg = BoltRequest.new_request(header, content, timeout_ms=timeout_ms, codec=self._get_serialize_protocol(interface)) 85 | fut = asyncio.run_coroutine_threadsafe(self.invoke(pkg), 86 | loop=self._loop) 87 | try: 88 | ret = fut.result(timeout=timeout_ms / 1000) 89 | except (TimeoutError, CancelledError) as e: 90 | logger.error("call to [{}:{}] timeout/cancelled. {}".format( 91 | interface, method, e)) 92 | raise 93 | return ret.content 94 | 95 | def invoke_async(self, 96 | interface, 97 | method, 98 | content, 99 | *, 100 | spanctx, 101 | callback=None, 102 | timeout_ms=None, 103 | **headers): 104 | """ 105 | call callback if callback is a callable, 106 | otherwise return a future 107 | Callback should recv a bytes object as the only argument, which is the response pkg's content 108 | """ 109 | header = SofaHeader.build_header(spanctx, interface, method, **headers) 110 | pkg = BoltRequest.new_request(header, content, timeout_ms=timeout_ms or -1, codec=self._get_serialize_protocol(interface)) 111 | fut = asyncio.run_coroutine_threadsafe(self.invoke(pkg), 112 | loop=self._loop) 113 | if callable(callback): 114 | fut.add_done_callback( 115 | self.callback_wrapper( 116 | callback, timeout_ms / 1000 if timeout_ms else None)) 117 | return fut 118 | return fut 119 | 120 | @staticmethod 121 | def callback_wrapper(callback, timeout=None): 122 | """get future's result, then feed to callback""" 123 | @functools.wraps(callback) 124 | def _inner(fut): 125 | try: 126 | ret = fut.result(timeout) 127 | except (CancelledError, TimeoutError): 128 | logger.error("Failed to get result") 129 | return 130 | return callback(ret.content) 131 | 132 | return _inner 133 | 134 | async def _heartbeat_timer(self, address, interval=30): 135 | """Invoke heartbeat periodly""" 136 | while True: 137 | await asyncio.sleep(interval) 138 | await self.invoke_heartbeat(address) 139 | 140 | async def invoke_heartbeat(self, address): 141 | """ 142 | Send heartbeat to server 143 | 144 | :return bool, if the server response properly. 145 | TODO: to break the connection if server response wrongly 146 | """ 147 | pkg = HeartbeatRequest.new_request() 148 | resp = await self.invoke(pkg, address=address) 149 | if resp.request_id != pkg.request_id: 150 | logger.error( 151 | "heartbeat response request_id({}) mismatch with request({}).". 152 | format(resp.request_id, pkg.request_id)) 153 | return False 154 | if resp.respstatus != RESPSTATUS.SUCCESS: 155 | logger.error( 156 | "heartbeat response status ({}) on request({}).".format( 157 | resp.respstatus, resp.request_id)) 158 | return False 159 | return True 160 | 161 | async def _get_connection(self, address): 162 | try: 163 | # fast path return existed connection 164 | if address in self.connection_mapping: 165 | return self.connection_mapping[address] 166 | 167 | async with self._pending_dial.setdefault(address, asyncio.Lock()): 168 | if address in self.connection_mapping: 169 | return self.connection_mapping[address] 170 | 171 | reader, writer = await asyncio.open_connection(*address) 172 | task = asyncio.ensure_future( 173 | self._recv_response(reader, writer)) 174 | return self.connection_mapping.setdefault( 175 | address, (task, writer)) 176 | except Exception as e: 177 | logger.error("Get connection of {} failed: {}".format(address, e)) 178 | raise 179 | 180 | async def invoke(self, request: BoltRequest, *, address=None): 181 | """ 182 | A request response wrapper 183 | :param address: a inet address, currently only for heartbeat request 184 | """ 185 | address = address or self._get_address(request.header['service']) 186 | logger.debug("invoke to address: {}".format(address)) 187 | 188 | event = await self._send_request(request, address=address) 189 | if event is None: 190 | logger.debug( 191 | "no related event, should be a async/oneway call, return now") 192 | return 193 | await event.wait() 194 | return self.response_mapping.pop(request.request_id) 195 | 196 | async def _send_request(self, request: BoltRequest, *, address): 197 | """ 198 | send request and put request_id in request_mapping for response match 199 | :param request: 200 | :param address: a inet address, currently only for heartbeat request 201 | :return: 202 | """ 203 | assert isinstance(request, BoltRequest) 204 | 205 | async def _send(retry=3): 206 | if retry <= 0: 207 | raise PyboltError("send request failed.") 208 | readtask, writer = await self._get_connection(address) 209 | try: 210 | await writer.drain() # avoid back pressure 211 | writer.write(request.to_stream()) 212 | await writer.drain() 213 | except Exception as e: # pragma: no cover 214 | logger.error( 215 | "Request sent to {} failed: {}, may try again.".format( 216 | address, e)) 217 | readtask.cancel() 218 | self.connection_mapping.pop(address, None) 219 | self._pending_dial.pop(address, None) 220 | await _send(retry - 1) 221 | 222 | # generate event object first, ensure every successfully sent request has a event 223 | self.request_mapping[request.request_id] = asyncio.Event() 224 | try: 225 | await _send() 226 | except PyboltError: 227 | logger.error("failed to send request {}".format( 228 | request.request_id)) 229 | self.request_mapping.pop(request.request_id) 230 | return 231 | except Exception: 232 | logger.error(traceback.format_exc()) 233 | self.request_mapping.pop(request.request_id) 234 | return 235 | 236 | if request.ptype == PTYPE.ONEWAY: 237 | self.request_mapping.pop(request.request_id) 238 | return 239 | 240 | return self.request_mapping[request.request_id] 241 | 242 | async def _recv_response(self, reader, writer): 243 | """ 244 | wait response and put it in response_mapping, than notify the invoke coro 245 | :param reader: 246 | :return: 247 | """ 248 | while True: 249 | pkg = None 250 | try: 251 | fixed_header_bs = await reader.readexactly( 252 | BoltResponse.bolt_header_size()) 253 | header = BoltResponse.bolt_header_from_stream(fixed_header_bs) 254 | bs = await reader.readexactly(header['class_len'] + 255 | header['header_len'] + 256 | header['content_len']) 257 | pkg = BoltResponse.bolt_content_from_stream(bs, header) 258 | if pkg.class_name != BoltResponse.class_name: 259 | raise ServerError("wrong class_name:[{}]".format( 260 | pkg.class_name)) 261 | if pkg.cmdcode == CMDCODE.HEARTBEAT: 262 | continue 263 | elif pkg.cmdcode == CMDCODE.REQUEST: 264 | # raise error, the connection will be dropped 265 | raise ServerError("wrong cmdcode:[{}]".format(pkg.cmdcode)) 266 | if pkg.respstatus != RESPSTATUS.SUCCESS: 267 | raise ServerError.from_statuscode(pkg.respstatus) 268 | if pkg.request_id not in self.request_mapping: 269 | continue 270 | self.response_mapping[pkg.request_id] = pkg 271 | 272 | except PyboltError as e: 273 | logger.error(e) 274 | except (asyncio.CancelledError, EOFError, 275 | ConnectionResetError) as e: 276 | logger.error(e) 277 | writer.close() 278 | break 279 | except Exception: 280 | logger.error(traceback.format_exc()) 281 | writer.close() 282 | break 283 | finally: 284 | with suppress(AttributeError, KeyError): 285 | # wake up the coro 286 | event = self.request_mapping.pop(pkg.request_id) 287 | event.set() 288 | -------------------------------------------------------------------------------- /anthunder/client/base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : base.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | from anthunder.protocol.constants import CODEC 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | 27 | class _BaseClient(object): 28 | """ 29 | Basic class for client implementation. Provides subscribe/unsubscribe method. 30 | """ 31 | def __init__(self, app_name, *, service_register): 32 | """ 33 | Check ApplicationInfo's comment for params' explanations. 34 | """ 35 | self._service_register = service_register 36 | 37 | def _get_address(self, interface) -> tuple: 38 | if interface is None: 39 | # on heartbeat, interface would be None. 40 | # for compatibility, return localaddress. 41 | return ("127.0.0.1", 12220) 42 | addstr = self._service_register.get_address(interface) 43 | addstup = addstr.split(':', 2) 44 | return addstup[0], int(addstup[1]) 45 | 46 | def _get_serialize_protocol(self, interface): 47 | meta = self._service_register.get_metadata(interface) 48 | if meta.serializeType == "hessian2": 49 | return CODEC.HESSIAN 50 | if meta.serializeType == "protobuf": 51 | return CODEC.PROTOBUF 52 | raise ValueError("Unknown serializeType {} of interface {}".format( 53 | meta.serializeType, interface)) 54 | 55 | def invoke_sync(self, interface, method, content, **kwargs): 56 | raise NotImplementedError() 57 | 58 | def invoke_async(self, interface, method, content, **kwargs): 59 | raise NotImplementedError() 60 | 61 | def invoke_oneway(self, interface, method, content, **kwargs): 62 | raise NotImplementedError() 63 | -------------------------------------------------------------------------------- /anthunder/client/client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : client 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/17 12:22 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/17 12:22 by jiaqi.hjq init 23 | """ 24 | import logging 25 | import time 26 | 27 | try: 28 | from selectors import DefaultSelector, EVENT_READ 29 | except ImportError: 30 | from selectors34 import DefaultSelector, EVENT_READ 31 | 32 | from mysockpool import PoolManager 33 | from mysockpool.exceptions import SocketValueError 34 | 35 | from anthunder.exceptions import ServerError 36 | from anthunder.helpers.singleton import Singleton 37 | from anthunder.protocol import BoltResponse, BoltRequest, SofaHeader 38 | from anthunder.protocol.constants import PTYPE, RESPSTATUS 39 | from .base import _BaseClient 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | 44 | class Client(_BaseClient): 45 | __metaclass__ = Singleton 46 | """ 47 | Client Entrance 48 | 49 | Usage: 50 | Client(interface).sync(method, content, **kw) 51 | 52 | Need to keep the api stable 53 | """ 54 | 55 | def _raw_invoke(self, interface, method_name, content, spanctx=None, target_app="", uid="", 56 | timeout_ms=None, bolt_ptype=PTYPE.REQUEST, **sofa_headers_extra): 57 | 58 | """ 59 | :param content: 60 | :param service: 61 | :param target_app: 62 | :param uid: 63 | :param rpc_trace_context: preserved, rpc_trace_context object, should be expanded as a dict like 64 | {'rpc_trace_context.traceId': 'xxxxx', ...} 65 | :param kwargs: 66 | """ 67 | # FIXME spanctx might be None here 68 | logger.debug("Calling interface {}, spanctx: {}".format(interface, spanctx.baggage)) 69 | serialize = self._service_register 70 | header = SofaHeader.build_header(spanctx, interface, method_name, target_app=target_app, uid=uid, 71 | **sofa_headers_extra) 72 | p = BoltRequest.new_request(header, content, timeout_ms=timeout_ms or -1, ptype=bolt_ptype, 73 | codec=self._get_serialize_protocol(interface)) 74 | for i in range(3): 75 | # try three times, to avoid connection failure 76 | try: 77 | conn = self._get_pool(interface).get_conn() 78 | conn.send(p.to_stream()) 79 | except SocketValueError as e: 80 | logger.info("Call to interface {} failed, requiest_id: {}, " 81 | "retried: {}, reason: {}".format(interface, p.request_id, i, e)) 82 | continue 83 | except Exception as e: 84 | logger.error("Call to interface {} failed, requiest_id: {}, " 85 | "retried: {}, reason: {}".format(interface, p.request_id, i, e)) 86 | break 87 | else: 88 | break 89 | logger.debug("Called interface {}, request_id: {}".format(interface, p.request_id)) 90 | return p.request_id, conn 91 | 92 | def _get_pool(self, interface): 93 | return PoolManager().connection_pool_from_pool_key( 94 | PoolManager.PoolCls.ConnectionCls.PoolKeyCls(*self._get_address(interface))) 95 | 96 | def invoke_oneway(self, interface, method_name, content, spanctx=None, target_app="", uid="", **headers): 97 | _, c = self._raw_invoke(interface, method_name, content, target_app=target_app, uid=uid, 98 | spanctx=spanctx, bolt_ptype=PTYPE.ONEWAY, **headers) 99 | self._get_pool(interface).put_conn(c) 100 | 101 | def invoke_sync(self, interface, method_name, content, spanctx=None, target_app="", uid="", timeout_ms=None, **headers): 102 | """ 103 | :param request: 104 | :param timeout: if timeout > 0, this specifies the maximum wait time, in 105 | seconds 106 | if timeout <= 0, the select() call won't block, and will 107 | report the currently ready file objects 108 | if timeout is None, select() will block until a monitored 109 | file object becomes ready 110 | :return: serialized response content 111 | :raise: TimeoutError 112 | """ 113 | assert isinstance(timeout_ms, (int, float)) 114 | pkg = BoltResponse 115 | deadline = time.time() + timeout_ms / 1000 + 1 116 | req_id, c = self._raw_invoke(interface, method_name, content, target_app=target_app, uid=uid, 117 | spanctx=spanctx, timeout_ms=timeout_ms, **headers) 118 | 119 | with DefaultSelector() as sel: 120 | sel.register(c, EVENT_READ) 121 | total_size = pkg.bolt_header_size() 122 | resp_bytes = b'' 123 | header = None 124 | while len(resp_bytes) < total_size: 125 | ready = sel.select(timeout=deadline - time.time()) 126 | if not ready: 127 | # timeout 128 | c.close() 129 | raise TimeoutError('Sync call timeout') 130 | for key, event in ready: 131 | resp_bytes += key.fileobj.recv(total_size - len(resp_bytes)) 132 | if not header and len(resp_bytes) >= total_size: 133 | header = pkg.bolt_header_from_stream(resp_bytes) 134 | body_size = header['class_len'] + header['header_len'] + header['content_len'] 135 | total_size += body_size 136 | self._get_pool(interface).put_conn(c) 137 | 138 | resp = pkg.bolt_content_from_stream(resp_bytes[pkg.bolt_header_size():], header) 139 | if resp.request_id != req_id: 140 | raise ServerError("RequestId not match") 141 | if resp.respstatus != RESPSTATUS.SUCCESS: 142 | raise ServerError.from_statuscode(resp.respstatus) 143 | return resp.content 144 | 145 | def invoke_async(self, interface, method_name, content, spanctx=None, callback=None, **kw): 146 | logger.warning("Invoke async to interface {}, not supported yet".format(interface)) 147 | pass 148 | -------------------------------------------------------------------------------- /anthunder/command/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | logger = logging.getLogger(__name__) -------------------------------------------------------------------------------- /anthunder/command/fail_response.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : fail_response.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | from anthunder.protocol import BoltResponse 23 | from anthunder.protocol._sofa_header import empty_header 24 | from anthunder.protocol.constants import PTYPE, CMDCODE 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class FailResponse(BoltResponse): 30 | @classmethod 31 | def response_to(cls, request_id, status_code, **kwargs): 32 | return cls(empty_header, b'', respstatus=status_code, 33 | ptype=PTYPE.RESPONSE, cmdcode=CMDCODE.RESPONSE, request_id=request_id, **kwargs) 34 | -------------------------------------------------------------------------------- /anthunder/command/heartbeat.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : heartbeat 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | from anthunder.helpers.request_id import RequestId 23 | from anthunder.protocol import BoltRequest, BoltResponse 24 | from anthunder.protocol._sofa_header import empty_header 25 | from anthunder.protocol.constants import PTYPE, CMDCODE 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class HeartbeatRequest(BoltRequest): 31 | class_name = b"" 32 | 33 | @classmethod 34 | def new_request(cls): 35 | return cls(empty_header, b'', ptype=PTYPE.REQUEST, cmdcode=CMDCODE.HEARTBEAT, request_id=next(RequestId)) 36 | 37 | 38 | class HeartbeatResponse(BoltResponse): 39 | class_name = b"" 40 | 41 | @classmethod 42 | def response_to(cls, request_id): 43 | return cls(empty_header, b'', ptype=PTYPE.RESPONSE, cmdcode=CMDCODE.HEARTBEAT, request_id=request_id) 44 | -------------------------------------------------------------------------------- /anthunder/discovery/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ 20 | __all__ = ['LocalRegistry'] 21 | 22 | from .local import LocalRegistry -------------------------------------------------------------------------------- /anthunder/discovery/local.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | logger = logging.getLogger(__name__) 23 | from anthunder.helpers.singleton import Singleton 24 | from anthunder.helpers.immutable_dict import ImmutableValueDict 25 | from anthunder.model.service import ProviderMetaInfo 26 | 27 | 28 | class LocalRegistry(object): 29 | """ 30 | A simple service registry with all addresses hardcode in object of this class. 31 | For test purpose only. 32 | Any service running in production should be in service mesh or use a service discovery 33 | services to obtain remote addresses. 34 | """ 35 | __metaclass__ = Singleton 36 | 37 | keep_alive = False # only for mesh heartbeat 38 | 39 | def __init__(self, servicemap): 40 | self._service_meta_map = ImmutableValueDict(servicemap) 41 | 42 | def subscribe(self, service): 43 | logger.warning("local registry does not support this method") 44 | 45 | def unsubscribe(self, service): 46 | logger.warning("local registry does not support this method") 47 | 48 | def publish(self, service): 49 | logger.warning("local registry does not support this method") 50 | 51 | def unpublish(self, service): 52 | logger.warning("local registry does not support this method") 53 | 54 | def get_address(self, interface: str) -> str: 55 | """ 56 | :return: address str 57 | """ 58 | meta = self._service_meta_map.get(interface) 59 | if meta is None: 60 | raise Exception( 61 | "No address available for {}, you meed to declare it explicitly in LocalRegistry's init parameter" 62 | .format(interface)) 63 | return meta.address 64 | 65 | def get_metadata(self, interface: str) -> ProviderMetaInfo: 66 | meta = self._service_meta_map.get(interface) 67 | if meta is None: 68 | raise Exception( 69 | "No available interface for {}, you meed to declare it explicitly in LocalRegistry's init parameter" 70 | .format(interface)) 71 | return meta.metadata 72 | 73 | 74 | class FixedAddressRegistry(LocalRegistry): 75 | """always returns fixed address, for test purpose only.""" 76 | def __init__(self, address, metadata: ProviderMetaInfo = None): 77 | self._address = address 78 | self._metadata = metadata or ProviderMetaInfo() 79 | 80 | def get_address(self, service) -> str: 81 | return self._address 82 | 83 | def get_metadata(self, service: str) -> ProviderMetaInfo: 84 | return self._metadata -------------------------------------------------------------------------------- /anthunder/discovery/mosn/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : startup 18 | Author : jiaqi.hjq 19 | """ 20 | from threading import RLock 21 | 22 | import attr 23 | import requests 24 | from requests import ConnectionError 25 | 26 | from anthunder.helpers.singleton import Singleton 27 | from anthunder.model.service import ProviderMetaInfo, SubServiceMeta 28 | 29 | 30 | @attr.s 31 | class PublishServiceRequest(object): 32 | serviceName = attr.ib() 33 | providerMetaInfo = attr.ib() 34 | port = attr.ib() 35 | protocolType = attr.ib(default="DEFAULT") 36 | onlyPublishInCloud = attr.ib(default=False) 37 | 38 | 39 | @attr.s 40 | class ApplicationInfo(object): 41 | """ 42 | ApplicationInfo is the request sent to mosnd to register your application. 43 | For compatibility reasons, the param with False value (None, empty str, False, etc..) will be dropped 44 | when posting to monsd. 45 | 46 | :param: appName: your application's name 47 | :param: dataCenter: (optional) the datacenter where the application deployed. 48 | :param: zone: (optional) the zone where the application deployed. 49 | :param: registryEndPoint: (optional) the configcenter's endpoint address you want to register to. 50 | :param: antShareCloud: (optional) is your application deployed at Ant Cloud. 51 | :param: accessKey: (must when antShareCloud=True) your access key. 52 | :param: secretKey: (must when antShareCloud=True) your secrect key. 53 | """ 54 | appName = attr.ib() 55 | dataCenter = attr.ib(default="") 56 | zone = attr.ib(default="") 57 | registryEndPoint = attr.ib(default="") 58 | accessKey = attr.ib(default="") 59 | secretKey = attr.ib(default="") 60 | antShareCloud = attr.ib(default=False) 61 | 62 | 63 | class MosnClient(object): 64 | __metaclass__ = Singleton 65 | 66 | def __init__(self, 67 | *, 68 | keep_alive=True, 69 | service_api="http://127.0.0.1:13330/"): 70 | """ 71 | :param appinfo: application infomation data, see ApplicationInfo's comments. 72 | :type appinfo: ApplicationInfo, see ApplicationInfo's comments. 73 | """ 74 | self._sess = requests.session() 75 | self._rlock = RLock() 76 | self._started = False 77 | self.keep_alive = keep_alive 78 | self.service_api = service_api 79 | self._service_meta_dict = {} 80 | 81 | def startup(self, appinfo: ApplicationInfo): 82 | with self._rlock: 83 | if not self._started: 84 | self._post("configs/application", 85 | attr.asdict(appinfo, filter=lambda a, v: v)) 86 | self._started = True 87 | return self._started 88 | 89 | def subscribe(self, interface: str): 90 | # subscribe returns: `json:"errorMessage"` 91 | # `json:"success"` 92 | # `json:"serviceName"` 93 | # `json:"datas"` 94 | ret = self._post("services/subscribe", dict(serviceName=interface)) 95 | meta = SubServiceMeta.from_bolt_url(ret["datas"][0]) 96 | self._service_meta_dict[interface] = meta 97 | return 98 | 99 | def unsubscribe(self, interface: str): 100 | return self._post("services/unsubscribe", dict(serviceName=interface)) 101 | 102 | def publish(self, address, interface: str, provider: ProviderMetaInfo): 103 | """ 104 | :param publish_service_request: 105 | :type publish_service_request: PublishServiceRequest 106 | :return: 107 | """ 108 | req = PublishServiceRequest(port=str(address[1]), 109 | serviceName=interface, 110 | providerMetaInfo=provider) 111 | 112 | return self._post("services/publish", attr.asdict(req)) 113 | 114 | def unpublish(self, interface: str): 115 | return self._post("services/unpublish", dict(serviceName=interface)) 116 | 117 | def get_address(self, interface: str) -> str: 118 | """ 119 | :return: address str 120 | """ 121 | meta = self._service_meta_dict.get(interface) 122 | if meta is None: 123 | raise Exception( 124 | "No address available for {}, do you subscribe it?".format( 125 | interface)) 126 | return meta.address 127 | 128 | def get_metadata(self, interface: str) -> ProviderMetaInfo: 129 | meta = self._service_meta_dict.get(interface) 130 | if meta is None: 131 | raise Exception( "No available interface for {}, do you subscribe it?".format( interface)) 132 | return meta.metadata 133 | 134 | def _post(self, endpoint, json): 135 | addr = self.service_api + endpoint 136 | r = self._sess.post(addr, json=json) 137 | if r.status_code != 200: 138 | raise ConnectionError("Connect to service mesh failed: {}".format( 139 | r.status_code)) 140 | result = r.json() 141 | if result.get('success') is not True: 142 | raise ConnectionError("Connect to service mesh failed: {}".format( 143 | result.get('errorMessage', "MISSING ERROR MESSAGE"))) 144 | return result 145 | -------------------------------------------------------------------------------- /anthunder/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : exceptions 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 14:13 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 14:13 by jiaqi.hjq init 23 | """ 24 | 25 | 26 | class PyboltError(Exception): 27 | pass 28 | 29 | 30 | class ServerError(PyboltError): 31 | def __init__(self, msg): 32 | super(ServerError, self).__init__("ServerError: {}".format(msg)) 33 | 34 | @classmethod 35 | def from_statuscode(cls, code): 36 | return cls("ServerError: RESPSTATUS={0.value}, {0.name}".format(code)) 37 | 38 | 39 | class ClientError(PyboltError): 40 | def __init__(self, msg): 41 | super(ClientError, self).__init__("ClientError: {}".format(msg)) 42 | -------------------------------------------------------------------------------- /anthunder/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/17 18:00 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/17 18:00 by jiaqi.hjq init 23 | """ -------------------------------------------------------------------------------- /anthunder/helpers/immutable_dict.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : immutable_dict 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class ImmutableValueDict(dict): 26 | def __setitem__(self, key, value): 27 | if key in self: 28 | raise KeyError("Key {} exists, cannot be set to value {}".format(key, value)) 29 | super(ImmutableValueDict, self).__setitem__(key, value) 30 | -------------------------------------------------------------------------------- /anthunder/helpers/request_id.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : request_id 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | from random import randint 22 | from itertools import cycle, chain 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | _max = 0x7fffffff # 4 bytes signed int 27 | _start = randint(0, _max) 28 | RequestId = cycle(chain(range(_start, _max + 1), range(0, _start))) 29 | -------------------------------------------------------------------------------- /anthunder/helpers/singleton.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : singletone 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/17 18:00 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/17 18:00 by jiaqi.hjq init 23 | """ 24 | 25 | 26 | class Singleton(type): 27 | _instances = {} 28 | 29 | def __call__(cls, *args, **kwargs): 30 | if cls not in cls._instances: 31 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 32 | return cls._instances[cls] 33 | -------------------------------------------------------------------------------- /anthunder/listener/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | 20 | """ 21 | 22 | __all__ = ['SockListener', 'AioListener'] 23 | 24 | from .sock_listener import SockListener 25 | from .aio_listener import AioListener -------------------------------------------------------------------------------- /anthunder/listener/aio_listener.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : aio_listener 18 | Author : jiaqi.hjq 19 | """ 20 | # Needs python >= 3.4 21 | import asyncio 22 | import logging 23 | import threading 24 | import traceback 25 | import opentracing 26 | 27 | from anthunder.listener.base_listener import NoProcessorError 28 | 29 | try: 30 | import uvloop 31 | 32 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 33 | except ImportError: 34 | pass 35 | 36 | from threading import RLock 37 | from mytracer.helpers import tracer 38 | from concurrent.futures import ThreadPoolExecutor 39 | 40 | from anthunder.command.fail_response import FailResponse 41 | from anthunder.command.heartbeat import HeartbeatResponse 42 | from anthunder.exceptions import ClientError 43 | from anthunder.protocol import BoltRequest, SofaHeader, BoltResponse 44 | from anthunder.protocol.constants import PTYPE, CMDCODE, RESPSTATUS 45 | from anthunder.protocol.exceptions import ProtocolError 46 | from .base_listener import BaseListener, BaseHandler 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | 51 | class AioThreadpoolRequestHandler(BaseHandler): 52 | MAX_WORKER = 30 53 | 54 | def __init__(self): 55 | self.executor = ThreadPoolExecutor(max_workers=self.MAX_WORKER) 56 | self.local = threading.local() 57 | self.lock = RLock() 58 | 59 | def handle_request(self, spanctx, service, method, body): 60 | """do not run heavy job here, just put payloads to executors and return future object""" 61 | logger.info("receive biz request of {}:{}".format(service, method)) 62 | try: 63 | ServiceCls = self.interface_mapping[service] 64 | except KeyError as e: 65 | logger.error("Service not found in interface registry: [{}]".format(service)) 66 | raise NoProcessorError("Service not found in interface registry: [{}]".format(service)) from e 67 | try: 68 | svc_obj = ServiceCls(spanctx) 69 | func = getattr(svc_obj, method) 70 | except AttributeError as e: 71 | logger.error("No such method[{}]".format(method)) 72 | raise NoProcessorError("No such method[{}]".format(method)) from e 73 | 74 | future = self.executor.submit(func, body) 75 | logger.info("biz request submitted to function({})".format(func)) 76 | return future 77 | 78 | def register_interface(self, interface, service_cls, *service_cls_args, **service_cls_kwargs): 79 | """ 80 | register interface: service_cls relationship 81 | :param interface: the interface name bind to the service 82 | :param service_cls: the service class factory. 83 | Will be called will a spanctx of each request and returns a Service Object. 84 | :param service_cls_args: extra positional arguments for service_cls 85 | :param service_cls_kwargs: extra keyword arguments for service_cls 86 | :return: None 87 | """ 88 | with self.lock: 89 | if service_cls_args or service_cls_kwargs: 90 | def service_cls_wrapper(spanctx): 91 | return service_cls(spanctx, *service_cls_args, **service_cls_kwargs) 92 | else: 93 | service_cls_wrapper = service_cls 94 | self.interface_mapping[interface] = service_cls_wrapper 95 | 96 | 97 | class AioListener(BaseListener): 98 | handlerCls = AioThreadpoolRequestHandler 99 | 100 | def __init__(self, *args, **kwargs): 101 | super(AioListener, self).__init__(*args, **kwargs) 102 | self._loop = asyncio.new_event_loop() 103 | self._server = None 104 | 105 | def initialize(self): 106 | pass 107 | 108 | def run_forever(self): 109 | """loop thread""" 110 | asyncio.set_event_loop(self._loop) 111 | coro = asyncio.start_server(self._handler_connection, *self.address, **self.server_kwargs) 112 | self._server = self._loop.run_until_complete(coro) 113 | logger.info("Aio Listener initialized, entering event loop") 114 | self._loop.run_forever() 115 | 116 | def shutdown(self): 117 | """quit the server loop""" 118 | logger.info("Aio Listener shutting down") 119 | if self._loop and self._server: 120 | self._server.close() 121 | asyncio.run_coroutine_threadsafe(self._server.wait_closed(), self._loop) 122 | 123 | async def _dispatch(self, call_type, request_id, sofa_header, body, *, timeout_ms, codec, writer): 124 | """send request to handler""" 125 | service = sofa_header.get('sofa_head_target_service') or sofa_header.get('service') 126 | if not service: 127 | await self._write_msg( 128 | writer, 129 | FailResponse.response_to(request_id, 130 | RESPSTATUS.CLIENT_SEND_ERROR, 131 | codec=codec).to_stream(), 132 | ) 133 | logger.error("Missing service name in sofa header [{}]".format(sofa_header)) 134 | return 135 | method = sofa_header.get('sofa_head_method_name') 136 | if not method: 137 | await self._write_msg( 138 | writer, 139 | FailResponse.response_to(request_id, 140 | RESPSTATUS.CLIENT_SEND_ERROR, 141 | codec=codec).to_stream()) 142 | logger.error("Missing method name in sofa header [{}]".format(sofa_header)) 143 | return 144 | 145 | spanctx = tracer.extract(opentracing.Format.TEXT_MAP, sofa_header) 146 | 147 | try: 148 | future = self.handler.handle_request(spanctx, service, method, body) 149 | except NoProcessorError: 150 | await self._write_msg( 151 | writer, 152 | FailResponse.response_to(request_id, 153 | RESPSTATUS.NO_PROCESSOR, 154 | codec=codec).to_stream(), 155 | ) 156 | return 157 | except Exception: 158 | await self._write_msg( 159 | writer, 160 | FailResponse.response_to(request_id, 161 | RESPSTATUS.SERVER_EXCEPTION, 162 | codec=codec).to_stream(), 163 | ) 164 | return 165 | if PTYPE.ONEWAY == call_type: 166 | # Just return future 167 | return future 168 | 169 | try: 170 | afut = asyncio.wrap_future(future) 171 | result = await asyncio.wait_for(afut, timeout_ms / 1000 if timeout_ms > 0 else None) 172 | except (asyncio.CancelledError, asyncio.TimeoutError) as e: 173 | logger.error("Task run cancelled/timeout in {}ms, service:[{}]: error:[{}]".format(timeout_ms, service, e)) 174 | await self._write_msg( 175 | writer, 176 | FailResponse.response_to(request_id, 177 | RESPSTATUS.TIMEOUT, 178 | codec=codec).to_stream(), 179 | ) 180 | return 181 | except Exception: 182 | logger.error(traceback.format_exc()) 183 | await self._write_msg( 184 | writer, 185 | FailResponse.response_to(request_id, 186 | RESPSTATUS.UNKNOWN, 187 | codec=codec).to_stream(), 188 | ) 189 | return 190 | 191 | if result: 192 | header = dict() 193 | tracer.inject(spanctx, opentracing.Format.TEXT_MAP, header) 194 | pkg = BoltResponse.response_to(result, 195 | request_id=request_id, 196 | codec=codec) 197 | try: 198 | await self._write_msg(writer, pkg.to_stream()) 199 | except Exception: 200 | logger.error(traceback.format_exc()) 201 | 202 | async def _write_msg(self, writer, msg): 203 | await writer.drain() # clean the buffer, avoid backpressure 204 | writer.write(msg) 205 | await writer.drain() 206 | 207 | async def _close_writer(self, writer): 208 | try: 209 | writer.write_eof() 210 | await writer.drain() 211 | except: 212 | pass 213 | writer.close() 214 | 215 | async def _handler_connection(self, reader, writer): 216 | """ 217 | Full duplex model 218 | Only recv request here, run a new coro to process and send back response in same connection. 219 | """ 220 | logger.info("connection created.") 221 | first_req = True 222 | while True: 223 | try: 224 | try: 225 | fixed_header_bs = await reader.readexactly(BoltRequest.bolt_header_size()) 226 | except asyncio.IncompleteReadError: 227 | if first_req: 228 | # just connected, do nothing. most likely L4 health checks from mosn/upstream 229 | await self._close_writer(writer) 230 | break 231 | # break the loop 232 | raise 233 | 234 | first_req = False 235 | logger.debug("received bolt header({})".format(fixed_header_bs)) 236 | header = BoltRequest.bolt_header_from_stream(fixed_header_bs) 237 | call_type = header['ptype'] 238 | cmdcode = header['cmdcode'] 239 | 240 | class_name = await reader.readexactly(header['class_len']) 241 | logger.debug("received classname({})".format(class_name)) 242 | 243 | bs = await reader.readexactly(header['header_len']) 244 | logger.debug("received sofa header({})".format(bs)) 245 | 246 | sofa_header = SofaHeader.from_bytes(bs) 247 | body = await reader.readexactly(header['content_len']) 248 | logger.debug("received sofa body({})".format(body)) 249 | 250 | if cmdcode == CMDCODE.HEARTBEAT: 251 | logger.info("received heartbeat, request_id={}".format(header['request_id'])) 252 | asyncio.ensure_future( 253 | self._write_msg(writer, HeartbeatResponse.response_to(header['request_id']).to_stream())) 254 | continue 255 | 256 | if cmdcode == CMDCODE.RESPONSE: 257 | raise ClientError("wrong cmdcode:[{}]".format(cmdcode)) 258 | 259 | if class_name != "com.alipay.sofa.rpc.core.request.SofaRequest".encode(): 260 | raise ClientError("wrong class_name:[{}]".format(class_name)) 261 | 262 | logger.debug("dispatching request[{}]".format(header['request_id'])) 263 | asyncio.ensure_future( 264 | self._dispatch(call_type, 265 | header['request_id'], 266 | sofa_header, 267 | body, 268 | timeout_ms=header['timeout'], 269 | codec=header['codec'], 270 | writer=writer)) 271 | except ClientError as e: 272 | logger.error(str(e) + " returning CLIENT_SEND_ERROR") 273 | await self._write_msg(writer, FailResponse.response_to(header['request_id'], 274 | RESPSTATUS.CLIENT_SEND_ERROR).to_stream()) 275 | continue 276 | 277 | except ProtocolError as e: 278 | logger.error(str(e) + " returning CODEC_EXCEPTION") 279 | await self._write_msg(writer, FailResponse.response_to(header['request_id'], 280 | RESPSTATUS.CODEC_EXCEPTION).to_stream()) 281 | continue 282 | 283 | except EOFError as e: 284 | logger.warning("Connection closed by remote: {}".format(e)) 285 | try: 286 | await writer.drain() # clean the buffer, avoid backpressure 287 | writer.write(FailResponse.response_to(header['request_id'], 288 | RESPSTATUS.CONNECTION_CLOSED).to_stream()) 289 | except: 290 | pass 291 | await self._close_writer(writer) 292 | break 293 | 294 | except Exception: 295 | logger.error("Unknow exception, close connection") 296 | logger.error(traceback.format_exc()) 297 | try: 298 | await writer.drain() # clean the buffer, avoid backpressure 299 | writer.write(FailResponse.response_to(header['request_id'], RESPSTATUS.UNKNOWN).to_stream()) 300 | except: 301 | pass 302 | await self._close_writer(writer) 303 | break 304 | -------------------------------------------------------------------------------- /anthunder/listener/base_listener.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : base_listener 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | import threading 22 | 23 | from anthunder.exceptions import ServerError 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class BaseHandler(object): 29 | """Handle bolt request""" 30 | # stores interface: 31 | # "interface": (function, protobuf_cls) 32 | interface_mapping = dict() 33 | 34 | def register_interface(self, interface, service_cls, *service_cls_args, 35 | **service_cls_kwargs): 36 | raise NotImplementedError() 37 | 38 | def handle_request(self, ctx, service, method, body): 39 | raise NotImplementedError() 40 | 41 | 42 | class BaseListener(object): 43 | """ 44 | Base class for listener(server) implementation. provides publish/unpublish method. 45 | """ 46 | handlerCls = BaseHandler 47 | 48 | def __init__(self, 49 | address, 50 | app_name, 51 | *, 52 | service_register=None, 53 | **server_kwargs): 54 | """ 55 | :param address: the socket address will be listened on. 56 | :type address: tuple (host:str, port:int) 57 | Check ApplicationInfo's comment for other params' explanations. 58 | """ 59 | if isinstance(address, str): 60 | address = address.split(':', 2) 61 | self.address = address 62 | self.app_name = app_name 63 | self.server_kwargs = server_kwargs 64 | self.handler = self.handlerCls() 65 | self.service_register = service_register 66 | self.service_provider = dict() 67 | 68 | def initialize(self): 69 | raise NotImplementedError() 70 | 71 | def register_interface(self, 72 | interface, 73 | *, 74 | provider_meta, 75 | service_cls, 76 | service_cls_args=None, 77 | service_cls_kwargs=None): 78 | service_cls_args = service_cls_args or tuple() 79 | service_cls_kwargs = service_cls_kwargs or dict() 80 | self.handler.register_interface(interface, service_cls, 81 | *service_cls_args, 82 | **service_cls_kwargs) 83 | self.service_provider[interface] = provider_meta 84 | 85 | def publish(self): 86 | """ 87 | Publish all the interfaces in handler.interface_mapping to mosnd 88 | """ 89 | if self.service_register: 90 | for service_name, provider_meta in self.service_register: 91 | self.service_register.publish(self.address, service_name, 92 | provider_meta) 93 | 94 | def unpublish(self): 95 | """ 96 | Revoke all the interfaces in handler.interface_mapping from mosnd. 97 | """ 98 | if self.service_register: 99 | for service_name in self.service_register: 100 | self.service_register.unpublish(service_name) 101 | 102 | def run_forever(self): 103 | raise NotImplementedError() 104 | 105 | def shutdown(self): 106 | raise NotImplementedError() 107 | 108 | def run_threading(self): 109 | t = threading.Thread(target=self.run_forever, daemon=True) 110 | t.start() 111 | 112 | 113 | class NoProcessorError(ServerError): 114 | pass 115 | -------------------------------------------------------------------------------- /anthunder/listener/sock_listener.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : sock_listener.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | import traceback 22 | from errno import ECONNRESET 23 | 24 | from socketserver import StreamRequestHandler, ThreadingTCPServer 25 | 26 | import opentracing 27 | from mytracer import tracer 28 | 29 | from anthunder.command.fail_response import FailResponse 30 | from anthunder.command.heartbeat import HeartbeatResponse 31 | from anthunder.exceptions import ClientError 32 | from anthunder.protocol import BoltRequest, SofaHeader 33 | from anthunder.protocol.constants import CMDCODE, RESPSTATUS 34 | from anthunder.protocol import BoltResponse 35 | 36 | from .base_listener import BaseListener, BaseHandler, NoProcessorError 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | 41 | class SockServiceHandler(BaseHandler): 42 | """ 43 | handling service dispatch 44 | """ 45 | 46 | def handle_request(self, spanctx, service, method, body): 47 | """blocking handles request""" 48 | try: 49 | ServiceCls = self.interface_mapping[service] 50 | except KeyError as e: 51 | logger.error("Service not found in interface registry: [{}]".format(service)) 52 | raise NoProcessorError("Service not found in interface registry: [{}]".format(service)) 53 | try: 54 | svc_obj = ServiceCls(spanctx) 55 | func = getattr(svc_obj, method) 56 | except AttributeError as e: 57 | logger.error("No such method[{}]".format(method)) 58 | raise NoProcessorError("No such method[{}]".format(method)) 59 | 60 | return func(body) 61 | 62 | def register_interface(self, interface, service_cls, *service_cls_args, **service_cls_kwargs): 63 | """ 64 | register interface: service_cls relationship 65 | :param interface: the interface name bind to the service 66 | :param service_cls: the service class factory. 67 | Will be called will a spanctx of each request and returns a Service Object. 68 | :param service_cls_args: extra positional arguments for service_cls 69 | :param service_cls_kwargs: extra keyword arguments for service_cls 70 | :return: None 71 | """ 72 | if service_cls_args or service_cls_kwargs: 73 | def service_cls_wrapper(spanctx): 74 | return service_cls(spanctx, *service_cls_args, **service_cls_kwargs) 75 | else: 76 | service_cls_wrapper = service_cls 77 | self.interface_mapping[interface] = service_cls_wrapper 78 | 79 | 80 | class SockBoltHandler(StreamRequestHandler): 81 | """A tcp request handler, handles bolt protocol""" 82 | 83 | service_handler = None # for service_handler injection, should be a duck type of BaseHandler 84 | 85 | def _readexactly(self, bs_cnt): 86 | bs = b'' 87 | while len(bs) < bs_cnt: 88 | bs += self.rfile.read(bs_cnt - len(bs)) 89 | return bs 90 | 91 | def handle(self): 92 | try: 93 | fixed_header_bs = self._readexactly(BoltRequest.bolt_header_size()) 94 | header = BoltRequest.bolt_header_from_stream(fixed_header_bs) 95 | call_type = header['ptype'] 96 | cmdcode = header['cmdcode'] 97 | 98 | class_name = self._readexactly(header['class_len']) 99 | bs = self._readexactly(header['header_len']) 100 | sofa_header = SofaHeader.from_bytes(bs) 101 | body = self._readexactly(header['content_len']) 102 | 103 | request_id = header['request_id'] 104 | 105 | if cmdcode == CMDCODE.HEARTBEAT: 106 | self.wfile.write(HeartbeatResponse.response_to(request_id).to_stream()) 107 | self.wfile.flush() 108 | return 109 | 110 | if cmdcode == CMDCODE.RESPONSE: 111 | raise ClientError("wrong cmdcode:[{}]".format(cmdcode)) 112 | 113 | if class_name != "com.alipay.sofa.rpc.core.request.SofaRequest".encode(): 114 | raise ClientError("wrong class_name:[{}]".format(class_name)) 115 | 116 | service = sofa_header.get('sofa_head_target_service') or sofa_header.get('service') 117 | if not service: 118 | self.wfile.write(FailResponse.response_to(request_id, RESPSTATUS.CLIENT_SEND_ERROR).to_stream()) 119 | self.wfile.flush() 120 | logger.error("Missing service name in sofa header [{}]".format(sofa_header)) 121 | return 122 | method = sofa_header.get('sofa_head_method_name') 123 | if not method: 124 | self.wfile.write(FailResponse.response_to(request_id, RESPSTATUS.CLIENT_SEND_ERROR).to_stream()) 125 | self.wfile.flush() 126 | logger.error("Missing method name in sofa header [{}]".format(sofa_header)) 127 | return 128 | 129 | spanctx = tracer.extract(opentracing.Format.TEXT_MAP, sofa_header) 130 | # call servicehandler 131 | ret = self.service_handler.handle_request(spanctx, service, method, body) 132 | self.wfile.write(BoltResponse.response_to(ret, request_id=request_id).to_stream()) 133 | self.wfile.flush() 134 | 135 | except OSError as e: 136 | if e.errno != ECONNRESET: 137 | raise 138 | 139 | except Exception as e: 140 | logger.error(traceback.format_exc()) 141 | 142 | 143 | class SockListener(BaseListener): 144 | handlerCls = SockServiceHandler 145 | 146 | def __init__(self, *args, **kwargs): 147 | super(SockListener, self).__init__(*args, **kwargs) 148 | # inject service_handler 149 | SockBoltHandler.service_handler = self.handler 150 | self.server = ThreadingTCPServer(self.address, SockBoltHandler) 151 | 152 | def run_forever(self): 153 | self.server.serve_forever() 154 | 155 | def shutdown(self): 156 | self.server.shutdown() 157 | -------------------------------------------------------------------------------- /anthunder/model/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : base_listener 18 | Author : jiaqi.hjq 19 | """ 20 | all = ["SubServiceMeta", "ProviderMetaInfo", 'Request'] 21 | 22 | from .service import BaseService, SubServiceMeta, PubServiceMeta, ProviderMetaInfo 23 | from .request import Request -------------------------------------------------------------------------------- /anthunder/model/request.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : request 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/21 16:38 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/21 16:38 by jiaqi.hjq init 23 | """ 24 | 25 | 26 | class Request(object): 27 | """ 28 | Basic class for request object 29 | 30 | currently holds the rpc_trace_context attributes 31 | """ 32 | 33 | def __init__(self, spanctx): 34 | self.spanctx = spanctx 35 | -------------------------------------------------------------------------------- /anthunder/model/service.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : base_listener 18 | Author : jiaqi.hjq 19 | """ 20 | import attr 21 | from urllib.parse import parse_qs 22 | 23 | 24 | class BaseService(object): 25 | """ 26 | Service classes provides service interfaces. 27 | After registering to a interface, Listener will create a object of Service type on each request on this interface, 28 | and call to the method specified in request header with request body bytes. 29 | The object is created with a spanctx as its first positional argument, such spanctx can than be referenced 30 | by self.ctx, and used in logging and/or passing to downstream in later rpc calling. 31 | """ 32 | def __init__(self, ctx): 33 | self.ctx = ctx 34 | 35 | 36 | @attr.s 37 | class ProviderMetaInfo(object): 38 | appName = attr.ib(default="default") 39 | protocol = attr.ib(default="1") 40 | version = attr.ib(default="4.0") 41 | serializeType = attr.ib(default="protobuf") 42 | 43 | 44 | class SubServiceMeta(object): 45 | """ 46 | SubServiceMeta is metadata and addresses from service subscriber. 47 | In sofa stack with MOSN as data plane, this metadata is returned as a list of 48 | bolt url, with address as host:port, and metadata as querystring. 49 | """ 50 | def __init__(self, address_list: list, metadata=None): 51 | self.addresses = [address_list] if isinstance(address_list, 52 | str) else address_list 53 | self.metadata = metadata or ProviderMetaInfo() 54 | 55 | @classmethod 56 | def from_bolt_url(cls, url: str): 57 | # TODO: to support multiple addresses with weight sets in querystring 58 | # 127.0.0.1:12220?p=1&v=4.0&_SERIALIZETYPE=hessian2&app_name=someapp 59 | # 127.0.0.1:12220?p=1&v=4.0&_SERIALIZETYPE=protobuf&app_name=someapp 60 | addr, qs = url.split('?', 2) 61 | qsd = parse_qs(qs) 62 | pmi = ProviderMetaInfo(qsd.get('app_name', "default"), 63 | qsd.get("protocol", "1"), 64 | qsd.get("version", "4.0"), 65 | qsd.get("_SERIALIZETYPE", "protobuf")) 66 | 67 | o = cls(addr, pmi) 68 | return o 69 | 70 | @property 71 | def address(self): 72 | # TODO: currently only returns the first address. 73 | # should return different address round-robin or base on weight. 74 | return self.addresses[0] 75 | 76 | 77 | PubServiceMeta = ProviderMetaInfo -------------------------------------------------------------------------------- /anthunder/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 13:19 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 13:19 by jiaqi.hjq init 23 | """ 24 | __all__ = ["BoltRequest", "BoltResponse", "SofaHeader", "RpcTraceContext"] 25 | 26 | from ._request_pkg import BoltRequest 27 | from ._response_pkg import BoltResponse 28 | from ._sofa_header import SofaHeader 29 | from ._rpc_trace_context import RpcTraceContext 30 | # * 31 | # Request command protocol for v1 32 | # 0 1 2 4 6 8 10 12 14 16 33 | # +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 34 | # |proto| type| cmdcode |ver2 | requestId |codec| timeout | classLen | 35 | # +-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+ 36 | # |headerLen | contentLen | ... ... | 37 | # +-----------+-----------+-----------+ + 38 | # | className + header + content bytes | 39 | # + + 40 | # | ... ... | 41 | # +-----------------------------------------------------------------------------------------------+ 42 | # 43 | # proto: code for protocol 目前这个值是1,表示 bolt 44 | # type: request(1)/response(0)/request oneway(2) 45 | # cmdcode: code for remoting command 心跳是0,request 是1, response 是2 46 | # ver2:version for remoting command 目前是1,以后可能扩展 47 | # requestId: id of request 请求的 requestId,响应时会带上 48 | # codec: code for codec 序列化,hessian 是1,pb 是11,java 是2 49 | # timeout: timeout for request 默认-1,本地调用的超时时间,这个是给服务端用来实现 failfast,单位毫秒 50 | # headerLen: length of header headerMap 的字节数 51 | # contentLen: length of content content 的字节数. 52 | # 53 | # Response command protocol for v1 54 | # 0 1 2 3 4 6 8 10 12 14 16 55 | # +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ 56 | # |proto| type| cmdcode |ver2 | requestId |codec|respstatus | classLen |headerLen | 57 | # +-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+ 58 | # | contentLen | ... ... | 59 | # +-----------------------+ + 60 | # | className + header + content bytes | 61 | # + + 62 | # | ... ... | 63 | # +-----------------------------------------------------------------------------------------------+ 64 | # respstatus: response status 服务端响应结果状态 65 | # 66 | # 67 | # 其他 68 | # className固定,对于请求是com.alipay.sofa.rpc.core.request.SofaRequest 69 | # 对于响应是com.alipay.sofa.rpc.core.response.SofaResponse 70 | -------------------------------------------------------------------------------- /anthunder/protocol/_package_base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : bolt_request 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:38 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:38 by jiaqi.hjq init 23 | """ 24 | import struct 25 | 26 | from .exceptions import DecodeError, ParamTypeError 27 | from ._sofa_header import SofaHeader 28 | from .constants import PROTO, VER2, CODEC, PTYPE, CMDCODE, RESPSTATUS 29 | 30 | 31 | class BoltPackage(object): 32 | fmt = "" 33 | class_name = b"" 34 | bolt_headers = () 35 | 36 | def __init__(self, header, content, 37 | proto=PROTO.BOLT, ptype=None, cmdcode=None, ver2=VER2.REMOTING, request_id=None, 38 | codec=CODEC.PROTOBUF, timeout=None, respstatus=0, **kwargs): 39 | """ 40 | See init's docstring for definition of params 41 | :param class_name: 42 | :param header: 43 | :type header: SofaHeader 44 | :param content: 45 | :type content: bytes 46 | :param proto: 47 | :param ptype: 48 | :param cmdcode: 49 | :param ver2: 50 | :param request_id: 51 | :type request_id: int 52 | :param codec: 53 | :param timeout: optional, only in request pkg, milliseconds 54 | :type timeout: int 55 | :param respstatus: optional, only in response pkg 56 | :type respstatus: int 57 | """ 58 | self.header = header 59 | self._header_bytes = header.to_bytes() 60 | self.content = content 61 | 62 | self.proto = PROTO(proto) 63 | self.ptype = PTYPE(ptype) 64 | self.cmdcode = CMDCODE(cmdcode) 65 | self.ver2 = VER2(ver2) 66 | self.request_id = request_id 67 | self.codec = CODEC(codec) 68 | self.timeout = timeout or 0 69 | self.respstatus = RESPSTATUS(respstatus) 70 | 71 | def __repr__(self): 72 | return "{}({})".format(self.__class__.__name__, ",".join(map("{0[0]}={0[1]}".format, self.__dict__.items()))) 73 | 74 | __str__ = __repr__ 75 | 76 | @property 77 | def class_len(self): 78 | return len(self.class_name) 79 | 80 | @property 81 | def header_len(self): 82 | return len(self._header_bytes) 83 | 84 | @property 85 | def content_len(self): 86 | return len(self.content) 87 | 88 | @property 89 | def body_len(self): 90 | """total length of classname + header + content""" 91 | return self.class_len + self.header_len + self.content_len 92 | 93 | def validate(self): 94 | def _check(obj, need_type): 95 | if not isinstance(obj, need_type): 96 | raise ParamTypeError("TypeError: must be {}, not {}".format(need_type.__name__, type(obj))) 97 | 98 | _check(self.proto, PROTO) 99 | _check(self.ptype, PTYPE) 100 | _check(self.cmdcode, CMDCODE) 101 | _check(self.ver2, VER2) 102 | _check(self.request_id, int) 103 | _check(self.codec, CODEC) 104 | _check(self.class_name, bytes) 105 | _check(self.header, SofaHeader) 106 | _check(self.content, bytes) 107 | 108 | def to_stream(self): # pragma: no cover 109 | raise NotImplementedError 110 | 111 | @classmethod 112 | def bolt_content_from_stream(cls, stream, bolt_headers): 113 | try: 114 | bodyfmt = "%ds%ds%ds" % (bolt_headers['class_len'], bolt_headers['header_len'], bolt_headers['content_len']) 115 | class_name, header, content = struct.unpack(bodyfmt, stream) 116 | 117 | return cls(SofaHeader.from_bytes(header), content, **bolt_headers) 118 | except Exception as e: # pragma: no cover 119 | raise DecodeError(e) 120 | 121 | @classmethod 122 | def bolt_header_from_stream(cls, stream): 123 | try: 124 | values = struct.unpack(cls.fmt, stream[:cls.bolt_header_size()]) 125 | return dict(zip(cls.bolt_headers, values)) 126 | 127 | except Exception as e: # pragma: no cover 128 | raise DecodeError(e) 129 | 130 | @classmethod 131 | def from_stream(cls, stream): 132 | bh = cls.bolt_header_from_stream(stream) 133 | return cls.bolt_content_from_stream(stream[cls.bolt_header_size():], bh) 134 | 135 | @classmethod 136 | def bolt_header_size(cls): 137 | return struct.calcsize(cls.fmt) 138 | -------------------------------------------------------------------------------- /anthunder/protocol/_request_pkg.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : _request_pkg 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:39 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:39 by jiaqi.hjq init 23 | """ 24 | import struct 25 | from anthunder.helpers.request_id import RequestId 26 | from .constants import PTYPE, CMDCODE 27 | from .exceptions import EncodeError 28 | from ._package_base import BoltPackage 29 | 30 | 31 | class BoltRequest(BoltPackage): 32 | fmt = "!bbhblblHHL" 33 | class_name = b"com.alipay.sofa.rpc.core.request.SofaRequest" 34 | bolt_headers = ("proto", "ptype", "cmdcode", "ver2", "request_id", "codec", "timeout", 35 | "class_len", "header_len", "content_len") 36 | 37 | def to_stream(self): 38 | self.validate() 39 | try: 40 | bodyfmt = "%ds%ds%ds" % (self.class_len, self.header_len, self.content_len) 41 | return struct.pack(self.fmt + bodyfmt, self.proto, self.ptype, self.cmdcode, 42 | self.ver2, self.request_id, self.codec, self.timeout, 43 | self.class_len, self.header_len, self.content_len, 44 | self.class_name, self._header_bytes, self.content) 45 | except Exception as e: # pragma: no cover 46 | raise EncodeError(e) 47 | 48 | @classmethod 49 | def new_request(cls, header, content, ptype=PTYPE.REQUEST, timeout_ms=None, **kwargs): 50 | return cls(header, content, ptype=ptype, cmdcode=CMDCODE.REQUEST, 51 | request_id=next(RequestId), timeout=timeout_ms or -1, **kwargs) 52 | -------------------------------------------------------------------------------- /anthunder/protocol/_response_pkg.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : _response_pkg 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:39 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:39 by jiaqi.hjq init 23 | """ 24 | import struct 25 | 26 | from ._sofa_header import empty_header 27 | from .constants import PTYPE, CMDCODE 28 | from .exceptions import EncodeError 29 | from ._package_base import BoltPackage 30 | 31 | 32 | class BoltResponse(BoltPackage): 33 | fmt = "!bbhblbhHHL" 34 | class_name = b"com.alipay.sofa.rpc.core.response.SofaResponse" 35 | bolt_headers = ("proto", "ptype", "cmdcode", "ver2", "request_id", "codec", "respstatus", 36 | "class_len", "header_len", "content_len") 37 | 38 | def to_stream(self): 39 | self.validate() 40 | try: 41 | bodyfmt = "%ds%ds%ds" % (self.class_len, self.header_len, self.content_len) 42 | return struct.pack(self.fmt + bodyfmt, self.proto, self.ptype, self.cmdcode, 43 | self.ver2, self.request_id, self.codec, self.respstatus, 44 | self.class_len, self.header_len, self.content_len, 45 | self.class_name, self._header_bytes, self.content) 46 | except Exception as e: # pragma: no cover 47 | raise EncodeError(e) 48 | 49 | @classmethod 50 | def response_to(cls, content, request_id, **kwargs): # pragma: no cover 51 | return cls(empty_header, content, request_id=request_id, ptype=PTYPE.RESPONSE, cmdcode=CMDCODE.RESPONSE, **kwargs) 52 | -------------------------------------------------------------------------------- /anthunder/protocol/_rpc_trace_context.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : rpc_trace_context 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/18 15:45 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/18 15:45 by jiaqi.hjq init 23 | """ 24 | 25 | 26 | class RpcTraceContext(object): 27 | """ 28 | Holds rpc_trace_context.xxx contexts, can be expanded for adding them to sofa_headers 29 | """ 30 | prefix = "rpc_trace_context." 31 | 32 | def __init__(self, **kwargs): 33 | for k, v in kwargs.items(): 34 | setattr(self, k, v) 35 | 36 | def expand(self): 37 | return {self.prefix + k: str(v) for k, v in self.__dict__.items()} 38 | -------------------------------------------------------------------------------- /anthunder/protocol/_sofa_header.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : header 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/17 14:04 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/17 14:04 by jiaqi.hjq init 23 | """ 24 | import struct 25 | from mytracer import SpanContext 26 | 27 | from ._rpc_trace_context import RpcTraceContext 28 | from .exceptions import EncodeError, DecodeError 29 | 30 | 31 | def _int2bytes_be(i): 32 | return struct.pack('>i', i) 33 | 34 | 35 | def _bytes2int_be(bs): 36 | i, = struct.unpack('>i', bs) 37 | return i 38 | 39 | 40 | def _str_to_bytes_with_len(s, coding='utf-8'): 41 | """ 42 | a simple str serialize method 43 | 44 | java's writeInt writes 4 bytes in 'high bytes first' according to its documents 45 | 46 | :param s: 47 | :param coding: 48 | :return: bytes object 49 | """ 50 | assert isinstance(s, str) 51 | b = s.encode(coding) 52 | return _int2bytes_be(len(b)) + b 53 | 54 | 55 | def _bytes_to_str(b, coding='utf-8'): 56 | """ 57 | a simple str unserialize method 58 | 59 | java's writeInt writes 4 bytes in 'high bytes first' according to its documents 60 | 61 | :param b: 62 | :param coding: 63 | :return: list of str 64 | """ 65 | 66 | assert isinstance(b, bytes) 67 | ret = [] 68 | while b: 69 | if 4 > len(b): # pragma: no cover 70 | raise DecodeError('decoding bytes to int failed, not enough length') 71 | l = _bytes2int_be(b[:4]) # length 72 | if l == -1: 73 | # -1(\xff\xff\xff\xff) is null in java, see issue #2 74 | # set l = 0 will take this 4 bytes as b"", and continue to parse rest bytes. 75 | l = 0 76 | if l < 0: 77 | # Something went wrong 78 | raise DecodeError('decoding bytes to str failed, negative content length') 79 | n = 4 + l # next point 80 | if n > len(b): # pragma: no cover 81 | # incomplete bytes 82 | raise DecodeError('decoding bytes to str failed, not enough length') 83 | ret.append(b[4: n].decode(coding)) 84 | b = b[n:] 85 | 86 | return ret 87 | 88 | 89 | class SofaHeader(dict): 90 | """ 91 | Readonly dict, with special serialize method 92 | """ 93 | 94 | def __setitem__(self, key, value): # pragma: no cover 95 | pass 96 | 97 | @classmethod 98 | def from_bytes(cls, b): 99 | ss = _bytes_to_str(b) 100 | keys = ss[::2] 101 | vals = ss[1::2] 102 | if len(keys) != len(vals): # pragma: no cover 103 | raise DecodeError('number of keys and values not match') 104 | return cls(zip(keys, vals)) 105 | 106 | def to_bytes(self): 107 | try: 108 | return b''.join(_str_to_bytes_with_len(k) + _str_to_bytes_with_len(v) for k, v in self.items()) 109 | except AttributeError as e: # pragma: no cover 110 | raise EncodeError(e) 111 | 112 | def __len__(self): 113 | return len(self.to_bytes()) 114 | 115 | @classmethod 116 | def build_header(cls, spanctx, interface, method_name, target_app="", uid="", 117 | **sofa_headers_extra): 118 | """ 119 | :param spanctx: 120 | :type spanctx: SpanContext 121 | :param interface: 122 | :param method_name: 123 | :param target_app: 124 | :param uid: 125 | :param sofa_headers_extra: 126 | :return: 127 | """ 128 | rpc_trace_context_expand = RpcTraceContext(**spanctx.baggage).expand() 129 | 130 | kwargs = dict() 131 | kwargs.update(**sofa_headers_extra) 132 | kwargs.update(**rpc_trace_context_expand) 133 | 134 | header = cls(sofa_head_target_service=interface, 135 | sofa_head_method_name=method_name, 136 | sofa_head_target_app=target_app, 137 | service=interface, 138 | uid=uid, **kwargs) 139 | return header 140 | 141 | 142 | empty_header = SofaHeader() 143 | -------------------------------------------------------------------------------- /anthunder/protocol/constants.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : constants 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:47 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:47 by jiaqi.hjq init 23 | """ 24 | from enum import IntEnum 25 | 26 | 27 | class PROTO(IntEnum): 28 | BOLT = 1 29 | 30 | 31 | class PTYPE(IntEnum): 32 | RESPONSE = 0 33 | REQUEST = 1 34 | ONEWAY = 2 35 | 36 | 37 | class CMDCODE(IntEnum): 38 | HEARTBEAT = 0 39 | REQUEST = 1 40 | RESPONSE = 2 41 | 42 | 43 | class VER2(IntEnum): 44 | REMOTING = 1 45 | 46 | 47 | class CODEC(IntEnum): 48 | HESSIAN = 1 49 | PROTOBUF = 11 50 | JAVA = 2 51 | 52 | 53 | class RESPSTATUS(IntEnum): 54 | SUCCESS = 0x0000 55 | ERROR = 0x0001 56 | SERVER_EXCEPTION = 0x0002 57 | UNKNOWN = 0x0003 58 | SERVER_THREADPOOL_BUSY = 0x0004 59 | ERROR_COMM = 0x0005 60 | NO_PROCESSOR = 0x0006 61 | TIMEOUT = 0x0007 62 | CLIENT_SEND_ERROR = 0x0008 63 | CODEC_EXCEPTION = 0x0009 64 | CONNECTION_CLOSED = 0x0010 65 | SERVER_SERIAL_EXCEPTION = 0x0011 66 | SERVER_DESERIAL_EXCEPTION = 0x0012 67 | -------------------------------------------------------------------------------- /anthunder/protocol/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : exceptions 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/24 16:30 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/24 16:30 by jiaqi.hjq init 23 | """ 24 | from anthunder.exceptions import PyboltError 25 | 26 | 27 | class ProtocolError(PyboltError): 28 | pass 29 | 30 | 31 | class EncodeError(ProtocolError): 32 | pass 33 | 34 | 35 | class DecodeError(ProtocolError): 36 | pass 37 | 38 | 39 | class ParamTypeError(ProtocolError): 40 | pass 41 | -------------------------------------------------------------------------------- /install-protobuf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | version=3.6.1 3 | file=protoc-$version-linux-x86_64.zip 4 | set -ex 5 | if [ ! -f "$HOME/protobuf/bin/protoc" ]; then 6 | echo $PATH 7 | wget https://github.com/protocolbuffers/protobuf/releases/download/v$version/$file 8 | unzip $file -d $HOME/protobuf 9 | else 10 | echo "Using cached directory." 11 | fi 12 | -------------------------------------------------------------------------------- /mysockpool/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ 20 | __all__ = ["PoolManager", "ConnectionPool", "SocketConnection"] 21 | 22 | from .connection import SocketConnection 23 | from .connection_pool import ConnectionPool 24 | from .pool_manager import PoolManager 25 | from ._wait import wait_for_read, wait_for_write 26 | -------------------------------------------------------------------------------- /mysockpool/_wait.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : _wait 22 | """ 23 | from .exceptions import SocketValueError 24 | 25 | try: 26 | from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE 27 | except ImportError: 28 | # python2.7 29 | from selectors34 import DefaultSelector, EVENT_READ, EVENT_WRITE 30 | 31 | 32 | def _wait_for_io_events(socks, events, timeout): 33 | """ Waits for IO events to be available from a list of sockets 34 | or optionally a single socket if passed in. Returns a list of 35 | sockets that can be interacted with immediately. """ 36 | if not isinstance(socks, list): 37 | # Probably just a single socket. 38 | if hasattr(socks, "fileno"): 39 | socks = [socks] 40 | # Otherwise it might be a non-list iterable. 41 | else: 42 | socks = list(socks) 43 | with DefaultSelector() as selector: 44 | for sock in socks: 45 | try: 46 | selector.register(sock, events) 47 | except Exception as e: 48 | raise SocketValueError("SocketValueError", e) 49 | return [key[0].fileobj for key in 50 | selector.select(timeout) if key[1] & events] 51 | 52 | 53 | def wait_for_read(socks, timeout=None): 54 | """ Waits for reading to be available from a list of sockets 55 | or optionally a single socket if passed in. Returns a list of 56 | sockets that can be read from immediately. """ 57 | return _wait_for_io_events(socks, EVENT_READ, timeout) 58 | 59 | 60 | def wait_for_write(socks, timeout=None): 61 | """ Waits for writing to be available from a list of sockets 62 | or optionally a single socket if passed in. Returns a list of 63 | sockets that can be written to immediately. """ 64 | return _wait_for_io_events(socks, EVENT_WRITE, timeout) 65 | -------------------------------------------------------------------------------- /mysockpool/connection.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : connection.py 22 | """ 23 | import socket 24 | from collections import namedtuple 25 | from io import BytesIO 26 | 27 | from .exceptions import LocationValueError 28 | from ._wait import wait_for_write, wait_for_read 29 | 30 | 31 | class SocketConnection(object): 32 | PoolKeyCls = namedtuple('SocketPoolKey', ('host', 'port')) 33 | 34 | def __init__(self, pool_key, blocking=False, **kwargs): 35 | self.validate_pool_key(pool_key) 36 | self.pool_key = pool_key 37 | sock = socket.socket(socket.AF_INET) 38 | sock.setblocking(blocking) 39 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 40 | try: 41 | sock.connect((pool_key.host, pool_key.port)) 42 | except: 43 | pass 44 | self.sock = sock 45 | 46 | @classmethod 47 | def validate_pool_key(cls, pool_key): 48 | if not isinstance(pool_key, cls.PoolKeyCls): 49 | raise LocationValueError("Invalid pool_key object for creating new connection.") 50 | 51 | def close(self): 52 | try: 53 | self.sock.close() 54 | except: 55 | pass 56 | 57 | def send(self, *args, **kw): 58 | wait_for_write(self.sock) 59 | return self.sock.send(*args, **kw) 60 | 61 | def sendall(self, *args, **kw): 62 | wait_for_write(self.sock) 63 | return self.sock.sendall(*args, **kw) 64 | 65 | def recv(self, *args, **kw): 66 | return self.sock.recv(*args, **kw) 67 | 68 | def recvexactly(self, size): 69 | read = 0 70 | buf = BytesIO() 71 | while read < size: 72 | wait_for_read(self.sock) 73 | bs = self.sock.recv(size - read) 74 | buf.write(bs) 75 | read += len(bs) 76 | return buf.getvalue() 77 | 78 | def fileno(self): 79 | return self.sock.fileno() 80 | 81 | def __del__(self): 82 | self.close() 83 | -------------------------------------------------------------------------------- /mysockpool/connection_pool.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : connection_pool 22 | """ 23 | import logging 24 | import threading 25 | import queue 26 | 27 | from .connection import SocketConnection 28 | from .exceptions import LocationValueError, ClosedPoolError, EmptyPoolError 29 | from .utils import is_connection_dropped 30 | 31 | log = logging.getLogger(__name__) 32 | 33 | 34 | # Pool objects 35 | class ConnectionPool(object): 36 | """ 37 | Base class for all connection pools, such as 38 | :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. 39 | """ 40 | 41 | QueueCls = queue.LifoQueue 42 | ConnectionCls = SocketConnection 43 | 44 | def __init__( 45 | self, 46 | pool_key, 47 | initial_connections=1, 48 | max_connections=20, 49 | max_free_connections=5, 50 | min_free_connections=1, 51 | block=False, 52 | ): 53 | if not pool_key: 54 | raise LocationValueError("No pool_key specified.") 55 | 56 | self._lock = threading.RLock() 57 | 58 | self.ConnectionCls.validate_pool_key(pool_key) 59 | self.pool_key = pool_key 60 | 61 | self.block = block 62 | self.num_connections = 0 63 | self.min_free_connections = min_free_connections 64 | self.max_connections = max_connections 65 | 66 | self.pool = self.QueueCls(max_free_connections) 67 | for i in range(initial_connections): 68 | self.pool.put(self._new_conn()) 69 | 70 | def __str__(self): 71 | return '%s(%r)' % (type(self).__name__, self.pool_key) 72 | 73 | def __enter__(self): 74 | return self 75 | 76 | def __exit__(self, exc_type, exc_val, exc_tb): 77 | self.close() 78 | # Return False to re-raise any potential exceptions 79 | return False 80 | 81 | def _new_conn(self): 82 | with self._lock: 83 | if self.num_connections >= self.max_connections: 84 | raise EmptyPoolError( 85 | self, "Pool reached maximum size and no more " 86 | "connections are allowed.") 87 | 88 | self.num_connections += 1 89 | log.debug("Starting new connection (%d): %r", self.num_connections, 90 | self.pool_key) 91 | 92 | conn = self.ConnectionCls(self.pool_key) 93 | return conn 94 | 95 | def get_conn(self, timeout=None): 96 | """ 97 | Get a connection. Will return a pooled connection if one is available. 98 | 99 | If no connections are available and :prop:`.block` is ``False``, then a 100 | fresh connection is returned. 101 | 102 | :param timeout: 103 | Seconds to wait before giving up and raising 104 | :class:`urllib3.exceptions.EmptyPoolError` if the pool is empty and 105 | :prop:`.block` is ``True``. 106 | """ 107 | conn = None 108 | try: 109 | conn = self.pool.get(block=self.block, timeout=timeout) 110 | 111 | if self.pool.qsize() < self.min_free_connections: 112 | self.put_conn(self._new_conn()) 113 | 114 | except AttributeError: # self.pool is None 115 | raise ClosedPoolError(self, "Pool is closed.") 116 | 117 | except queue.Empty: 118 | if self.block: 119 | raise EmptyPoolError( 120 | self, "Pool reached maximum size and no more " 121 | "connections are allowed.") 122 | pass # Oh well, we'll create a new connection then 123 | 124 | # If this is a persistent connection, check if it got disconnected 125 | if conn and is_connection_dropped(conn): 126 | log.debug("Resetting dropped connection: %r", self.pool_key) 127 | conn.close() 128 | with self._lock: 129 | self.num_connections -= 1 130 | 131 | return conn or self._new_conn() 132 | 133 | def put_conn(self, conn): 134 | """ 135 | Put a connection back into the pool. 136 | 137 | :param conn: 138 | Connection object for the current host and port as returned by 139 | :meth:`._new_conn` or :meth:`._get_conn`. 140 | 141 | If the pool is already full, the connection is closed and discarded 142 | because we exceeded maxsize. If connections are discarded frequently, 143 | then maxsize should be increased. 144 | 145 | If the pool is closed, then the connection will be closed and discarded. 146 | """ 147 | try: 148 | self.pool.put(conn, block=False) 149 | return # Everything is dandy, done. 150 | except AttributeError: 151 | # self.pool is None. 152 | pass 153 | except queue.Full: 154 | # This should never happen if self.block == True 155 | log.warning("Connection pool is full, discarding connection: %s", 156 | self.pool_key) 157 | with self._lock: 158 | self.num_connections -= 1 159 | 160 | # Connection never got put back into the pool, close it. 161 | if conn: 162 | conn.close() 163 | 164 | def close(self): 165 | """ 166 | Close all pooled connections and disable the pool. 167 | """ 168 | # Disable access to the pool 169 | old_pool, self.pool = self.pool, None 170 | 171 | try: 172 | while True: 173 | conn = old_pool.get(block=False) 174 | if conn: 175 | conn.close() 176 | 177 | except queue.Empty: 178 | pass # Done. 179 | -------------------------------------------------------------------------------- /mysockpool/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : exceptions 22 | """ 23 | 24 | 25 | class MysockpoolError(Exception): 26 | "Base exception for errors caused within a pool." 27 | 28 | def __init__(self, pool, message): 29 | self.pool = pool 30 | super(MysockpoolError, self).__init__(self, "%s: %s" % (pool, message)) 31 | 32 | def __reduce__(self): 33 | # For pickling purposes. 34 | return self.__class__, (None, None) 35 | 36 | 37 | class EmptyPoolError(MysockpoolError): 38 | "Raised when a pool runs out of connections and no more are allowed." 39 | pass 40 | 41 | 42 | class ClosedPoolError(MysockpoolError): 43 | "Raised when a request enters a pool after the pool has been closed." 44 | pass 45 | 46 | 47 | class LocationValueError(ValueError, MysockpoolError): 48 | "Raised when there is something wrong with a given URL input." 49 | pass 50 | 51 | 52 | class SocketValueError(ValueError, MysockpoolError): 53 | "Raised when socket is unable to register in selector." 54 | -------------------------------------------------------------------------------- /mysockpool/origin-license.txt: -------------------------------------------------------------------------------- 1 | This is the MIT license: http://www.opensource.org/licenses/mit-license.php 2 | 3 | Copyright 2008-2016 Andrey Petrov and contributors (see CONTRIBUTORS.txt) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /mysockpool/pool_manager.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : pool_manager 22 | """ 23 | from contextlib import contextmanager 24 | 25 | from .recently_used_container import RecentlyUsedContainer 26 | from .connection_pool import ConnectionPool 27 | 28 | 29 | class PoolManager(object): 30 | """ 31 | Allows for arbitrary requests while transparently keeping track of 32 | necessary connection pools for you. 33 | 34 | :param num_pools: 35 | Number of connection pools to cache before discarding the least 36 | recently used pool. 37 | 38 | :param \\**connection_pool_kw: 39 | Additional parameters are used to create fresh 40 | :class:`urllib3.connectionpool.ConnectionPool` instances. 41 | """ 42 | 43 | PoolCls = ConnectionPool 44 | 45 | def __init__(self, max_pools=10, **pool_kwargs): 46 | self.pool_kwargs = pool_kwargs 47 | self.pools = RecentlyUsedContainer(max_pools, 48 | dispose_func=lambda p: p.close()) 49 | 50 | def __enter__(self): 51 | return self 52 | 53 | def __exit__(self, exc_type, exc_val, exc_tb): 54 | self.clear() 55 | # Return False to re-raise any potential exceptions 56 | return False 57 | 58 | def _new_pool(self, pool_key): 59 | """ 60 | Create a new :class:`ConnectionPool` based on host, port, scheme, and 61 | any additional pool keyword arguments. 62 | 63 | If ``request_context`` is provided, it is provided as keyword arguments 64 | to the pool class used. This method is used to actually create the 65 | connection pools handed out by :meth:`connection_from_url` and 66 | companion methods. It is intended to be overridden for customization. 67 | """ 68 | return self.PoolCls(pool_key, **self.pool_kwargs) 69 | 70 | def clear(self): 71 | """ 72 | Empty our store of pools and direct them all to close. 73 | 74 | This will not affect in-flight connections, but they will not be 75 | re-used after completion. 76 | """ 77 | self.pools.clear() 78 | 79 | def connection_pool_from_pool_key(self, pool_key): 80 | """ 81 | Get a :class:`ConnectionPool` based on the provided pool key. 82 | 83 | ``pool_key`` should be a namedtuple that only contains immutable 84 | objects. At a minimum it must have the ``scheme``, ``host``, and 85 | ``port`` fields. 86 | """ 87 | with self.pools.lock: 88 | # If the scheme, host, or port doesn't match existing open 89 | # connections, open a new ConnectionPool. 90 | pool = self.pools.get(pool_key) 91 | if pool: 92 | return pool 93 | 94 | # Make a fresh ConnectionPool of the desired type 95 | pool = self._new_pool(pool_key) 96 | self.pools[pool_key] = pool 97 | 98 | return pool 99 | 100 | @contextmanager 101 | def connection_scope(self, pool_key): 102 | try: 103 | pool = self.connection_pool_from_pool_key(pool_key) 104 | conn = pool.get_conn() 105 | yield conn 106 | except: 107 | raise 108 | finally: 109 | try: 110 | pool.put_conn(conn) 111 | except (NameError, AttributeError): 112 | pass 113 | -------------------------------------------------------------------------------- /mysockpool/recently_used_container.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : recently_used_container 22 | """ 23 | from collections.abc import MutableMapping 24 | from collections import OrderedDict 25 | from threading import RLock 26 | 27 | 28 | class RecentlyUsedContainer(MutableMapping): 29 | """ 30 | Provides a thread-safe dict-like container which maintains up to 31 | ``maxsize`` keys while throwing away the least-recently-used keys beyond 32 | ``maxsize``. 33 | 34 | :param maxsize: 35 | Maximum number of recent elements to retain. 36 | 37 | :param dispose_func: 38 | Every time an item is evicted from the container, 39 | ``dispose_func(value)`` is called. Callback which will get called 40 | """ 41 | 42 | ContainerCls = OrderedDict 43 | 44 | def __init__(self, maxsize=10, dispose_func=None): 45 | self._maxsize = maxsize 46 | self.dispose_func = dispose_func 47 | 48 | self._container = self.ContainerCls() 49 | self.lock = RLock() 50 | 51 | def __getitem__(self, key): 52 | # Re-insert the item, moving it to the end of the eviction line. 53 | with self.lock: 54 | item = self._container.pop(key) 55 | self._container[key] = item 56 | return item 57 | 58 | def __setitem__(self, key, value): 59 | evicted_value = _Null 60 | with self.lock: 61 | # Possibly evict the existing value of 'key' 62 | evicted_value = self._container.get(key, _Null) 63 | self._container[key] = value 64 | 65 | # If we didn't evict an existing value, we might have to evict the 66 | # least recently used item from the beginning of the container. 67 | if len(self._container) > self._maxsize: 68 | _key, evicted_value = self._container.popitem(last=False) 69 | 70 | if self.dispose_func and evicted_value is not _Null: 71 | self.dispose_func(evicted_value) 72 | 73 | def __delitem__(self, key): 74 | with self.lock: 75 | value = self._container.pop(key) 76 | 77 | if self.dispose_func: 78 | self.dispose_func(value) 79 | 80 | def __len__(self): 81 | with self.lock: 82 | return len(self._container) 83 | 84 | def __iter__(self): 85 | raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') 86 | 87 | def clear(self): 88 | with self.lock: 89 | # Copy pointers to all values, then wipe the mapping 90 | values = list(self._container.values()) 91 | self._container.clear() 92 | 93 | if self.dispose_func: 94 | for value in values: 95 | self.dispose_func(value) 96 | 97 | def keys(self): 98 | with self.lock: 99 | return list(self._container.keys()) 100 | 101 | 102 | _Null = object() 103 | -------------------------------------------------------------------------------- /mysockpool/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | Original Copyright 2008-2016 Andrey Petrov and contributors 18 | (see https://github.com/urllib3/urllib3/blob/master/CONTRIBUTORS.txt) 19 | under the MIT license (https://opensource.org/licenses/MIT). 20 | ------------------------------------------------------ 21 | File Name : utils 22 | """ 23 | from ._wait import wait_for_read 24 | 25 | 26 | def is_connection_dropped(conn): # Platform-specific 27 | """ 28 | Returns True if the connection is dropped and should be closed. 29 | 30 | :param conn: 31 | :class:`httplib.HTTPConnection` object. 32 | 33 | Note: For platforms like AppEngine, this will always return ``False`` to 34 | let the platform handle connection recycling transparently for us. 35 | """ 36 | sock = getattr(conn, 'sock', False) 37 | if sock is False: # Platform-specific: AppEngine 38 | return False 39 | if sock is None: # Connection already closed (such as by httplib). 40 | return True 41 | 42 | try: 43 | return bool(wait_for_read(sock, timeout=0.0)) 44 | except: 45 | return True 46 | -------------------------------------------------------------------------------- /mytracer/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/25 16:27 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/25 16:27 by jiaqi.hjq init 23 | 24 | ### Workflow for client side tracing: 25 | 26 | - Prepare request 27 | - Load the current trace state 28 | - Start a span 29 | - Inject the span into the request 30 | - Send request 31 | - Receive response 32 | - Finish the span 33 | 34 | ### The workflow for tracing a server request is as follows: 35 | 36 | - Server Receives Request 37 | - Extract the current trace state from the inter-process transport (HTTP, etc) 38 | - Start the span 39 | - Store the current trace state 40 | - Server Finishes Processing the Request / Sends Response 41 | - Finish the span 42 | 43 | """ 44 | 45 | __all__ = [ 46 | "MyTracer", 47 | "MySpan", 48 | "SpanContext", 49 | "new_span", 50 | "child_span_of", 51 | "follows_span_from", 52 | "RpcId", 53 | "TraceId", 54 | "tracer" 55 | ] 56 | 57 | from .tracer import MyTracer 58 | from .span import MySpan 59 | from .span_context import SpanContext 60 | from .helpers import new_span, child_span_of, follows_span_from, tracer 61 | from ._rpc_id import RpcId 62 | from ._trace_id import TraceId 63 | -------------------------------------------------------------------------------- /mytracer/_rpc_id.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : _span_id 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 14:59 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 14:59 by jiaqi.hjq init 23 | """ 24 | 25 | 26 | class RpcId(object): 27 | def __init__(self, *rpcids): 28 | """ 29 | span id, represented as `a.b.c...`, where `a`, `b`, `c` are numbers. 30 | 31 | :param rpcids: numbers 32 | :type rpcids: int or str, last one must be int. 33 | """ 34 | if not rpcids: 35 | self.rpcids = [1] 36 | else: 37 | def _innertrans(s): 38 | try: 39 | return int(s) 40 | except ValueError: 41 | return s 42 | 43 | self.rpcids = list(map(_innertrans, rpcids)) 44 | if not isinstance(self.rpcids[-1], int): 45 | raise TypeError("Illigal span id, last segment must be int, not '{}'.".format(self.rpcids[-1])) 46 | 47 | self._child_count = 0 48 | self._parent = None 49 | 50 | def __str__(self): 51 | return ".".join(map(str, self.rpcids)) 52 | 53 | __repr__ = __str__ 54 | 55 | @classmethod 56 | def extend_id(cls, ids, extend): 57 | """extend a existing ids to a new one""" 58 | new_ids = list(ids) 59 | new_ids.append(extend) 60 | return cls(*new_ids) 61 | 62 | def _new_child_of(self): 63 | self._child_count += 1 64 | obj = self.extend_id(self.rpcids, self._child_count) 65 | obj._parent = self # attach a link to parent rpcId 66 | return obj 67 | 68 | def _new_follows_from(self): 69 | obj = self.extend_id(self.rpcids[:-1], self.rpcids[-1] + 1) 70 | if self._parent: 71 | # a follows up span is a sibling span, means it is a child span of parent 72 | self._parent._child_count += 1 73 | obj._parent = self._parent 74 | return obj 75 | 76 | def new_by_reference_type(self, ref_type): 77 | return getattr(self, '_new_' + ref_type)() 78 | 79 | def __eq__(self, other): 80 | if not isinstance(other, self.__class__): 81 | return False 82 | if len(self.rpcids) != len(other.rpcids): 83 | return False 84 | return all(map(lambda x: x[0] == x[1], zip(self.rpcids, other.rpcids))) 85 | -------------------------------------------------------------------------------- /mytracer/_trace_id.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : trace_id 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 14:00 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 14:00 by jiaqi.hjq init 23 | """ 24 | import itertools 25 | import os 26 | import socket 27 | import time 28 | import ipaddress 29 | 30 | 31 | class TraceId(object): 32 | _sequence = itertools.cycle(range(1000, 9001)) 33 | 34 | def __init__(self, trace=None): 35 | if not isinstance(trace, str): 36 | _ip = int(ipaddress.ip_address(socket.gethostbyname(socket.gethostname()))) 37 | _ts = int(time.time() * 1000) 38 | _sq = next(self._sequence) 39 | _pid = os.getpid() 40 | self._trace_id = "{:0>8x}{:}{}{}".format(_ip, _ts, _sq, _pid) 41 | else: 42 | self._trace_id = trace 43 | 44 | def __str__(self): 45 | return self._trace_id 46 | 47 | __repr__ = __str__ 48 | 49 | def __eq__(self, other): 50 | if not isinstance(other, self.__class__): 51 | return False 52 | return self._trace_id == other._trace_id 53 | -------------------------------------------------------------------------------- /mytracer/helpers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : helpers 18 | Author : jiaqi.hjq 19 | """ 20 | from opentracing import Reference, ReferenceType 21 | 22 | from mytracer.tracer import MyTracer 23 | 24 | tracer = MyTracer() 25 | 26 | 27 | def new_span(operation_name, **tags): 28 | return tracer.start_span(operation_name, tags=tags) 29 | 30 | 31 | def child_span_of(span, operation_name=None, **tags): 32 | operation_name = operation_name or span.operation_name 33 | return tracer.start_span(operation_name, child_of=span, tags=tags) 34 | 35 | 36 | def follows_span_from(span, operation_name=None, **tags): 37 | operation_name = operation_name or span.operation_name 38 | ref = Reference(ReferenceType.FOLLOWS_FROM, span.context) 39 | return tracer.start_span(operation_name, references=ref, tags=tags) 40 | -------------------------------------------------------------------------------- /mytracer/span.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : span 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 15:34 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 15:34 by jiaqi.hjq init 23 | """ 24 | import time 25 | 26 | 27 | class MySpan(object): 28 | def __init__(self, tracer, context): 29 | self._tracer = tracer 30 | self._context = context 31 | self._tags = dict() 32 | self._logs = dict() 33 | self._start_time = time.time() 34 | self._finish_time = None 35 | self._operation_name = "" 36 | 37 | @property 38 | def context(self): 39 | return self._context 40 | 41 | @property 42 | def operation_name(self): 43 | return self._operation_name 44 | 45 | def set_operation_name(self, operation_name): 46 | self._operation_name = operation_name 47 | 48 | def set_baggage_item(self, key, value): 49 | self._context._baggage[key] = value 50 | 51 | def get_baggage_iterm(self, key): 52 | return self._context.baggage[key] 53 | 54 | def set_tag(self, key, value): 55 | self._tags[key] = value 56 | 57 | def set_start_time(self, starttime): 58 | self._start_time = starttime 59 | 60 | def finish(self): 61 | self._finish_time = time.time() 62 | 63 | def __enter__(self): 64 | self._start_time = time.time() 65 | return self 66 | 67 | def __exit__(self, exc_type, exc_val, exc_tb): 68 | self.finish() 69 | -------------------------------------------------------------------------------- /mytracer/span_context.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : span_context 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 15:33 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 15:33 by jiaqi.hjq init 23 | """ 24 | from ._rpc_id import RpcId 25 | from ._trace_id import TraceId 26 | 27 | 28 | class _ImmutableDict(dict): 29 | def __setitem__(self, key, value): 30 | raise TypeError("This dict is not allowed to modify") 31 | 32 | 33 | class SpanContext(object): 34 | """ 35 | A mapping holding the span related context. 36 | 37 | currently: 38 | 39 | sofaRpcId='0', 40 | sofaTraceId='0123456789abcdef0123456789abcd', 41 | 42 | """ 43 | 44 | def __init__(self, reference=None, **kwargs): 45 | """ 46 | :param reference: contains a type and a context 47 | :param kwargs: 48 | """ 49 | if reference: 50 | origin_span_baggage = reference.referenced_context.baggage.copy() 51 | kwargs['sofaRpcId'] = origin_span_baggage.pop('sofaRpcId').new_by_reference_type(reference.type) 52 | kwargs.update(origin_span_baggage) 53 | else: 54 | kwargs['sofaTraceId'] = TraceId(kwargs.pop('sofaTraceId', None)) 55 | _ids = list(kwargs.pop('sofaRpcId', '1').split('.')) 56 | kwargs['sofaRpcId'] = RpcId(*_ids) 57 | self._baggage = _ImmutableDict(**kwargs) 58 | 59 | @property 60 | def baggage(self): 61 | return self._baggage 62 | 63 | def __str__(self): 64 | return str(self.baggage) 65 | -------------------------------------------------------------------------------- /mytracer/tracer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : mytracer 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/25 16:30 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/25 16:30 by jiaqi.hjq init 23 | """ 24 | import threading 25 | 26 | import opentracing 27 | 28 | from .span import MySpan 29 | from .span_context import SpanContext 30 | 31 | 32 | class MyTracer(opentracing.Tracer): 33 | """ 34 | Tracer is the entry point API between instrumentation code and the 35 | tracing implementation. 36 | 37 | Should be statless 38 | """ 39 | spanCls = MySpan 40 | spanContextCls = SpanContext 41 | _local = threading.local() 42 | _prefix = 'rpc_trace_context.' 43 | 44 | def start_span(self, operation_name, child_of=None, references=None, tags=None, start_time=None): 45 | """Starts and returns a new Span representing a unit of work. 46 | 47 | 48 | Starting a root Span (a Span with no causal references):: 49 | 50 | tracer.start_span('...') 51 | 52 | 53 | Starting a child Span (see also start_child_span()):: 54 | 55 | tracer.start_span( 56 | '...', 57 | child_of=parent_span) 58 | 59 | 60 | Starting a child Span in a more verbose way:: 61 | 62 | tracer.start_span( 63 | '...', 64 | references=[opentracing.child_of(parent_span)]) 65 | 66 | 67 | :param operation_name: name of the operation represented by the new 68 | span from the perspective of the current service. 69 | :param child_of: (optional) a Span or SpanContext instance representing 70 | the parent in a REFERENCE_CHILD_OF Reference. If specified, the 71 | `references` parameter must be omitted. 72 | :param references: (optional) a list of Reference objects that identify 73 | one or more parent SpanContexts. (See the Reference documentation 74 | for detail) 75 | :param tags: an optional dictionary of Span Tags. The caller gives up 76 | ownership of that dictionary, because the Tracer may use it as-is 77 | to avoid extra data copying. 78 | :param start_time: an explicit Span start time as a unix timestamp per 79 | time.time() 80 | 81 | :return: Returns an already-started Span instance. 82 | """ 83 | if isinstance(child_of, SpanContext): 84 | ref = opentracing.child_of(child_of) 85 | elif isinstance(child_of, MySpan): 86 | ref = opentracing.child_of(child_of.context) 87 | elif references: 88 | ref = references if isinstance(references, opentracing.Reference) else references[0] 89 | else: 90 | ref = None 91 | 92 | span = self.spanCls(tracer=self, 93 | context=self.spanContextCls(ref)) 94 | 95 | span.set_operation_name(operation_name) 96 | 97 | _tags = tags or dict() 98 | for k, v in _tags: 99 | span.set_tag(k, v) 100 | if start_time: 101 | span.set_start_time(start_time) 102 | 103 | return span 104 | 105 | def extract(self, format, carrier): 106 | """ 107 | return a span context 108 | :param format: 109 | :param carrier: dict: currently support: 110 | SofaHeader(dict) 111 | :return: 112 | """ 113 | if format != opentracing.Format.TEXT_MAP: 114 | raise opentracing.UnsupportedFormatException() 115 | try: 116 | d = {k.split('.', 1)[1]: carrier[k] for k in carrier if k.startswith(self._prefix)} 117 | return self.spanContextCls(None, **d) 118 | except: 119 | raise opentracing.InvalidCarrierException() 120 | 121 | def inject(self, span_context, format, carrier): 122 | """ 123 | inject span context into a carrier 124 | :param span_context: 125 | :param format: 126 | :param carrier: 127 | :return: 128 | """ 129 | if format != opentracing.Format.TEXT_MAP: 130 | raise opentracing.UnsupportedFormatException() 131 | 132 | for k, v in span_context.baggage.items(): 133 | carrier[self._prefix + k] = str(v) 134 | return carrier 135 | -------------------------------------------------------------------------------- /performance_aio.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : performance 18 | Author : jiaqi.hjq 19 | """ 20 | import asyncio 21 | import logging 22 | import threading 23 | 24 | from concurrent.futures import ProcessPoolExecutor, wait 25 | 26 | from mytracer import SpanContext 27 | 28 | from anthunder import AioListener, BaseService, AioClient 29 | from tests.proto.python.SampleServicePbRequest_pb2 import SampleServicePbRequest 30 | from tests.proto.python.SampleServicePbResult_pb2 import SampleServicePbResult 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | interface = "com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0" 35 | 36 | 37 | ########## 38 | # server process 39 | 40 | class Counter(object): 41 | def __init__(self): 42 | self.count = 0 43 | self._lock = threading.RLock() 44 | 45 | def inc(self): 46 | self.count += 1 47 | 48 | async def print_count(self): 49 | secs = 0 50 | last_count = 0 51 | while True: 52 | await asyncio.sleep(1) 53 | secs += 1 54 | curr_count = self.count 55 | print("{} secs: {} requests: {} r/s".format(secs, curr_count, (curr_count - last_count))) 56 | last_count = curr_count 57 | 58 | 59 | counter = Counter() 60 | 61 | 62 | class CoroExecutor(object): 63 | def __init__(self, max_worker=None): 64 | pass 65 | 66 | def submit(self, func, *args, **kwargs): 67 | return asyncio.ensure_future(self._coro_wrapper(func, *args, **kwargs)) 68 | 69 | async def _coro_wrapper(self, func, *args, **kwargs): 70 | return func(*args, **kwargs) 71 | 72 | 73 | class TestSampleServicePb(BaseService): 74 | def __init__(self, ctx): 75 | super(TestSampleServicePb, self).__init__(ctx) 76 | 77 | def hello(self, bs: bytes): 78 | # add a delay 79 | # time.sleep(randint(50, 300) / 1000) 80 | obj = SampleServicePbRequest() 81 | obj.ParseFromString(bs) 82 | ret = SampleServicePbResult(result=obj.name).SerializeToString() 83 | counter.inc() 84 | return ret 85 | 86 | 87 | def _run_server(): 88 | listener = AioListener(('127.0.0.1', 12201), "test_app") 89 | print("starting") 90 | listener.run_threading() 91 | listener.handler.register_interface(interface, TestSampleServicePb) 92 | listener.handler.executor = CoroExecutor() 93 | asyncio.run_coroutine_threadsafe(counter.print_count(), loop=listener._loop) 94 | 95 | 96 | #### 97 | # fork clients 98 | 99 | def _call(name): 100 | client = AioClient("perftestapp") 101 | client.mesh_service_address = ("127.0.0.1", 12201) 102 | for i in range(100): 103 | try: 104 | ret = client.invoke_sync(interface, "hello", 105 | SampleServicePbRequest(name=str(name)).SerializeToString(), 106 | timeout_ms=1000, spanctx=SpanContext()) 107 | except Exception as e: 108 | pass 109 | print("{} finished".format(name)) 110 | 111 | 112 | def _acall(name): 113 | client = AioClient("perftestapp") 114 | client.mesh_service_address = ("127.0.0.1", 12201) 115 | fs = [client.invoke_async(interface, "hello", 116 | SampleServicePbRequest(name=str(name)).SerializeToString(), 117 | timeout_ms=1000, spanctx=SpanContext()) for i in range(100)] 118 | wait(fs) 119 | 120 | 121 | if __name__ == '__main__': 122 | _run_server() 123 | executor = ProcessPoolExecutor(max_workers=100) 124 | result = executor.map(_call, range(100), timeout=15) 125 | 126 | executor.shutdown(wait=False) 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.13.0 2 | attrs>=18.1.0 3 | opentracing>=1.3.0 4 | requests-mock>=1.5.0 5 | protobuf>=3.5 6 | uvloop>=0.15; python_version >= '3.7' 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : setup.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:36 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:36 by jiaqi.hjq init 23 | """ 24 | import sys 25 | from setuptools import setup, find_packages 26 | 27 | install_requires = [ 28 | "opentracing==1.3.0", 29 | "requests>=2.13.0", 30 | "attrs>=18.1.0", 31 | ] 32 | tests_requires=[ 33 | "requests-mock>=1.5.0", 34 | ] 35 | 36 | with open('README.en.md', 'r', encoding='utf-8') as f: 37 | readme = f.read() 38 | with open('HISTORY.md', 'r', encoding='utf-8') as f: 39 | history = f.read() 40 | 41 | setup( 42 | name='anthunder', 43 | version='0.9', 44 | author='wanderxjtu', 45 | author_email='wanderhuang@gmail.com', 46 | url='https://github.com/alipay/sofa-bolt-python', 47 | packages=find_packages(exclude=["tests.*", "tests"]), 48 | license="Apache License 2.0", 49 | install_requires=install_requires, 50 | include_package_data=True, 51 | test_suite="tests", 52 | tests_requires=tests_requires, 53 | description="an(t)thunder is a sofa-bolt protocol lib.", 54 | long_description=readme + '\n\n' + history, 55 | long_description_content_type="text/markdown", 56 | classifiers=[ 57 | 'Development Status :: 4 - Beta', 58 | 'Intended Audience :: Developers', 59 | 'Natural Language :: English', 60 | 'License :: OSI Approved :: Apache Software License', 61 | 'Programming Language :: Python', 62 | 'Programming Language :: Python :: 3', 63 | 'Programming Language :: Python :: 3.5', 64 | 'Programming Language :: Python :: 3.6', 65 | 'Programming Language :: Python :: 3.7', 66 | 'Programming Language :: Python :: 3.8', 67 | 'Programming Language :: Python :: 3.9', 68 | 'Programming Language :: Python :: Implementation :: CPython', 69 | 'Programming Language :: Python :: Implementation :: PyPy' 70 | ], 71 | ) 72 | -------------------------------------------------------------------------------- /sync_call_demo.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : demo.py 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | 22 | from mytracer import SpanContext 23 | 24 | logging.basicConfig() 25 | 26 | from multiprocessing import Process, freeze_support 27 | import time 28 | from random import randint 29 | 30 | from anthunder import AioListener as Listener, AioClient 31 | from anthunder import BaseService 32 | from anthunder.discovery import LocalRegistry 33 | from anthunder.model import ProviderMetaInfo, SubServiceMeta 34 | 35 | from tests.proto.python.SampleServicePbRequest_pb2 import SampleServicePbRequest 36 | from tests.proto.python.SampleServicePbResult_pb2 import SampleServicePbResult 37 | 38 | 39 | # Here we use LocalRegistry with local address for test purposes. 40 | # see sample script in README for using MosnClient for service discovery 41 | localaddress = '127.0.0.1:12200' 42 | localinterface = "com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0" 43 | provider = ProviderMetaInfo(appName="test_app") 44 | registry = LocalRegistry({localinterface: SubServiceMeta(localaddress, provider)}) 45 | 46 | 47 | class TestSampleServicePb(BaseService): 48 | def __init__(self, ctx, *, server_name): 49 | super().__init__(ctx) 50 | self._name = server_name 51 | 52 | def hello(self, bs): 53 | # add a delay 54 | time.sleep(randint(50, 300) / 100) 55 | 56 | obj = SampleServicePbRequest() 57 | obj.ParseFromString(bs) 58 | print("server: Processing request", obj) 59 | # reference a pre init member 60 | ret = obj.name + self._name 61 | return SampleServicePbResult(result=ret).SerializeToString() 62 | 63 | 64 | def run_server(): 65 | 66 | listener = Listener(localaddress, "test_app") 67 | time.sleep(0.1) 68 | 69 | # some initialize work 70 | server_name = "A_DYNAMIC_NAME" 71 | 72 | listener.register_interface( 73 | localinterface, 74 | provider_meta=provider, 75 | service_cls=TestSampleServicePb, 76 | service_cls_kwargs={"server_name": server_name}) 77 | 78 | listener.run_forever() 79 | 80 | 81 | class ServiceProvider(object): 82 | def __init__(self, client): 83 | self._client = client 84 | 85 | def hello(self, spanctx): 86 | self._client.invoke_sync(spanctx) 87 | 88 | 89 | def run_client(text): 90 | print("client start", text) 91 | spanctx = SpanContext() 92 | 93 | client = AioClient("test_app", service_register=registry) 94 | 95 | content = client.invoke_sync( 96 | localinterface, 97 | "hello", 98 | SampleServicePbRequest(name=text).SerializeToString(), 99 | timeout_ms=5000, 100 | spanctx=spanctx) 101 | print("client", content) 102 | result = SampleServicePbResult() 103 | result.ParseFromString(content) 104 | 105 | print("client", result) 106 | 107 | 108 | if __name__ == "__main__": 109 | freeze_support() 110 | 111 | print("starting server") 112 | server_proc = Process(target=run_server) 113 | server_proc.start() 114 | time.sleep(1) 115 | 116 | print("starting client") 117 | run_client("client1") 118 | run_client("client2") 119 | run_client("client3") 120 | 121 | server_proc.terminate() 122 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:58 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:58 by jiaqi.hjq init 23 | """ -------------------------------------------------------------------------------- /tests/mysockpool_test/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | """ -------------------------------------------------------------------------------- /tests/mysockpool_test/test_poolman.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_connection 18 | Author : jiaqi.hjq 19 | """ 20 | import unittest 21 | 22 | try: 23 | from unittest import mock 24 | except ImportError: 25 | import mock 26 | 27 | from mysockpool.connection import SocketConnection 28 | from mysockpool.connection_pool import ConnectionPool 29 | from mysockpool.exceptions import EmptyPoolError 30 | from mysockpool.pool_manager import PoolManager 31 | 32 | 33 | class TestPoolManager(unittest.TestCase): 34 | def test_pool_man(self): 35 | poolman = PoolManager(2) 36 | conn0 = poolman.connection_pool_from_pool_key( 37 | poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8080)) 38 | self.assertEqual(len(poolman.pools), 1) 39 | conn1 = poolman.connection_pool_from_pool_key( 40 | poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8081)) 41 | self.assertEqual(len(poolman.pools), 2) 42 | conn2 = poolman.connection_pool_from_pool_key( 43 | poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8082)) 44 | # should be limited to 2 45 | self.assertEqual(len(poolman.pools), 2) 46 | conn11 = poolman.connection_pool_from_pool_key( 47 | poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8081)) 48 | # Same one 49 | self.assertIs(conn11, conn1) 50 | conn01 = poolman.connection_pool_from_pool_key( 51 | poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8080)) 52 | # Not the same one 53 | self.assertIsNot(conn01, conn0) 54 | self.assertIsNotNone(conn01.pool) 55 | self.assertIsNone(conn0.pool) 56 | 57 | def test_pool_conn_scope(self): 58 | poolman = PoolManager(2, initial_connections=2) 59 | pool_key = poolman.PoolCls.ConnectionCls.PoolKeyCls(host='127.0.0.1', port=8080) 60 | pool = poolman.connection_pool_from_pool_key(pool_key) 61 | before = pool.num_connections 62 | 63 | with poolman.connection_scope(pool_key) as conn: 64 | self.assertEqual(before - 1, pool.pool.qsize()) 65 | self.assertEqual(before, pool.pool.qsize()) 66 | 67 | @mock.patch("mysockpool.connection_pool.is_connection_dropped", 68 | mock.MagicMock(return_value=False)) 69 | def test_pool(self): 70 | class Handler(object): 71 | def __init__(self, *args, **kwargs): 72 | pass 73 | 74 | pool_key = SocketConnection.PoolKeyCls(host='localhost', port=8081) 75 | pool = ConnectionPool(pool_key, max_connections=10, max_free_connections=4, min_free_connections=2, 76 | initial_connections=3) 77 | self.assertEqual(3, pool.num_connections) 78 | self.assertEqual(3, pool.pool.qsize()) 79 | c0 = pool.get_conn() 80 | self.assertEqual(3, pool.num_connections) 81 | self.assertEqual(2, pool.pool.qsize()) 82 | c1 = pool.get_conn() 83 | self.assertEqual(4, pool.num_connections) 84 | self.assertEqual(2, pool.pool.qsize()) 85 | 86 | conns = [pool.get_conn() for i in range(6)] 87 | with self.assertRaises(EmptyPoolError): 88 | c2 = pool.get_conn() 89 | print(len(conns)) 90 | self.assertEqual(10, pool.num_connections) 91 | [pool.put_conn(conn) for conn in conns] 92 | 93 | self.assertEqual(7, pool.num_connections) 94 | -------------------------------------------------------------------------------- /tests/mytracer_test/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 14:00 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 14:00 by jiaqi.hjq init 23 | """ -------------------------------------------------------------------------------- /tests/mytracer_test/test_helpers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_helpers 18 | Author : jiaqi.hjq 19 | """ 20 | import time 21 | import unittest 22 | 23 | import opentracing 24 | from opentracing import Reference, ReferenceType 25 | 26 | from mytracer._rpc_id import RpcId 27 | from mytracer._trace_id import TraceId 28 | from mytracer.helpers import new_span, child_span_of, follows_span_from 29 | from mytracer.tracer import MyTracer 30 | 31 | 32 | class TestHelpers(unittest.TestCase): 33 | def test_new_span(self): 34 | with new_span("new_span") as span: 35 | # 1 36 | self.assertEqual(span.operation_name, "new_span") 37 | self.assertEqual(span.get_baggage_iterm("sofaRpcId"), RpcId(1)) 38 | 39 | with child_span_of(span) as cspan: 40 | # 1.1 41 | print(cspan.context) 42 | self.assertEqual(cspan.operation_name, "new_span") 43 | self.assertEqual(cspan.get_baggage_iterm("sofaRpcId"), RpcId(1, 1)) 44 | with follows_span_from(cspan, "follow_span") as fspan: 45 | # 1.2 46 | self.assertEqual(fspan.operation_name, "follow_span") 47 | self.assertEqual(fspan.get_baggage_iterm("sofaRpcId"), RpcId(1, 2)) 48 | 49 | with follows_span_from(span, "follow_span") as fspan: 50 | # 2 51 | self.assertEqual(fspan.operation_name, "follow_span") 52 | self.assertEqual(fspan.get_baggage_iterm("sofaRpcId"), RpcId(2)) 53 | 54 | with child_span_of(span, "child_span") as cspan: 55 | # 1.3 56 | print(cspan.context) 57 | self.assertEqual(cspan.operation_name, "child_span") 58 | self.assertEqual(cspan.get_baggage_iterm("sofaRpcId"), RpcId(1, 3)) 59 | 60 | def test_exception_barrier(self): 61 | with self.assertRaises(Exception): 62 | with new_span("new2") as span: 63 | raise Exception("") 64 | -------------------------------------------------------------------------------- /tests/mytracer_test/test_trace_id.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_trace_id 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/28 14:00 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/28 14:00 by jiaqi.hjq init 23 | """ 24 | import unittest 25 | 26 | from mytracer._rpc_id import RpcId 27 | from mytracer._trace_id import TraceId 28 | 29 | 30 | class TestTraceId(unittest.TestCase): 31 | def test_trace_id(self): 32 | self.assertNotEqual(str(TraceId()), str(TraceId())) 33 | id = TraceId() 34 | print(str(id)) 35 | 36 | def test_rpc_id(self): 37 | sid = RpcId() 38 | print(sid) 39 | self.assertEqual(str(sid), '1') 40 | self.assertEqual(sid, RpcId(1)) 41 | sid = sid._new_follows_from() 42 | print(sid) 43 | self.assertEqual(str(sid), '2') 44 | sid = sid._new_child_of() 45 | self.assertEqual(str(sid), '2.1') 46 | 47 | self.assertNotEqual(RpcId(1, 1), RpcId(1, 1, 1)) 48 | self.assertNotEqual(RpcId(1), 1) 49 | self.assertNotEqual(RpcId(1, 1, 2), RpcId(1, 1, 1)) 50 | -------------------------------------------------------------------------------- /tests/mytracer_test/test_tracer.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_tracer 18 | Author : jiaqi.hjq 19 | """ 20 | import time 21 | import unittest 22 | 23 | import opentracing 24 | from opentracing import Reference, ReferenceType 25 | 26 | from mytracer._rpc_id import RpcId 27 | from mytracer._trace_id import TraceId 28 | from mytracer.tracer import MyTracer 29 | 30 | 31 | class TestTracer(unittest.TestCase): 32 | def test_start_span(self): 33 | span = MyTracer().start_span("test_ops", start_time=time.time()) 34 | print(str(span.context)) 35 | self.assertEqual(span.get_baggage_iterm('sofaRpcId'), RpcId(1)) 36 | 37 | childspan = MyTracer().start_span("test_ops", child_of=span) 38 | print(str(childspan.context)) 39 | self.assertEqual(span.get_baggage_iterm('sofaTraceId'), childspan.get_baggage_iterm('sofaTraceId')) 40 | self.assertEqual(childspan.get_baggage_iterm('sofaRpcId'), RpcId(1, 1)) 41 | 42 | followspan = MyTracer().start_span("test_ops", references=Reference(ReferenceType.FOLLOWS_FROM, 43 | childspan.context)) 44 | print(str(followspan.context)) 45 | self.assertEqual(span.get_baggage_iterm('sofaTraceId'), followspan.get_baggage_iterm('sofaTraceId')) 46 | self.assertEqual(followspan.get_baggage_iterm('sofaRpcId'), RpcId(1, 2)) 47 | 48 | def test_extract_inject(self): 49 | carrier = {"rpc_trace_context.test": "value1", 50 | "rpc_trace_context.sofaTraceId": "testTId", 51 | "rpc_trace_context.sofaRpcId": "1.1", 52 | "pc_trace_context.sofaRpcId": "testRId", 53 | } 54 | spanctx = MyTracer().extract(opentracing.Format.TEXT_MAP, carrier) 55 | self.assertEqual(spanctx.baggage['sofaRpcId'], RpcId(1, 1)) 56 | self.assertEqual(spanctx.baggage['sofaTraceId'], TraceId("testTId")) 57 | 58 | print(str(spanctx)) 59 | 60 | with MyTracer().start_span("test_ops2", child_of=spanctx) as childspan: 61 | carrier = dict() 62 | MyTracer().inject(childspan.context, opentracing.Format.TEXT_MAP, carrier) 63 | print(carrier) 64 | print(str()) 65 | -------------------------------------------------------------------------------- /tests/proto/ComplexServicePbRequest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.request"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | import "SubServicePbRequest.proto"; 6 | message ComplexServicePbRequest { 7 | string name = 1; 8 | SubServicePbRequest subServicePbRequest = 2; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /tests/proto/ComplexServicePbResult.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.result"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | import "SubServicePbResult.proto"; 6 | 7 | message ComplexServicePbResult { 8 | string result = 1; 9 | SubServicePbResult subServicePbResult = 2; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /tests/proto/SampleServicePbRequest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.request"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | 6 | message SampleServicePbRequest { 7 | string name = 1; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/proto/SampleServicePbResult.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.result"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | 6 | message SampleServicePbResult { 7 | string result = 1; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/proto/SubServicePbRequest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.request"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | 6 | message SubServicePbRequest { 7 | string subName = 1; 8 | 9 | } 10 | 11 | -------------------------------------------------------------------------------- /tests/proto/SubServicePbResult.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_package = "com.alipay.rpc.common.service.facade.pb.result"; 3 | option java_multiple_files = true; 4 | option go_package = "com.alipay.rpc.common.service.facade.pb"; 5 | 6 | message SubServicePbResult { 7 | string subResult = 1; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/proto/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : __init__.py 18 | Author : jiaqi.hjq 19 | Create Time : 2018/5/21 15:38 20 | Description : describe the main function of this file 21 | Change Activity: 22 | version0 : 2018/5/21 15:38 by jiaqi.hjq init 23 | """ -------------------------------------------------------------------------------- /tests/proto/python/SampleService.py: -------------------------------------------------------------------------------- 1 | # automatically generated file, edit carefully!! 2 | from anthunder.discovery.local import FixedAddressRegistry 3 | from anthunder import Client, Request 4 | 5 | 6 | class SampleService(Request): 7 | CLIENT = Client("anthunderTestAppName", 8 | service_register=FixedAddressRegistry("127.0.0.1:12200")) 9 | 10 | def hello(self, **kwargs): 11 | from .SampleServicePbRequest_pb2 import SampleServicePbRequest as RequestMessage 12 | from .SampleServicePbResult_pb2 import SampleServicePbResult as ResultMessage 13 | 14 | req = RequestMessage(**kwargs).SerializeToString() 15 | resp = self.CLIENT.invoke_sync( 16 | "com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0", 17 | "hello", 18 | req, 19 | timeout_ms=500, 20 | target_app="bar1", 21 | spanctx=self.spanctx) 22 | result = ResultMessage() 23 | result.ParseFromString(resp) 24 | return result 25 | 26 | def helloComplex(self, **kwargs): 27 | from .ComplexServicePbRequest_pb2 import ComplexServicePbRequest as RequestMessage 28 | from .ComplexServicePbResult_pb2 import ComplexServicePbResult as ResultMessage 29 | 30 | req = RequestMessage(**kwargs).SerializeToString() 31 | resp = self.CLIENT.invoke_sync( 32 | "com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0", 33 | "helloComplex", 34 | req, 35 | timeout_ms=500, 36 | target_app="bar1", 37 | spanctx=self.spanctx) 38 | result = ResultMessage() 39 | result.ParseFromString(resp) 40 | return result -------------------------------------------------------------------------------- /tests/proto/python/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.dirname(__file__)) -------------------------------------------------------------------------------- /tests/test_aio_listener_client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_server 18 | Author : jiaqi.hjq 19 | """ 20 | import unittest 21 | 22 | from anthunder.command.heartbeat import HeartbeatRequest, HeartbeatResponse 23 | from anthunder.protocol.constants import RESPSTATUS 24 | from anthunder.discovery.local import FixedAddressRegistry 25 | 26 | import concurrent.futures 27 | import functools 28 | import threading 29 | import time 30 | from unittest import mock 31 | from random import randint 32 | 33 | from mytracer import SpanContext 34 | 35 | from anthunder import AioClient, AioListener, BaseService 36 | from tests.proto.python import SampleService 37 | from tests.proto.python.SampleServicePbRequest_pb2 import SampleServicePbRequest 38 | from tests.proto.python.SampleServicePbResult_pb2 import SampleServicePbResult 39 | try: 40 | from asyncio import all_tasks 41 | except ImportError: 42 | from asyncio import Task 43 | all_tasks = Task.all_tasks 44 | 45 | 46 | class TestListener(unittest.TestCase): 47 | @classmethod 48 | def setUpClass(cls): 49 | lsn = AioListener(('127.0.0.1', 12200), "test_app") 50 | lsn.run_threading() 51 | cls.listener = lsn 52 | cls.interface = "com.alipay.rpc.common.service.facade.pb.SampleServicePb:1.0" 53 | time.sleep(0.1) 54 | 55 | class TestSampleServicePb(BaseService): 56 | def __init__(self, ctx, *, seed=None): 57 | self._seed = seed 58 | super().__init__(ctx) 59 | 60 | def hello(self, bs): 61 | # add a delay 62 | time.sleep(self._seed) 63 | 64 | obj = SampleServicePbRequest() 65 | obj.ParseFromString(bs) 66 | print("Processing Request", obj) 67 | return SampleServicePbResult( 68 | result=obj.name).SerializeToString() 69 | 70 | cls.listener.handler.register_interface(cls.interface, 71 | TestSampleServicePb, 72 | seed=randint(50, 300) / 1000) 73 | 74 | def test_server(self): 75 | # mocked, will call to localhost 76 | threading.Thread(target=SampleService.SampleService( 77 | SpanContext()).hello, 78 | kwargs=dict(name="abcde-test0")).start() 79 | result = SampleService.SampleService( 80 | SpanContext()).hello(name="abcde-test") 81 | 82 | print(result) 83 | self.assertEqual(result.result, "abcde-test") 84 | 85 | def test_aio_client(self): 86 | client = AioClient( 87 | "anthunderTestApp", 88 | service_register=FixedAddressRegistry("127.0.0.1:12200")) 89 | _result = list() 90 | _ts = list() 91 | 92 | def _call(name): 93 | content = client.invoke_sync( 94 | self.interface, 95 | "hello", 96 | SampleServicePbRequest(name=str(name)).SerializeToString(), 97 | timeout_ms=5000, 98 | spanctx=SpanContext()) 99 | result = SampleServicePbResult() 100 | result.ParseFromString(content) 101 | print(result.result == str(name)) 102 | _result.append(result.result == str(name)) 103 | 104 | for i in range(10): 105 | t = threading.Thread(target=_call, args=(i, )) 106 | t.start() 107 | _ts.append(t) 108 | 109 | for t in _ts: 110 | t.join() 111 | 112 | for task in all_tasks(client._loop): 113 | print(task) 114 | # _recv_response * 1 115 | # _heartbeat_timer * 1 116 | self.assertLessEqual(len(all_tasks(client._loop)), 2) 117 | self.assertEqual(len(_result), 10) 118 | self.assertTrue(all(_result)) 119 | 120 | def test_heartbeat(self): 121 | pkg = HeartbeatRequest.new_request() 122 | from socket import socket 123 | with socket() as s: 124 | s.connect(('127.0.0.1', 12200)) 125 | s.send(pkg.to_stream()) 126 | buf = b'' 127 | buf += s.recv(1024) 128 | 129 | resp = HeartbeatResponse.from_stream(buf) 130 | print(resp) 131 | 132 | self.assertEqual(resp.request_id, pkg.request_id) 133 | self.assertEqual(resp.respstatus, RESPSTATUS.SUCCESS) 134 | 135 | def test_aio_client_async(self): 136 | print("async client") 137 | client = AioClient( 138 | "anthunderTestApp", 139 | service_register=FixedAddressRegistry("127.0.0.1:12200")) 140 | _result = list() 141 | 142 | def _cb(content, expect): 143 | result = SampleServicePbResult() 144 | result.ParseFromString(content) 145 | print(expect == result.result) 146 | _result.append(expect == result.result) 147 | 148 | def _acall(name): 149 | return client.invoke_async( 150 | self.interface, 151 | "hello", 152 | SampleServicePbRequest(name="async" + 153 | str(name)).SerializeToString(), 154 | timeout_ms=500, 155 | callback=functools.partial(_cb, expect="async" + str(name)), 156 | spanctx=SpanContext()) 157 | 158 | fs = [_acall(i) for i in range(10)] 159 | concurrent.futures.wait(fs, timeout=1.5) 160 | time.sleep(0.1) 161 | self.assertEqual(len(_result), 10) 162 | self.assertTrue(all(_result)) 163 | 164 | @classmethod 165 | def tearDownClass(cls): 166 | cls.listener.shutdown() 167 | 168 | 169 | if __name__ == '__main__': 170 | unittest.main() 171 | -------------------------------------------------------------------------------- /tests/test_bolt_package.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_bolt_package 18 | Author : jiaqi.hjq 19 | Create Time : 2018/4/28 11:58 20 | Description : description what the main function of this file 21 | Change Activity: 22 | version0 : 2018/4/28 11:58 by jiaqi.hjq init 23 | """ 24 | import unittest 25 | 26 | from anthunder.command.heartbeat import HeartbeatRequest, HeartbeatResponse 27 | from anthunder.protocol import BoltResponse, BoltRequest, SofaHeader 28 | from anthunder.protocol._sofa_header import _str_to_bytes_with_len, _bytes_to_str 29 | from anthunder.protocol.constants import PTYPE, CMDCODE, RESPSTATUS 30 | from .proto.python import SampleServicePbResult_pb2, SampleServicePbRequest_pb2 31 | 32 | 33 | class TestBoltPackage(unittest.TestCase): 34 | def test_str_to_bytes_with_len(self): 35 | s = "abcdefg" 36 | bs = _str_to_bytes_with_len(s) 37 | print(bs) 38 | self.assertEqual(bs, b'\x00\x00\x00\x07abcdefg') 39 | 40 | def test_repr(self): 41 | p = BoltResponse(SofaHeader(a='1', b='2'), b"cdefgab", ptype=PTYPE.ONEWAY, request_id=0, 42 | cmdcode=CMDCODE.HEARTBEAT, 43 | respstatus=1) 44 | print(p) 45 | s = p.to_stream() 46 | pr = BoltResponse.from_stream(s) 47 | self.assertNotEqual(id(p), id(pr)) 48 | self.assertEqual(p.header, pr.header) 49 | self.assertEqual(p.content, pr.content) 50 | self.assertEqual(p.cmdcode, pr.cmdcode) 51 | self.assertEqual(p.request_id, pr.request_id) 52 | print(pr) 53 | 54 | p = BoltRequest(SofaHeader(a='1', b='2'), b"jklmnhi", ptype=PTYPE.ONEWAY, request_id=0, 55 | cmdcode=CMDCODE.HEARTBEAT, 56 | timeout=-1) 57 | print(p) 58 | s = p.to_stream() 59 | pr = BoltRequest.from_stream(s) 60 | self.assertNotEqual(id(p), id(pr)) 61 | self.assertEqual(p.header, pr.header) 62 | self.assertEqual(p.content, pr.content) 63 | self.assertEqual(p.cmdcode, pr.cmdcode) 64 | self.assertEqual(p.request_id, pr.request_id) 65 | print(pr) 66 | 67 | def test_header(self): 68 | h = SofaHeader(keya='key1', keyabcxcs='key2') 69 | b = h.to_bytes() 70 | self.assertEqual(len(h), len(b)) 71 | print(b) 72 | h_recover = SofaHeader.from_bytes(b) 73 | print(h_recover) 74 | self.assertEqual(h, h_recover) 75 | 76 | def test_from_stream(self): 77 | bs = b'\x01\x00\x00\x02\x01\x00\x00\x00\x84\x0b\x00\x00\x00\x2e\x00\x00\x00\x00\x00\x03\x63\x6f\x6d\x2e\x61\x6c\x69\x70\x61\x79\x2e\x73\x6f\x66\x61\x2e\x72\x70\x63\x2e\x63\x6f\x72\x65\x2e\x72\x65\x73\x70\x6f\x6e\x73\x65\x2e\x53\x6f\x66\x61\x52\x65\x73\x70\x6f\x6e\x73\x65\x0a\x01\x61' 78 | p = BoltResponse.from_stream(bs) 79 | self.assertEqual(p.body_len, len(bs) - p.bolt_header_size()) 80 | print(p.content) 81 | re = SampleServicePbResult_pb2.SampleServicePbResult() 82 | re.ParseFromString(p.content) 83 | # print(p) 84 | print(re.result) 85 | self.assertEqual(re.result, 'a') 86 | 87 | bs = b'\x01\x01\x00\x01\x01\x00\x00\x00\x6d\x0b\x00\x00\x00\x64\x00\x2c\x02\xe6\x00\x00\x00\x03\x63\x6f\x6d\x2e\x61\x6c\x69\x70\x61\x79\x2e\x73\x6f\x66\x61\x2e\x72\x70\x63\x2e\x63\x6f\x72\x65\x2e\x72\x65\x71\x75\x65\x73\x74\x2e\x53\x6f\x66\x61\x52\x65\x71\x75\x65\x73\x74\x00\x00\x00\x18\x73\x6f\x66\x61\x5f\x68\x65\x61\x64\x5f\x74\x61\x72\x67\x65\x74\x5f\x73\x65\x72\x76\x69\x63\x65\x00\x00\x00\x3b\x63\x6f\x6d\x2e\x61\x6c\x69\x70\x61\x79\x2e\x72\x70\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x73\x65\x72\x76\x69\x63\x65\x2e\x66\x61\x63\x61\x64\x65\x2e\x70\x62\x2e\x53\x61\x6d\x70\x6c\x65\x53\x65\x72\x76\x69\x63\x65\x50\x62\x3a\x31\x2e\x30\x00\x00\x00\x1b\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x52\x70\x63\x49\x64\x00\x00\x00\x01\x30\x00\x00\x00\x1f\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x7a\x70\x72\x6f\x78\x79\x54\x69\x6d\x65\x6f\x75\x74\x00\x00\x00\x03\x31\x30\x30\x00\x00\x00\x1d\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x54\x72\x61\x63\x65\x49\x64\x00\x00\x00\x1e\x30\x62\x61\x36\x31\x36\x61\x31\x31\x35\x32\x33\x32\x35\x31\x38\x31\x39\x31\x31\x34\x31\x30\x30\x32\x31\x38\x37\x33\x38\x00\x00\x00\x1f\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x43\x61\x6c\x6c\x65\x72\x49\x64\x63\x00\x00\x00\x03\x64\x65\x76\x00\x00\x00\x1e\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x43\x61\x6c\x6c\x65\x72\x49\x70\x00\x00\x00\x0d\x31\x31\x2e\x31\x36\x36\x2e\x32\x32\x2e\x31\x36\x31\x00\x00\x00\x1e\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x50\x65\x6e\x41\x74\x74\x72\x73\x00\x00\x00\x00\x00\x00\x00\x1b\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x7a\x70\x72\x6f\x78\x79\x55\x49\x44\x00\x00\x00\x00\x00\x00\x00\x20\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x43\x61\x6c\x6c\x65\x72\x5a\x6f\x6e\x65\x00\x00\x00\x05\x47\x5a\x30\x30\x42\x00\x00\x00\x14\x73\x6f\x66\x61\x5f\x68\x65\x61\x64\x5f\x74\x61\x72\x67\x65\x74\x5f\x61\x70\x70\x00\x00\x00\x04\x62\x61\x72\x31\x00\x00\x00\x07\x73\x65\x72\x76\x69\x63\x65\x00\x00\x00\x3b\x63\x6f\x6d\x2e\x61\x6c\x69\x70\x61\x79\x2e\x72\x70\x63\x2e\x63\x6f\x6d\x6d\x6f\x6e\x2e\x73\x65\x72\x76\x69\x63\x65\x2e\x66\x61\x63\x61\x64\x65\x2e\x70\x62\x2e\x53\x61\x6d\x70\x6c\x65\x53\x65\x72\x76\x69\x63\x65\x50\x62\x3a\x31\x2e\x30\x00\x00\x00\x19\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x45\x6c\x61\x73\x74\x69\x63\x00\x00\x00\x01\x46\x00\x00\x00\x1d\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x79\x73\x50\x65\x6e\x41\x74\x74\x72\x73\x00\x00\x00\x00\x00\x00\x00\x22\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x7a\x70\x72\x6f\x78\x79\x54\x61\x72\x67\x65\x74\x5a\x6f\x6e\x65\x00\x00\x00\x00\x00\x00\x00\x1f\x72\x70\x63\x5f\x74\x72\x61\x63\x65\x5f\x63\x6f\x6e\x74\x65\x78\x74\x2e\x73\x6f\x66\x61\x43\x61\x6c\x6c\x65\x72\x41\x70\x70\x00\x00\x00\x03\x66\x6f\x6f\x00\x00\x00\x15\x73\x6f\x66\x61\x5f\x68\x65\x61\x64\x5f\x6d\x65\x74\x68\x6f\x64\x5f\x6e\x61\x6d\x65\x00\x00\x00\x05\x68\x65\x6c\x6c\x6f\x0a\x01\x61' 88 | p = BoltRequest.from_stream(bs) 89 | print(p.header) 90 | re = SampleServicePbRequest_pb2.SampleServicePbRequest() 91 | re.ParseFromString(p.content) 92 | print(re) 93 | self.assertEqual(re.name, "a") 94 | 95 | def test_heartbeat(self): 96 | pkg = HeartbeatRequest.new_request() 97 | print(pkg) 98 | print(pkg.to_stream()) 99 | print(pkg.header) 100 | self.assertEqual(pkg.class_len, 0) 101 | self.assertEqual(pkg.header_len, 0) 102 | self.assertEqual(pkg.content_len, 0) 103 | self.assertEqual(pkg.cmdcode, CMDCODE.HEARTBEAT) 104 | 105 | resp = HeartbeatResponse.response_to(pkg.request_id) 106 | print(resp) 107 | print(resp.to_stream()) 108 | self.assertEqual(resp.class_len, 0) 109 | self.assertEqual(resp.header_len, 0) 110 | self.assertEqual(resp.content_len, 0) 111 | self.assertEqual(resp.cmdcode, CMDCODE.HEARTBEAT) 112 | self.assertEqual(resp.respstatus, RESPSTATUS.SUCCESS) 113 | self.assertEqual(pkg.request_id, resp.request_id) 114 | 115 | 116 | if __name__ == '__main__': 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_helpers 18 | Author : jiaqi.hjq 19 | """ 20 | import logging 21 | import unittest 22 | 23 | from anthunder.helpers.immutable_dict import ImmutableValueDict 24 | from anthunder.helpers.request_id import RequestId 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class TestHelpers(unittest.TestCase): 30 | def test_request_id(self): 31 | rid = next(RequestId) 32 | print(rid) 33 | self.assertTrue(0 <= rid <= 0x7fffffff) 34 | 35 | def test_immutabledict(self): 36 | d = ImmutableValueDict() 37 | d['a'] = 'b' 38 | self.assertEqual(d['a'], 'b') 39 | with self.assertRaises(KeyError): 40 | d['a'] = 'a' 41 | self.assertEqual(d['a'], 'b') 42 | d['b'] = 'a' 43 | self.assertEqual(d['b'], 'a') 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/test_mesh_client.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Copyright (c) 2018-present, Ant Financial Service Group 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | ------------------------------------------------------ 17 | File Name : test_mesh_client 18 | Author : jiaqi.hjq 19 | """ 20 | import json 21 | import unittest 22 | import attr 23 | import requests_mock 24 | 25 | from anthunder.discovery.mosn import MosnClient, ApplicationInfo 26 | from anthunder.model.service import ProviderMetaInfo 27 | 28 | 29 | class TestMeshClient(unittest.TestCase): 30 | interface = "com.alipay.pybolt.test:1.0" 31 | provider = ProviderMetaInfo(protocol="1", 32 | version="4.0", 33 | serializeType="protobuf", 34 | appName="pybolt_test_app") 35 | subinterface = "com.alipay.pybolt.subtest:1.0" 36 | appmeta = ApplicationInfo("pybolt_test_app", "", "", "") 37 | 38 | @requests_mock.Mocker() 39 | def test_start(self, session_mock): 40 | session_mock.post('http://127.0.0.1:13330/configs/application', 41 | text=json.dumps(dict(success=True))) 42 | mesh = MosnClient() 43 | mesh.startup(self.appmeta) 44 | 45 | @requests_mock.Mocker() 46 | def test_pub(self, session_mock): 47 | session_mock.post('http://127.0.0.1:13330/configs/application', 48 | text=json.dumps(dict(success=True))) 49 | session_mock.post('http://127.0.0.1:13330/services/publish', 50 | text=json.dumps(dict(success=True))) 51 | print(attr.asdict(self.provider)) 52 | mesh = MosnClient() 53 | mesh.startup(self.appmeta) 54 | mesh.publish(("127.0.0.1", 12200), 55 | self.interface, 56 | provider=self.provider) 57 | 58 | @requests_mock.Mocker() 59 | def test_subscribe(self, session_mock): 60 | session_mock.post('http://127.0.0.1:13330/configs/application', 61 | text=json.dumps(dict(success=True))) 62 | session_mock.post('http://127.0.0.1:13330/services/subscribe', 63 | text=json.dumps(dict(success=True, datas=["127.0.0.1:12220?p=1&v=4.0&_SERIALIZETYPE=hessian2&app_name=someapp"]))) 64 | mesh = MosnClient() 65 | mesh.startup(self.appmeta) 66 | mesh.subscribe(self.subinterface) 67 | 68 | @requests_mock.Mocker() 69 | def test_unpublish(self, session_mock): 70 | session_mock.post('http://127.0.0.1:13330/configs/application', 71 | text=json.dumps(dict(success=True))) 72 | session_mock.post('http://127.0.0.1:13330/services/unpublish', 73 | text=json.dumps(dict(success=True))) 74 | mesh = MosnClient() 75 | mesh.startup(self.appmeta) 76 | mesh.unpublish(self.interface) 77 | 78 | @requests_mock.Mocker() 79 | def test_unsubscribe(self, session_mock): 80 | print(session_mock) 81 | session_mock.post('http://127.0.0.1:13330/configs/application', 82 | text=json.dumps(dict(success=True))) 83 | session_mock.post('http://127.0.0.1:13330/services/unsubscribe', 84 | text=json.dumps(dict(success=True))) 85 | mesh = MosnClient() 86 | mesh.startup(self.appmeta) 87 | mesh.unsubscribe(self.subinterface) 88 | 89 | 90 | if __name__ == '__main__': 91 | unittest.main() 92 | --------------------------------------------------------------------------------