{"id":16531345,"url":"https://github.com/hamishforbes/zedcup","last_synced_at":"2026-04-27T18:33:07.560Z","repository":{"id":143139904,"uuid":"142029139","full_name":"hamishforbes/zedcup","owner":"hamishforbes","description":"Zero Conf Upstream load balancing and failover for Openresty and Consul","archived":false,"fork":false,"pushed_at":"2018-09-05T14:30:12.000Z","size":302,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-06-11T13:17:02.431Z","etag":null,"topics":["consul","load-balancer","lua","openresty"],"latest_commit_sha":null,"homepage":null,"language":"Perl","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/hamishforbes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2018-07-23T14:54:36.000Z","updated_at":"2020-09-14T06:20:27.000Z","dependencies_parsed_at":null,"dependency_job_id":"899c853d-ef40-4e24-8f26-1657a50ec0ef","html_url":"https://github.com/hamishforbes/zedcup","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hamishforbes/zedcup","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hamishforbes%2Fzedcup","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hamishforbes%2Fzedcup/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hamishforbes%2Fzedcup/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hamishforbes%2Fzedcup/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hamishforbes","download_url":"https://codeload.github.com/hamishforbes/zedcup/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hamishforbes%2Fzedcup/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32349611,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-27T17:12:42.749Z","status":"ssl_error","status_checked_at":"2026-04-27T17:12:41.658Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["consul","load-balancer","lua","openresty"],"created_at":"2024-10-11T18:08:34.713Z","updated_at":"2026-04-27T18:33:07.546Z","avatar_url":"https://github.com/hamishforbes.png","language":"Perl","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zedcup\n\nZero Conf Upstream load balancing and failover for Openresty and Consul\n\n[![Build Status](https://travis-ci.com/hamishforbes/zedcup.svg?branch=master)](https://travis-ci.com/hamishforbes/zedcup)\n\n# Table of Contents\n\n* [Status](#status)\n* [Overview](#overview)\n* [Configuration](#configuration)\n* [API](#api)\n    * [zedcup](#zedcup)\n    * [handler](#handler)\n* [Events](#events)\n\n\n# Status\n\nExperimental, API may change without warning.\n\n# Overview\n\n```lua\nhttp {\n    lua_package_path \"/PATH/TO/zedcup/lib/?.lua;;\";\n\n    lua_shared_dict zedcup_cache 1m;\n    lua_shared_dict zedcup_locks 1m;\n    lua_shared_dict zedcup_ipc 1m;\n    lua_socket_log_errors off;\n\n    init_by_lua_block {\n        require \"resty.core\"\n\n        require(\"zedcup\").init({\n            consul = {\n                host = \"127.0.0.1\",\n                port = 8500,\n            },\n        })\n    }\n\n    init_worker_by_lua_block {\n        require(\"zedcup\").run_workers()\n    }\n\n    server {\n        listen 80;\n\n        server_name zedcup;\n\n        location /_configure {\n            content_by_lua_block {\n                local conf = {\n                    pools = {\n                        {\n                            name = \"primary\",\n                            timeout = 100,\n                            healthcheck = {\n                                path = \"/_health\"\n                            },\n                            hosts = {\n                                { name = \"web01\", host = \"10.10.10.1\", port = 80 },\n                                { name = \"web02\", host = \"10.10.10.2\", port = 80 }\n                            }\n                        },\n                        {\n                            name = \"secondary\",\n                            hosts = {\n                                {\n                                    name = \"dr01\", host = \"10.20.20.1\", weight = 10, port = \"80\",\n                                    healthcheck = {\n                                        path = \"/dr_check\",\n                                        headers = {\n                                            [\"Host\"] = \"www.example.com\"\n                                        }\n                                    },\n                                }\n                            }\n                        },\n                    }\n                }\n\n                local ok, err = require(\"zedcup\").configure_instance(\"test\", conf)\n                if not ok then error(err) end\n            }\n        }\n\n        location / {\n            content_by_lua_block {\n                local handler, err = require(\"zedcup\").create_handler(\"test\")\n                assert(handler, err)\n\n                local res, err = handler:request({ path = \"/test\" })\n                assert(res, err)\n\n                ngx.say(res.status)\n                ngx.say(res:read_body())\n\n                handler:set_keepalive()\n\n            }\n        }\n    }\n\n}\n```\n\n## Dependencies\n  * pintsized/lua-resty-http\n  * thibaultcha/lua-resty-mlcache\n  * hamishforbes/lua-resty-consul\n\n# Configuration\n\nAll configuration beyond the bare minimum required to connect to Consul, is stored in the Consul KV store.\n\nConfigs can be saved to Consul with the [configure](#configure) and [configure_instance](#configure_instance) methods.\n\n\n### Global configuration\n\nConsul keys: `\u003cprefix\u003e/config/\u003ckey\u003e`\n\nDefaults\n\n```lua\n{\n    host_revive_interval   = 10,\n    cache_update_interval  = 1,\n    healthcheck_interval   = 10,\n    watcher_interval       = 10,\n    session_renew_interval = 10,\n    session_ttl            = 30,\n    worker_lock_ttl        = 30,\n    consul_wait_time       = 600,\n}\n```\n\n```\nconsul kv put zedcup/config/consul_wait_time 300\n```\n\n### Instance configuration\n\nConsul keys: `\u003cprefix\u003e/instances/\u003cinstance\u003e/\u003ckey\u003e/\u003csub-key\u003e`\n\nDefaults\n\n```lua\n{\n    ssl         = false,\n    healthcheck = nil\n}\n\n```\n\n```\nconsul kv put zedcup/instances/my_instance/healthcheck/path /_healtcheck\n```\n\n#### SSL configuration\n\n```lua\ninstance.ssl = {\n    ssl_verify = true,\n    sni_name   = \"sni.domain.tld\n}\n```\n\n### Pool configuration\n\nConsul keys: `\u003cprefix\u003e/instances/\u003cinstance\u003e/pools/\u003cindex\u003e/\u003ckey\u003e`\n\nDefaults\n```lua\n{\n    name          = index -- If name is not set the index number will be used\n    up            = true, -- Set to false to never try hosts in this pool\n    method        = \"weighted_rr\",\n    timeout       = 2000, -- (ms) socket connect timeout\n    error_timeout = 60,   -- (s) down hosts will be revived after this long without an error\n    max_errors    = 3,    -- Number of failures within error_timeout before a host is marked down\n\n    -- HTTP options\n    read_timeout      = 10000, -- (ms) Timeout set after successful connection\n    keepalive_timeout = 60000, -- (ms)\n    keepalive_pool    = 128,   -- (ms)\n    status_codes      = { \"5xx\", \"4xx\" } -- Table of status codes which indicate a request failure\n    healthcheck       = nil\n}\n```\n\n```\nconsul kv put zedcup/instances/my_instance/pools/1/name my_pool_name\n```\n\n### Host configuration\n\nConsul keys: `\u003cprefix\u003e/instances/\u003cinstance\u003e/pools/\u003cindex\u003e/hosts/\u003cindex\u003e/\u003ckey\u003e`\n\nDefaults\n```lua\n{\n    name        = index -- If name is not set the index number will be used\n    host        = nil,  -- Required, hostname, IP or unix socket path\n    port        = nil,  -- Required unless host is a unix socket\n    up          = true, -- Set to false to mark this host as failed\n    weight      = 1,\n    healthcheck = nil\n}\n```\n\n```\nconsul kv put zedcup/instances/my_instance/pools/1/hosts/1/port 8080\n```\n\n### Healthcheck configuration\n\nHTTP healthchecks can be configured at the instance, pool or host level.  \nSetting the healthcheck param at any of these levels to `true` will use the defaults.\n\nHealthchecks are only performed from 1 node in the cluster at a time.\n\nDefaults\n```lua\n{\n    ssl        = nil,   -- Override instance SSL configuration\n    interval   = 60,    -- Frequency of checks\n    method     = \"GET\", -- HTTP requset method\n    path       = \"/\",   -- HTTP URI path\n    headers = {         -- Table of headers to send\n        [\"User-Agent\"] = \"zedcup/\".. _M._VERSION.. \" HTTP Check (lua)\"\n    },\n    status_codes = { \"5xx\", \"4xx\" } -- Status codes which indicate a failure, this default is only used if the pool has no status codes configured\n}\n```\n\n```\nconsul kv put zedcup/instances/my_instance/healthcheck/headers/Host www.real-domain.tld\n```\n\n# API\n\n## Zedcup\n\n * [init](#init)\n * [initted](#initted)\n * [run_workers](#run_workers)\n * [config](#config)\n * [configure](#configure)\n * [configure_instance](#configure_instance)\n * [remove_instance](#remove_instance)\n * [instance_list](#instance_list)\n * [bind](#bind)\n * [create_handler](#create_handler)\n\n### init\n\n`syntax: ok = zedcup.init(opts?)`\n\nInitialise zedcup with enough configuration to access consul and retrieve the rest of the configuration.\n\n`opts` is an optional table which will be merged with the defaults:   \n\n```lua\n{\n    prefix = \"zedcup\", -- Consul KV store prefix to use\n    consul = {},       -- Consul connection settings, see lua-resty-consul for defaults\n    dicts  = {         -- The 3 required shared dictionaries\n        cache = \"zedcup_cache\",\n        locks = \"zedcup_locks\",\n        ipc   = \"zedcup_ipc\",\n    }\n}\n```\n\n### initted\n\n`syntax: ok = zedcup.initted()`\n\nReturns `true` if `zedcup.init()` has already been called, otherwise `false`\n\n### run_workers\n\n`syntax: zedcup.run_workers()`\n\nStart all the required workers, returns `nil`\n\n### config\n\n`syntax: config, err = zedcup.config()`\n\nGet the global zedcup configuration from consul.\n\nReturns `nil` and an error on failure.\n\n### configure\n\n`syntax: ok, err = zedcup.configure(config)`\n\nSet the global zedcup configuration (as a table) in consul.\n\nWill overwrite any existing configuration.\n\nReturns `nil` and an error on failure.\n\n### configure_instance\n\n`syntax: ok, err = zedcup.configure_instance(instance, config)`\n\nSet or create the configuration for the named `instance`.\n\nWill clear any existing configuration and state for the instance.\n\nReturns `nil` and an error on failure.\n\n### remove_instance\n\n`syntax: ok, err = zedcup.remove_instance(instance)`\n\nDelete configuration and state for the named `instance`.\n\nReturns `nil` and an error on failure.\n\n### instance_list\n\n`syntax: list, err = zedcup.instance_list()`\n\nGet a list of zedcup instances from consul.\n\nThe list is a mixed associative/numeric table that is both iterable with `ipairs` and has a named key for each instance.\n\n```lua\nlocal list, err = require(\"zedcup\").instance_list()\nif err then\n    ngx.log(ngx.ERR, err)\n    return\nend\n\nif list[\"my_instance\"] then\n    ngx.say(\"Instance exists\")\nend\n\nfor _, instance in ipairs(list) do\n    ngx.say(\"Instance name: \", instance)\nend\n```\n\nReturns `nil` and an error on failure.\n\n### bind\n\n`syntax: ok, err = zedcup.bind(event, callback)`\n\nGlobally bind a callback function to a particularly [events](#events).\n\nCallbacks bound globally will receive 2 arguments,  \nthe first is the instance name and the second the event data.\n\n```lua\nlocal ok, err = require(\"zedcup\").bind(\"host_connect_error\", function(instance, data)\n    if instance == \"instance_i_care_about\" then\n        ngx.say(\"Error connecting to host: '\", data.host.name, \"': \", data.err)\n    end\nend)\n\n```\n\nCallbacks are executed in the order they were bound.\n\nReturns `nil` and an error on failure.\n\n### create_handler\n\n`syntax: handler, err = zedcup.create_handler(instance)`\n\nReturns a short lived [handler](#handler) object for the given instance.\n\nHandler objects are not intended to live beyond the lifetime of a request.\n\n```lua\nlocal handler, err = zedcup.create_handler(\"my_instance_name\")\nif err then return nil, err end\n\nlocal sock, err = handler:connect()\n\n```\n\nReturns `nil` and an error on failure.\n\n## Handler\n\n * [bind](#bind)\n * [config](#config)\n * [connect](#connect)\n * [request](#request)\n * [get_client_body_reader](#get_client_body_reader)\n * [set_keepalive](#set_keepalive)\n * [get_reused_times](#get_reused_times)\n * [close](#close)\n\n### bind\n\n`syntax: ok, err = handler:bind(event, callback)`\n\nBind a callback function to a particularly [events](#events) for the lifetime of the handler only.\n\n```lua\nlocal ok, err = handler:bind(\"host_connect_error\", function(data)\n    ngx.say(\"Error connecting to host: '\", data.host.name, \"': \", data.err)\nend)\n\n```\n\nCallbacks are executed in the order they were bound and before global callbacks.\n\nReturns `nil` and an error on failure.\n\n### config\n\n`syntax: config, err = handler:config()`\n\nGet the instance configuration from consul.\n\nReturns `nil` and an error on failure.\n\n### connect\n\n`syntax: sock, err = handler:connect(sock?)`\n\nReturns a connected [ngx.socket.tcp](openresty/lua-nginx-module#ngxsockettcp) socket.\n\nIf the `sock` paramater is not provided a new socket is object is created and returned.   \nThe `sock` parameter can also be a lua-resty client driver, as long as it supports the `connect` and `set_timeout` methods.\n\nIf the zedcup instance is configured for SSL then the ssl handshake will already have been performed.\n\nThis allows load balancing and failover of client drivers such as [lua-resty-redis](https://github.com/openresty/lua-resty-redis)\n\n```lua\nlocal handler, err = zedcup.create_handler(\"my_instance_name\")\nif err then return nil, err end\n\nlocal sock, err = handler:connect()\n\nsock:send(\"data\")\n\nlocal redis = require(\"resty.redis\").new()\n\nredis, err = handler:connect(redis)\n\nredis:get(\"foo\")\n\n```\n\nReturns `nil` and an error on failure.\n\n### request\n\n`syntax: res, err = handler:request(params)`\n\nConvenience method for making an HTTP request to the configured upstream host.\n\nA handler object can be used in place of a resty-http instance.\n\nTakes the same arguments and returns the same values as [resty-http:requst()](https://github.com/pintsized/lua-resty-http#request)\n\n\n### get_client_body_reader\n\nProxy method for [resty-http:get_client_body_reader()](https://github.com/pintsized/lua-resty-http#get_client_body_reader)\n\n### set_keepalive\n\nProxy method for [resty-http:set_keepalive()](https://github.com/pintsized/lua-resty-http#set_keepalive)\n\n### get_reused_times\n\nProxy method for [resty-http:get_reused_times()](https://github.com/pintsized/lua-resty-http#get_reused_times)\n\n### close\n\nProxy method for [resty-http:close()](https://github.com/pintsized/lua-resty-http#closed)\n\n# Events\n\n * [host_connect](#host_connect)\n * [host_connect_error](#host_connect_error)\n * [host_request_error](#host_request_error)\n * [host_up](#host_up)\n * [host_down](#host_down)\n\n## host_connect\n\n`syntax: bind(\"host_connect\", function(data) end)`\n\nFired whenever a successful connection is established to a host.\n\n```lua\ndata = {\n    pool = { ... pool configuration ... },\n    host = { ... host configuration ... }\n}\n```\n\n## host_connect_error\n\n`syntax: handler:bind(\"host_connect_error\", function(data) end)`\n\nFired when a connection to a host fails.\n\n```lua\ndata = {\n    pool = { ... pool configuration ... },\n    host = { ... host configuration ... },\n    err = \"Error message\"\n}\n```\n\n## host_request_error\n`syntax: handler:bind(\"host_request_error\", function(data) end)`\n\nFired when an HTTP request to a host fails.\n\n```lua\ndata = {\n    pool = { ... pool configuration ... },\n    host = { ... host configuration ... },\n    err = \"Error message\"\n}\n```\n\n## host_up\n\n`syntax: zedcup.bind(\"host_request_error\", function(instance, data) end)`\n\nFired when a host transitions from down to up when the error timeout expires.\n\n```lua\ndata = {\n    pool = { ... pool configuration ... },\n    host = { ... host configuration ... },\n}\n```\n\nN.B.: Callbacks for this event must be bound globally, hosts are only revived by a background worker.\n\n## host_down\n\n`syntax: handler:bind(\"host_request_error\", function(data) end)`\n\nFired when a host transitions from up to down when max_errors is exceeded.\n\n```lua\ndata = {\n    pool = { ... pool configuration ... },\n    host = { ... host configuration ... },\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhamishforbes%2Fzedcup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhamishforbes%2Fzedcup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhamishforbes%2Fzedcup/lists"}