{"id":16701969,"url":"https://github.com/lpgauth/shackle","last_synced_at":"2025-04-04T10:05:14.420Z","repository":{"id":34197341,"uuid":"38052515","full_name":"lpgauth/shackle","owner":"lpgauth","description":"High-Performance Erlang Network Client Framework","archived":false,"fork":false,"pushed_at":"2024-03-18T18:09:49.000Z","size":4891,"stargazers_count":188,"open_issues_count":2,"forks_count":45,"subscribers_count":21,"default_branch":"master","last_synced_at":"2024-10-13T18:46:25.291Z","etag":null,"topics":["client","erlang","framework","high-performance","ssl","tcp","udp"],"latest_commit_sha":null,"homepage":"","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lpgauth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-06-25T13:56:24.000Z","updated_at":"2024-07-21T15:56:09.000Z","dependencies_parsed_at":"2022-09-26T16:20:30.927Z","dependency_job_id":"b272f926-ee57-40a0-a72b-0e513ddedbe1","html_url":"https://github.com/lpgauth/shackle","commit_stats":{"total_commits":298,"total_committers":11,"mean_commits":27.09090909090909,"dds":"0.046979865771812124","last_synced_commit":"0e7ac035e9022a1b57ef7e8ac5f824bf26fb2007"},"previous_names":[],"tags_count":52,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lpgauth%2Fshackle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lpgauth%2Fshackle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lpgauth%2Fshackle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lpgauth%2Fshackle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lpgauth","download_url":"https://codeload.github.com/lpgauth/shackle/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247150202,"owners_count":20892098,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["client","erlang","framework","high-performance","ssl","tcp","udp"],"created_at":"2024-10-12T18:46:20.078Z","updated_at":"2025-04-04T10:05:14.398Z","avatar_url":"https://github.com/lpgauth.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# shackle\n\nHigh-Performance Erlang Network Client Framework\n\n![Build Status](https://github.com/lpgauth/shackle/workflows/Erlang%20CI/badge.svg)\n\n#### Requirements\n\n* Erlang 19.0+\n\n#### Features\n\n* Backpressure via backlog (OOM protection)\n* Fast pool implementation (random, round_robin)\n* Managed timeouts\n* Multi-protocol support (SSL / TCP / UDP)\n* Performance-optimized\n* Request pipelining\n* Smart reconnect mechanism (exponential backoff)\n\n#### Framework goals\n\n* Reusability\n* Speed\n* Concurrency\n* Safety\n\n## How-to\n\n#### Implementing a client\n\n```erlang\n-behavior(shackle_client).\n-export([\n    init/0,\n    setup/2,\n    handle_request/2,\n    handle_data/2,\n    terminate/1\n]).\n\n-record(state, {\n    buffer =       \u003c\u003c\u003e\u003e :: binary(),\n    request_counter = 0 :: non_neg_integer()\n}).\n\n-spec init(Options :: term()) -\u003e\n    {ok, State :: term()} |\n    {error, Reason :: term()}.\n\ninit(_Options) -\u003e\n    {ok, #state {}}.\n\n-spec setup(Socket :: inet:socket(), State :: term()) -\u003e\n    {ok, State :: term()} |\n    {error, Reason :: term(), State :: term()}.\n\nsetup(Socket, State) -\u003e\n    case gen_tcp:send(Socket, \u003c\u003c\"INIT\"\u003e\u003e) of\n        ok -\u003e\n            case gen_tcp:recv(Socket, 0) of\n                {ok, \u003c\u003c\"OK\"\u003e\u003e} -\u003e\n                    {ok, State};\n                {error, Reason} -\u003e\n                    {error, Reason, State}\n            end;\n        {error, Reason} -\u003e\n            {error, Reason, State}\n    end.\n\n-spec handle_request(Request :: term(), State :: term()) -\u003e\n    {ok, RequestId :: external_request_id(), Data :: iodata(), State :: term()}.\n\nhandle_request(noop,  State) -\u003e\n    Data = arithmetic_protocol:request(0, noop, 0, 0),\n\n    {ok, undefined, Data, State};\nhandle_request({Operation, A, B}, #state {\n        request_counter = RequestCounter\n    } = State) -\u003e\n\n    RequestId = request_id(RequestCounter),\n    Data = request(RequestId, Operation, A, B),\n\n    {ok, RequestId, Data, State#state {\n        request_counter = RequestCounter + 1\n    }}.\n\n-spec handle_data(Data :: binary(), State :: term()) -\u003e\n    {ok, [{RequestId :: external_request_id(), Reply :: term()}], State :: term()}.\n\nhandle_data(Data, #state {\n        buffer = Buffer\n    } = State) -\u003e\n\n    Data2 = \u003c\u003cBuffer/binary, Data/binary\u003e\u003e,\n    {Replies, Buffer2} = parse_replies(Data2, []),\n\n    {ok, Replies, State#state {\n        buffer = Buffer2\n    }}.\n\n-spec terminate(State :: term()) -\u003e ok.\n\nterminate(_State) -\u003e ok.\n```\n\n#### Starting client pool\n\n```erlang\nshackle_pool:start(shackle_pool:name(), client(), client_options(), pool_options())\n```\n\n##### client_options:\n\n\u003ctable width=\"100%\"\u003e\n  \u003ctheader\u003e\n    \u003cth\u003eName\u003c/th\u003e\n    \u003cth\u003eType\u003c/th\u003e\n    \u003cth\u003eDefault\u003c/th\u003e\n    \u003cth\u003eDescription\u003c/th\u003e\n  \u003c/theader\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eaddress\u003c/td\u003e\n    \u003ctd\u003einet:ip_address() | inet:hostname()\u003c/td\u003e\n    \u003ctd\u003e\"127.0.0.1\"\u003c/td\u003e\n    \u003ctd\u003eserver address (formerly ip)\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eport\u003c/td\u003e\n    \u003ctd\u003einet:port_number()\u003c/td\u003e\n    \u003ctd\u003eundefined\u003c/td\u003e\n    \u003ctd\u003eserver port\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003eprotocol\u003c/td\u003e\n    \u003ctd\u003eshackle_tcp | shackle_udp | shackle_ssl\u003c/td\u003e\n    \u003ctd\u003eshackle_tcp\u003c/td\u003e\n    \u003ctd\u003eserver protocol\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ereconnect\u003c/td\u003e\n    \u003ctd\u003eboolean()\u003c/td\u003e\n    \u003ctd\u003etrue\u003c/td\u003e\n    \u003ctd\u003ereconnect closed connections\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ereconnect_time_max\u003c/td\u003e\n    \u003ctd\u003epos_integer() | infinity\u003c/td\u003e\n    \u003ctd\u003e120000\u003c/td\u003e\n    \u003ctd\u003emaximum reconnect time in milliseconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ereconnect_time_min\u003c/td\u003e\n    \u003ctd\u003epos_integer()\u003c/td\u003e\n    \u003ctd\u003e1000\u003c/td\u003e\n    \u003ctd\u003eminimum reconnect time in milliseconds\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003esocket_options\u003c/td\u003e\n    \u003ctd\u003e[gen_tcp:connect_option() | gen_udp:option()]\u003c/td\u003e\n    \u003ctd\u003e[]\u003c/td\u003e\n    \u003ctd\u003eoptions passed to the socket\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n##### pool_options:\n\n\u003ctable width=\"100%\"\u003e\n  \u003ctheader\u003e\n    \u003cth\u003eName\u003c/th\u003e\n    \u003cth\u003eType\u003c/th\u003e\n    \u003cth\u003eDefault\u003c/th\u003e\n    \u003cth\u003eDescription\u003c/th\u003e\n  \u003c/theader\u003e\n  \u003ctr\u003e\n    \u003ctd\u003ebacklog_size\u003c/td\u003e\n    \u003ctd\u003epos_integer() | infinity\u003c/td\u003e\n    \u003ctd\u003e1024\u003c/td\u003e\n    \u003ctd\u003emaximum number of concurrent requests per connection\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003emax_retries\u003c/td\u003e\n    \u003ctd\u003enon_neg_integer()\u003c/td\u003e\n    \u003ctd\u003e3\u003c/td\u003e\n    \u003ctd\u003emaximum number of tries to find an active server\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003epool_size\u003c/td\u003e\n    \u003ctd\u003epos_integer()\u003c/td\u003e\n    \u003ctd\u003e16\u003c/td\u003e\n    \u003ctd\u003enumber of connections\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003epool_strategy\u003c/td\u003e\n    \u003ctd\u003erandom | round_robin\u003c/td\u003e\n    \u003ctd\u003erandom\u003c/td\u003e\n    \u003ctd\u003econnection selection strategy\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n#### Calling / Casting client\n\n```erlang\n1\u003e shackle:call(pool_name, {get, \u003c\u003c\"test\"\u003e\u003e}).\n{ok, \u003c\u003c\"bar\"\u003e\u003e}\n\n2\u003e {ok, ReqId} = shackle:cast(pool_name, {get, \u003c\u003c\"foo\"\u003e\u003e}, 500).\n{ok, {anchor, anchor_client, #Ref\u003c0.0.0.2407\u003e}}\n\n3\u003e shackle:receive_response(ReqId).\n{ok, \u003c\u003c\"bar\"\u003e\u003e}\n```\n\n## Telemetry\nShackle integrates with the backend-agnostic [telemetry](https://hexdocs.pm/telemetry/) library. See `shackle_telemetry` for the list of telemetry events that shackle can emit.\n\n## Tests\n\n```makefile\nmake dialyzer\nmake eunit\nmake xref\n```\n\n## Performance testing\n\nTo run performance testing targets you must first start the server:\n\n```\n./bin/server.sh\n```\n\nThen you can run the `bench` or `profile` target:\n\n```makefile\nmake bench\nmake profile\n```\n\n## Clients\n\n\u003ctable width=\"100%\"\u003e\n  \u003ctheader\u003e\n    \u003cth\u003eName\u003c/th\u003e\n    \u003cth\u003eDescription\u003c/th\u003e\n  \u003c/theader\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/lpgauth/anchor\"\u003eanchor\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eMemcached Client\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/vsavkov/aspike-node\"\u003easpike-node\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eAerospike Client\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/lpgauth/buoy\"\u003ebuoy\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eHTTP 1.1 Client\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/lpgauth/flare\"\u003eflare\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eKafka Producer\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/lpgauth/marina\"\u003emarina\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eCassandra CQL Client\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003ca href=\"https://github.com/lpgauth/statsderl\"\u003estatsderl\u003c/a\u003e\u003c/td\u003e\n    \u003ctd\u003eStatsD Client\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n## License\n\n```license\nThe MIT License (MIT)\n\nCopyright (c) 2015-2023 Louis-Philippe Gauthier\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flpgauth%2Fshackle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flpgauth%2Fshackle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flpgauth%2Fshackle/lists"}