{"id":27970243,"url":"https://github.com/justindfuller/nozzle","last_synced_at":"2025-05-07T21:57:21.485Z","repository":{"id":257330611,"uuid":"856623638","full_name":"JustinDFuller/nozzle","owner":"JustinDFuller","description":"The Hose Nozzle Pattern","archived":false,"fork":false,"pushed_at":"2025-03-31T06:33:59.000Z","size":59,"stargazers_count":5,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-07T21:57:14.952Z","etag":null,"topics":["go","library","rate-limiting"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/justindfuller/nozzle","language":"Go","has_issues":false,"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/JustinDFuller.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":"2024-09-12T22:48:04.000Z","updated_at":"2024-12-01T15:11:48.000Z","dependencies_parsed_at":"2024-09-16T03:25:48.012Z","dependency_job_id":"a2513c23-1868-4451-b3aa-ebc80f4cacbf","html_url":"https://github.com/JustinDFuller/nozzle","commit_stats":null,"previous_names":["justindfuller/nozzle"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JustinDFuller%2Fnozzle","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JustinDFuller%2Fnozzle/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JustinDFuller%2Fnozzle/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JustinDFuller%2Fnozzle/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JustinDFuller","download_url":"https://codeload.github.com/JustinDFuller/nozzle/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252961817,"owners_count":21832193,"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":["go","library","rate-limiting"],"created_at":"2025-05-07T21:57:20.892Z","updated_at":"2025-05-07T21:57:21.464Z","avatar_url":"https://github.com/JustinDFuller.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# nozzle\n\n[![Go Reference](https://pkg.go.dev/badge/github.com/justindfuller/nozzle.svg)](https://pkg.go.dev/github.com/justindfuller/nozzle)\n[![Build Status](https://github.com/JustinDFuller/nozzle/actions/workflows/build.yml/badge.svg)](https://github.com/JustinDFuller/nozzle/actions/workflows/build.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/justindfuller/nozzle)](https://goreportcard.com/report/github.com/justindfuller/nozzle)\n![Go Test Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/justindfuller/63d4999a653a0555c9806062b40c0139/raw/coverage.json)\n\nThe Hose Nozzle Pattern\n\n## TL;DR\n\nIt allows/disallows actions gradually (like a hose nozzel) instead of totally, (like a switch).\n\n## 📖 Table of Contents 📖\n* [Explanation](#what)\n* [Usage](#usage)\n* [Observability](#observability)\n* [Performance](#performance)\n* [Documentation](#documentation)\n\n## What?\n\nImagine these two control devices in your home:\n\n1. A Light Switch\n2. A Hose Nozzle\n\nA light switch has two modes: off and on (generally; yes, I am aware of dimmers). A hose nozzle has many positions between fully off and fully on.\n\nThe [circuit breaker pattern](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) mimics a part of your home similar to a light switch. A circuit breaker stays on at full power, then, when something goes wrong, it switches completely off. In the physical world, circuit breakers play an important role: they prevent surging electricity from destroying our electronics.\n\nIn technology, the circuit breaker pattern prevents one system from overloading another, giving it time to recover.\n\nHowever, in many systems, particularly systems that experience errors due to extreme sudden scaling, it may not be necessary to shut things completely off.\n\n### Example Scenario\n\nImagine a scenario where an application is handing 1000 requests per second (RPS). Suddenly, it receives 10,000 requests per second. Now, assume the application takes somewhere between a few seconds and a minute to scale up. Until it scales up, it can only handle a bit more than 1000 requests per second, the rest return errors.\n\nIf you are using the circuit breaker pattern, and if you configured your circuit breaker to trip above a 10% error rate, you will likely go from 1000 RPS to 0 RPS. Then, once the application scales up, you may jump up to 10,000 RPS. Or, if the surge has passed, you will return to 1000 RPS.\n\nThis is not ideal. During this time, the system was able to handle the original 1000 RPS. In fact, as it scales up, it was likely able to handle increasingly (but gradually) higher amounts of traffic.\n\n### Alternative\n\nA better strategy would be to quickly (but gradually) scale the allowed traffic down until the system reaches the desired success rate. Then, to attempt to scale back up until the error threshold is passed.\n\nThus: we have the nozzle pattern. Like a hose nozzle, it gradually opens and closes in response to behaviors. Its goal is to stay 100% open, but it will only open as far as it can without passing the specified error threshold.\n\n### Illustration\n\nThe following images illustrate the difference in behavior.\n\nFirst, the circuit breaker: once the threshold of 25% is crossed, the circuit breaker engages and fully shuts off requests. This results in a total loss of traffic. After a few seconds, the half-open step begins. Once it sees the half-open requests succeed, it fully re-opens.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://github.com/user-attachments/assets/7dbc3c30-158a-45c0-91de-4c0d46e6c7de\" alt=\"Circuit Breaker Illustration\" height=\"300px\" /\u003e\n\u003c/p\u003e\n\nSecond, the nozzle: once the threshold of 25% is crossed, the nozzle begins closing. First, slowly, then increasingly more quickly. Once it notices the failure rate has decreased below the threshold, it begins to open again.\n\nIn this case, you should notice it takes longer to return to full throughput, but overall loses fewer requests.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://github.com/user-attachments/assets/074c024c-63bc-40e4-8953-214d2c9f69cc\" alt=\"Nozzle Illustration\" height=\"300px\" /\u003e\n\u003c/p\u003e\n\n## Usage\n\nFirst, install the package:\n\n```\ngo get github.com/justindfuller/nozzle\n```\n\nThen, create a nozzle:\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n\n    \"github.com/justindfuller/nozzle\"\n)\n\nfunc main() {}\n    n := nozzle.New(nozzle.Options[*http.Response]{\n        Interval:              time.Second,\n        AllowedFailurePercent: 50,\n    })\n\n    for i := 0; i \u003c 1000; i++ {\n        res, err := n.DoError(func() (*http.Response, error) {\n            res, err := http.Get(\"https://google.com\")\n\n            return res, err\n        })\n        if err != nil {\n            log.Println(err)\n\n            continue\n        }\n\n        log.Println(res)\n    }\n}\n```\n\nThe Nozzle will attempt to execute as many requests as possible.\n\nIf you are not working with errors, you can use a Boolean Nozzle.\n\n```go\npackage main\n\nimport (\n    \"net/http\"\n\n    \"github.com/justindfuller/nozzle\"\n)\n\nfunc main() {}\n    n := nozzle.New(nozzle.Options[*http.Response]{\n        Interval:              time.Second,\n        AllowedFailurePercent: 50,\n    })\n\n    for i := 0; i \u003c 1000; i++ {\n        res, ok := n.DoBool(func() (*http.Response, bool) {\n            res, err := http.Get(\"https://google.com\")\n\n            return res, err != nil \u0026\u0026 res.StatusCode == http.StatusOK\n        })\n        if !ok {\n            log.Println(\"Request failed\")\n\n            continue\n        }\n\n        log.Println(res)\n    }\n}\n```\n\n### Generics\n\nAs you can see, this package uses generics. This allows the Nozzle's methods to return the same type as the function you pass to it. This allows the Nozzle to perform its work without interrupting the control-flow of your application.\n\n## Observability\n\nYou may want to collect metrics to help you observe when your nozzle is opening and closing. You can accomplish this with `nozzle.OnStateChange`. `OnStateChange` will be called _at most_ once per `Interval` but only if a change occured.\n\n```go\nnozzle.New(nozzle.Options[*example]{\n    Interval:              time.Second,\n    AllowedFailurePercent: 50,\n    OnStateChange: func(noz *nozzle.Nozzle[*example]) {\n        logger.Info(\n            \"Nozzle State Change\",\n            \"state\",\n            s,\n            \"flowRate\",\n            noz.FlowRate(),\n            \"failureRate\",\n            noz.FailureRate(),\n            \"successRate\",\n            noz.SuccessRate(),\n        )\n        /**\n         Example output:\n         {\n            \"message\": \"Nozzle State Change\",\n            \"state\": \"opening\",\n            \"flowRate\": 50,\n            \"failureRate\": 20,\n            \"successRate\": 80\n         }\n        **/\n    },\n}\n```\n\n## Performance\n\nThe performance is excellent. 0 bytes per operation, 0 allocations per operation. It works with concurrent goroutines without any race conditions.\n\n```go\n@JustinDFuller ➜ /workspaces/nozzle (main) $ make bench\ngoos: linux\ngoarch: amd64\npkg: github.com/justindfuller/nozzle\ncpu: AMD EPYC 7763 64-Core Processor\nBenchmarkNozzle_DoBool_Open-2             908032            1316 ns/op               0 B/op       0 allocs/op\nBenchmarkNozzle_DoBool_Closed-2          2301523             445.2 ns/op             0 B/op       0 allocs/op\nBenchmarkNozzle_DoBool_Half-2             981314            1313 ns/op               0 B/op       0 allocs/op\nBenchmarkNozzle_DoError_Open-2            892647            1446 ns/op               0 B/op       0 allocs/op\nBenchmarkNozzle_DoError_Closed-2         2554688             452.1 ns/op             0 B/op       0 allocs/op\nBenchmarkNozzle_DoError_Half-2            964617            1311 ns/op               0 B/op       0 allocs/op\nBenchmarkNozzle_DoBool_Control-2         1292871             960.8 ns/op             0 B/op       0 allocs/op\nPASS\nok      github.com/justindfuller/nozzle 11.410s\n```\n\n## Documentation\n\nPlease refer to the go documentatio hosted on [pkg.go.dev](https://pkg.go.dev/github.com/justindfuller/nozzle). You can see [all available types and methods](https://pkg.go.dev/github.com/justindfuller/nozzle#pkg-index) and [runnable examples](https://pkg.go.dev/github.com/justindfuller/nozzle#pkg-examples).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustindfuller%2Fnozzle","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjustindfuller%2Fnozzle","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustindfuller%2Fnozzle/lists"}