├── .github └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── hc.go └── hc_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-go@v2 17 | with: 18 | go-version: '^1.24' 19 | - run: go test -race -cover ./... 20 | 21 | lint: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions/setup-go@v2 26 | with: 27 | go-version: '^1.24' 28 | 29 | - uses: golangci/golangci-lint-action@v7 30 | with: 31 | version: latest 32 | args: --timeout=3m 33 | 34 | build: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions/setup-go@v2 39 | with: 40 | go-version: '^1.24' 41 | 42 | - run: go build -v . 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Emacs template 3 | # -*- mode: gitignore; -*- 4 | *~ 5 | \#*\# 6 | /.emacs.desktop 7 | /.emacs.desktop.lock 8 | *.elc 9 | auto-save-list 10 | tramp 11 | .\#* 12 | 13 | # Org-mode 14 | .org-id-locations 15 | *_archive 16 | 17 | # flymake-mode 18 | *_flymake.* 19 | 20 | # eshell files 21 | /eshell/history 22 | /eshell/lastdir 23 | 24 | # elpa packages 25 | /elpa/ 26 | 27 | # reftex files 28 | *.rel 29 | 30 | # AUCTeX auto folder 31 | /auto/ 32 | 33 | # cask packages 34 | .cask/ 35 | dist/ 36 | 37 | # Flycheck 38 | flycheck_*.el 39 | 40 | # server auth directory 41 | /server/ 42 | 43 | # projectiles files 44 | .projectile 45 | 46 | # directory configuration 47 | .dir-locals.el 48 | 49 | # network security 50 | /network-security.data 51 | 52 | 53 | ### Go template 54 | # Binaries for programs and plugins 55 | *.exe 56 | *.exe~ 57 | *.dll 58 | *.so 59 | *.dylib 60 | 61 | # Test binary, built with `go test -e` 62 | *.test 63 | 64 | # Output of the go coverage tool, specifically when used with LiteIDE 65 | *.out 66 | 67 | # Dependency directories (remove the comment below to include it) 68 | # vendor/ 69 | 70 | ### VisualStudioCode template 71 | .vscode/* 72 | !.vscode/settings.json 73 | !.vscode/tasks.json 74 | !.vscode/launch.json 75 | !.vscode/extensions.json 76 | *.code-workspace 77 | 78 | # Local History for Visual Studio Code 79 | .history/ 80 | 81 | ### macOS template 82 | # General 83 | .DS_Store 84 | .AppleDouble 85 | .LSOverride 86 | 87 | # Icon must end with two \r 88 | Icon 89 | 90 | # Thumbnails 91 | ._* 92 | 93 | # Files that might appear in the root of a volume 94 | .DocumentRevisions-V100 95 | .fseventsd 96 | .Spotlight-V100 97 | .TemporaryItems 98 | .Trashes 99 | .VolumeIcon.icns 100 | .com.apple.timemachine.donotpresent 101 | 102 | # Directories potentially created on remote AFP share 103 | .AppleDB 104 | .AppleDesktop 105 | Network Trash Folder 106 | Temporary Items 107 | .apdisk 108 | 109 | ### Vim template 110 | # Swap 111 | [._]*.s[a-v][a-z] 112 | !*.svg # comment out if you don't need vector files 113 | [._]*.sw[a-p] 114 | [._]s[a-rt-v][a-z] 115 | [._]ss[a-gi-z] 116 | [._]sw[a-p] 117 | 118 | # Session 119 | Session.vim 120 | Sessionx.vim 121 | 122 | # Temporary 123 | .netrwhist 124 | *~ 125 | # Auto-generated tag files 126 | tags 127 | # Persistent undo 128 | [._]*.un~ 129 | 130 | ### Windows template 131 | # Windows thumbnail cache files 132 | Thumbs.db 133 | Thumbs.db:encryptable 134 | ehthumbs.db 135 | ehthumbs_vista.db 136 | 137 | # Dump file 138 | *.stackdump 139 | 140 | # Folder config file 141 | [Dd]esktop.ini 142 | 143 | # Recycle Bin used on file shares 144 | $RECYCLE.BIN/ 145 | 146 | # Windows Installer files 147 | *.cab 148 | *.msi 149 | *.msix 150 | *.msm 151 | *.msp 152 | 153 | # Windows shortcuts 154 | *.lnk 155 | 156 | ### SublimeText template 157 | # Cache files for Sublime Text 158 | *.tmlanguage.cache 159 | *.tmPreferences.cache 160 | *.stTheme.cache 161 | 162 | # Workspace files are user-specific 163 | *.sublime-workspace 164 | 165 | # Project files should be checked into the repository, unless a significant 166 | # proportion of contributors will probably not be using Sublime Text 167 | # *.sublime-project 168 | 169 | # SFTP configuration file 170 | sftp-config.json 171 | sftp-config-alt*.json 172 | 173 | # Package control specific files 174 | Package Control.last-run 175 | Package Control.ca-list 176 | Package Control.ca-bundle 177 | Package Control.system-ca-bundle 178 | Package Control.cache/ 179 | Package Control.ca-certs/ 180 | Package Control.merged-ca-bundle 181 | Package Control.user-ca-bundle 182 | oscrypto-ca-bundle.crt 183 | bh_unicode_properties.cache 184 | 185 | # Sublime-github package stores a github token in this file 186 | # https://packagecontrol.io/packages/sublime-github 187 | GitHub.sublime-settings 188 | 189 | ### JetBrains template 190 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 191 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 192 | 193 | # User-specific stuff 194 | .idea 195 | .idea/**/workspace.xml 196 | .idea/**/tasks.xml 197 | .idea/**/usage.statistics.xml 198 | .idea/**/dictionaries 199 | .idea/**/shelf 200 | 201 | # Generated files 202 | .idea/**/contentModel.xml 203 | 204 | # Sensitive or high-churn files 205 | .idea/**/dataSources/ 206 | .idea/**/dataSources.ids 207 | .idea/**/dataSources.local.xml 208 | .idea/**/sqlDataSources.xml 209 | .idea/**/dynamic.xml 210 | .idea/**/uiDesigner.xml 211 | .idea/**/dbnavigator.xml 212 | 213 | # Gradle 214 | .idea/**/gradle.xml 215 | .idea/**/libraries 216 | 217 | # Gradle and Maven with auto-import 218 | # When using Gradle or Maven with auto-import, you should exclude module files, 219 | # since they will be recreated, and may cause churn. Uncomment if using 220 | # auto-import. 221 | # .idea/artifacts 222 | # .idea/compiler.xml 223 | # .idea/jarRepositories.xml 224 | # .idea/modules.xml 225 | # .idea/*.iml 226 | # .idea/modules 227 | # *.iml 228 | # *.ipr 229 | 230 | # CMake 231 | cmake-build-*/ 232 | 233 | # Mongo Explorer plugin 234 | .idea/**/mongoSettings.xml 235 | 236 | # File-based project format 237 | *.iws 238 | 239 | # IntelliJ 240 | out/ 241 | 242 | # mpeltonen/sbt-idea plugin 243 | .idea_modules/ 244 | 245 | # JIRA plugin 246 | atlassian-ide-plugin.xml 247 | 248 | # Cursive Clojure plugin 249 | .idea/replstate.xml 250 | 251 | # Crashlytics plugin (for Android Studio and IntelliJ) 252 | com_crashlytics_export_strings.xml 253 | crashlytics.properties 254 | crashlytics-build.properties 255 | fabric.properties 256 | 257 | # Editor-based Rest Client 258 | .idea/httpRequests 259 | 260 | # Android studio 3.1+ serialized cache file 261 | .idea/caches/build_file_checksums.ser 262 | 263 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | go: "1.24" 4 | tests: false # include test files or not, default is true. 5 | 6 | linters: 7 | default: none 8 | enable: 9 | - asasalint # Check for pass []any as any in variadic func(...any). 10 | - asciicheck # Checks that your code does not contain non-ASCII identifiers. 11 | - bodyclose # Checks whether HTTP response body is closed successfully. 12 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()). 13 | - errcheck # Checks for unchecked errors in go programs. 14 | - errorlint # Finds code that will cause problems with the error wrapping scheme introduced in Go 1.13. 15 | - gocritic # Provides diagnostics that check for bugs, performance and style issues. 16 | - godot # Check if comments end in a period. 17 | - gosec # Inspects source code for security problems. 18 | - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. 19 | - ineffassign # Detects when assignments to existing variables are not used. 20 | - noctx # Finds sending http request without context.Context. 21 | - nolintlint # Reports ill-formed or insufficient nolint directives. 22 | - prealloc # Finds slice declarations that could potentially be pre-allocated. 23 | - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. 24 | - staticcheck # It's a set of rules from staticcheck. 25 | - unconvert # Remove unnecessary type conversions. 26 | - unparam # Reports unused function parameters. 27 | - unused # Checks Go code for unused constants, variables, functions and types. 28 | 29 | settings: 30 | dogsled: 31 | # Checks assignments with too many blank identifiers. 32 | max-blank-identifiers: 2 33 | 34 | errcheck: 35 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 36 | check-type-assertions: true 37 | # Report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. 38 | check-blank: true 39 | # To disable the errcheck built-in exclude list. 40 | disable-default-exclusions: true 41 | # List of functions to exclude from checking, where each entry is a single function to exclude. 42 | # See https://github.com/kisielk/errcheck#excluding-functions for details. 43 | exclude-functions: 44 | - io/ioutil.ReadFile 45 | - io.Copy(*bytes.Buffer) 46 | - io.Copy(os.Stdout) 47 | - (*strings.Builder).WriteString 48 | 49 | errorlint: 50 | # Check whether fmt.Errorf uses the %w verb for formatting errors. 51 | errorf: false 52 | # Check for plain type assertions and type switches. 53 | asserts: true 54 | # Check for plain error comparisons. 55 | comparison: true 56 | 57 | gocritic: 58 | # Which checks should be disabled; can't be combined with 'enabled-checks'. 59 | disabled-checks: 60 | - whyNoLint 61 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 62 | # See https://github.com/go-critic/go-critic#usage -> section "Tags". 63 | enabled-tags: 64 | - diagnostic 65 | - style 66 | # Settings passed to gocritic. 67 | # The settings key is the name of a supported gocritic checker. 68 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 69 | settings: 70 | # Must be valid enabled check name. 71 | captLocal: 72 | # Whether to restrict checker to params only. 73 | # Default: true 74 | paramsOnly: false 75 | elseif: 76 | # Whether to skip balanced if-else pairs. 77 | # Default: true 78 | skipBalanced: false 79 | nestingReduce: 80 | # Min number of statements inside a branch to trigger a warning. 81 | # Default: 5 82 | bodyWidth: 4 83 | tooManyResultsChecker: 84 | # Maximum number of results. 85 | # Default: 5 86 | maxResults: 10 87 | truncateCmp: 88 | # Whether to skip int/uint/uintptr types. 89 | # Default: true 90 | skipArchDependent: false 91 | underef: 92 | # Whether to skip (*x).method() calls where x is a pointer receiver. 93 | # Default: true 94 | skipRecvDeref: false 95 | unnamedResult: 96 | # Whether to check exported functions. 97 | # Default: false 98 | checkExported: false 99 | 100 | godot: 101 | # Comments to be checked: `declarations`, `toplevel`, or `all`. 102 | scope: all 103 | # List of regexps for excluding particular comment lines from check. 104 | exclude: 105 | # Exclude todo and fixme comments. 106 | - '^fixme:' 107 | - '^todo:' 108 | # Check that each sentence starts with a capital letter. 109 | capital: false 110 | # Check that each sentence ends with a period. 111 | period: true 112 | 113 | revive: 114 | # Maximum number of open files at the same time. 115 | # See https://github.com/mgechev/revive#command-line-flags 116 | # Defaults to unlimited. 117 | max-open-files: 0 118 | # Sets the default failure confidence. 119 | # This means that linting errors with less than 0.8 confidence will be ignored. 120 | # Default: 0.8 121 | confidence: 0.1 122 | # Sets the default severity. 123 | # See https://github.com/mgechev/revive#configuration 124 | # Default: warning 125 | severity: warning 126 | # Enable all available rules. 127 | # Default: false 128 | enable-all-rules: true 129 | # Rules configuration 130 | rules: 131 | # Suggests using constant for magic numbers and string literals. 132 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant 133 | - name: add-constant 134 | arguments: 135 | - allowFloats: 0.0,0.,1.0,1.,2.0,2. 136 | allowInts: 0,1,2,3,4,5,6,7,8,9,10,24,30,31,64,128 137 | allowStrs: '""' 138 | maxLitCount: "5" 139 | severity: warning 140 | disabled: false 141 | 142 | # Warns when a function receives more parameters than the maximum set by the rule's configuration. 143 | # Enforcing a maximum number of parameters helps to keep the code readable and maintainable. 144 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit 145 | - name: argument-limit 146 | arguments: 147 | - 4 148 | severity: warning 149 | disabled: false 150 | 151 | # Check for commonly mistaken usages of the sync/atomic package 152 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic 153 | - name: atomic 154 | severity: warning 155 | disabled: false 156 | 157 | # Warns on bare (a.k.a. naked) returns 158 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters 159 | - name: banned-characters 160 | arguments: 161 | - Ω 162 | - Σ 163 | - σ 164 | - "7" 165 | severity: warning 166 | disabled: false 167 | 168 | # Checks given banned characters in identifiers(func, var, const). Comments are not checked. 169 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return 170 | - name: bare-return 171 | severity: warning 172 | disabled: false 173 | 174 | # Blank import should be only in a main or test package, or have a comment justifying it. 175 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports 176 | - name: blank-imports 177 | severity: warning 178 | disabled: false 179 | 180 | # Using Boolean literals (true, false) in logic expressions may make the code less readable. 181 | # This rule suggests removing Boolean literals from logic expressions. 182 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr 183 | - name: bool-literal-in-expr 184 | severity: warning 185 | disabled: false 186 | 187 | # Explicitly invoking the garbage collector is, except for specific uses in benchmarking, very dubious. 188 | # The garbage collector can be configured through environment variables as described here: https://pkg.go.dev/runtime 189 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc 190 | - name: call-to-gc 191 | severity: warning 192 | disabled: false 193 | 194 | # Description: Cognitive complexity is a measure of how hard code is to understand. 195 | # While cyclomatic complexity is good to measure "testability" of the code, cognitive complexity 196 | # aims to provide a more precise measure of the difficulty of understanding the code. 197 | # Enforcing a maximum complexity per function helps to keep code readable and maintainable. 198 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity 199 | - name: cognitive-complexity 200 | arguments: 201 | - 50 202 | severity: warning 203 | disabled: false 204 | 205 | # Methods or fields of struct that have names different only by capitalization could be confusing. 206 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming 207 | - name: confusing-naming 208 | severity: warning 209 | disabled: false 210 | 211 | # Function or methods that return multiple, no named, values of the same type could induce error. 212 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results 213 | - name: confusing-results 214 | severity: warning 215 | disabled: false 216 | 217 | # The rule spots logical expressions that evaluate always to the same value. 218 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr 219 | - name: constant-logical-expr 220 | severity: warning 221 | disabled: false 222 | 223 | # By convention, context.Context should be the first parameter of a function. 224 | # https://github.com/golang/go/wiki/CodeReviewComments#contexts 225 | # This rule spots function declarations that do not follow the convention. 226 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument 227 | - name: context-as-argument 228 | arguments: 229 | - allowTypesBefore = "*testing.T": null 230 | severity: warning 231 | disabled: false 232 | 233 | # Basic types should not be used as a key in context.WithValue. 234 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type 235 | - name: context-keys-type 236 | severity: warning 237 | disabled: false 238 | 239 | # Cyclomatic complexity is a measure of code complexity. 240 | # Enforcing a maximum complexity per function helps to keep code readable and maintainable. 241 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic 242 | - name: cyclomatic 243 | arguments: 244 | - 15 245 | severity: warning 246 | disabled: false 247 | 248 | # Spots comments without whitespace between slashes and words: //pragma. 249 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings 250 | - name: comment-spacings 251 | arguments: 252 | - nolint 253 | severity: warning 254 | disabled: false 255 | 256 | # This rule spots potential dataraces caused by go-routines capturing (by-reference) particular 257 | # identifiers of the function from which go-routines are created. 258 | # The rule is able to spot two of such cases: go-routines capturing named return values, 259 | # and capturing for-range values. 260 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace 261 | - name: datarace 262 | severity: warning 263 | disabled: false 264 | 265 | # Packages exposing functions that can stop program execution by exiting are hard to reuse. 266 | # This rule looks for program exits in functions other than main() or init(). 267 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit 268 | - name: deep-exit 269 | severity: warning 270 | disabled: false 271 | 272 | # This rule warns on some common mistakes when using defer statement. 273 | # It currently alerts on the following situations: 274 | # - [ call-chain ] - even if deferring call-chains of the form foo()() is valid, 275 | # it does not help code understanding (only the last call is deferred) 276 | # - [ loop ] - deferring inside loops can be misleading (deferred functions are not executed at the end 277 | # of the loop iteration but of the current function) and it could lead to exhausting the execution stack 278 | # - [ method-call ] - deferring a call to a method can lead to subtle bugs if the method does not have a pointer receiver 279 | # - [ recover ] - calling recover outside a deferred function has no effect 280 | # - [ immediate-recover ] - calling recover at the time a defer is registered, rather than as part of the deferred callback. 281 | # e.g. defer recover() or equivalent. 282 | # - [ return ] - returning values form a deferred function has no effect. 283 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer 284 | - name: defer 285 | arguments: 286 | - - call-chain 287 | - loop 288 | - method-call 289 | - recover 290 | - immediate-recover 291 | - return 292 | severity: warning 293 | disabled: false 294 | 295 | # Importing with . makes the programs much harder to understand because it is unclear 296 | # whether names belong to the current package or to an imported package. 297 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports 298 | - name: dot-imports 299 | severity: warning 300 | disabled: false 301 | 302 | # It is possible to unintentionally import the same package twice. 303 | # This rule looks for packages that are imported two or more times. 304 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#duplicated-imports 305 | - name: duplicated-imports 306 | severity: warning 307 | disabled: false 308 | 309 | # In GO it is idiomatic to minimize nesting statements, a typical example is to avoid if-then-else constructions. 310 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return 311 | - name: early-return 312 | severity: warning 313 | disabled: false 314 | 315 | # Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring. 316 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block 317 | - name: empty-block 318 | severity: warning 319 | disabled: false 320 | 321 | # Sometimes gofmt is not enough to enforce a common formatting of a code-base. 322 | # This rule warns when there are heading or trailing newlines in code blocks. 323 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines 324 | - name: empty-lines 325 | severity: warning 326 | disabled: false 327 | 328 | # By convention, for the sake of readability, variables of type error must be named with the prefix err. 329 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming 330 | - name: error-naming 331 | severity: warning 332 | disabled: false 333 | 334 | # By convention, for the sake of readability, the errors should be last in the list of returned values by a function. 335 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return 336 | - name: error-return 337 | severity: warning 338 | disabled: false 339 | 340 | # By convention, for better readability, error messages should not be capitalized or end with punctuation or a newline. 341 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings 342 | - name: error-strings 343 | severity: warning 344 | disabled: false 345 | 346 | # It is possible to get a simpler program by replacing errors.New(fmt.Sprintf()) with fmt.Errorf(). 347 | # This rule spots that kind of simplification opportunities. 348 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf 349 | - name: errorf 350 | severity: warning 351 | disabled: false 352 | 353 | # Exported function and methods should have comments. 354 | # This warns on undocumented exported functions and methods. 355 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported 356 | - name: exported 357 | severity: warning 358 | disabled: false 359 | 360 | # This rule helps to enforce a common header for all source files in a project by spotting those files 361 | # that do not have the specified header. 362 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header 363 | - name: file-header 364 | arguments: 365 | - "" 366 | severity: warning 367 | disabled: true 368 | 369 | # If a function controls the flow of another by passing it information on what to do, both functions are said to be control-coupled. 370 | # Coupling among functions must be minimized for better maintainability of the code. 371 | # This rule warns on boolean parameters that create a control coupling. 372 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter 373 | - name: flag-parameter 374 | severity: warning 375 | disabled: false 376 | 377 | # Functions returning too many results can be hard to understand/use. 378 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit 379 | - name: function-result-limit 380 | arguments: 381 | - 2 382 | severity: warning 383 | disabled: false 384 | 385 | # Functions too long (with many statements and/or lines) can be hard to understand. 386 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length 387 | - name: function-length 388 | arguments: 389 | - 50 390 | - 0 391 | severity: warning 392 | disabled: false 393 | 394 | # Typically, functions with names prefixed with Get are supposed to return a value. 395 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return 396 | - name: get-return 397 | severity: warning 398 | disabled: false 399 | 400 | # An if-then-else conditional with identical implementations in both branches is an error. 401 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches 402 | - name: identical-branches 403 | severity: warning 404 | disabled: false 405 | 406 | # Checking if an error is nil to just after return the error or nil is redundant. 407 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return 408 | - name: if-return 409 | severity: warning 410 | disabled: false 411 | 412 | # By convention, for better readability, incrementing an integer variable by 1 is recommended 413 | # to be done using the ++ operator. 414 | # This rule spots expressions like i += 1 and i -= 1 and proposes to change them into i++ and i--. 415 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement 416 | - name: increment-decrement 417 | severity: warning 418 | disabled: false 419 | 420 | # To improve the readability of code, it is recommended to reduce the indentation as much as possible. 421 | # This rule highlights redundant else-blocks that can be eliminated from the code. 422 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow 423 | - name: indent-error-flow 424 | severity: warning 425 | disabled: false 426 | 427 | # Warns when importing blocked packages. 428 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blocklist 429 | - name: imports-blocklist 430 | arguments: 431 | - crypto/md5 432 | - crypto/sha1 433 | severity: warning 434 | disabled: false 435 | 436 | # In GO it is possible to declare identifiers (packages, structs, interfaces, parameters, 437 | # receivers, variables, constants...) that conflict with the name of an imported package. 438 | # This rule spots identifiers that shadow an import. 439 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing 440 | - name: import-shadowing 441 | severity: warning 442 | disabled: false 443 | 444 | # Warns in the presence of code lines longer than a configured maximum. 445 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit 446 | - name: line-length-limit 447 | arguments: 448 | - 150 449 | severity: warning 450 | disabled: false 451 | 452 | # Packages declaring too many public structs can be hard to understand/use, 453 | # and could be a symptom of bad design. 454 | # This rule warns on files declaring more than a configured, maximum number of public structs. 455 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs 456 | - name: max-public-structs 457 | arguments: 458 | - 3 459 | severity: warning 460 | disabled: true 461 | 462 | # A function that modifies its parameters can be hard to understand. 463 | # It can also be misleading if the arguments are passed by value by the caller. 464 | # This rule warns when a function modifies one or more of its parameters. 465 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter 466 | - name: modifies-parameter 467 | severity: warning 468 | disabled: false 469 | 470 | # A method that modifies its receiver value can have undesired behavior. 471 | # The modification can be also the root of a bug because the actual value receiver could be a copy of that used at the calling site. 472 | # This rule warns when a method modifies its receiver. 473 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver 474 | - name: modifies-value-receiver 475 | severity: warning 476 | disabled: false 477 | 478 | # Packages declaring structs that contain other inline struct definitions can be hard to understand/read for other developers. 479 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs 480 | - name: nested-structs 481 | severity: warning 482 | disabled: false 483 | 484 | # conditional expressions can be written to take advantage of short circuit evaluation and speed up 485 | # its average evaluation time by forcing the evaluation of less time-consuming terms before more costly ones. 486 | # This rule spots logical expressions where the order of evaluation of terms seems non-optimal. 487 | # Please notice that confidence of this rule is low and is up to the user to decide if the suggested 488 | # rewrite of the expression keeps the semantics of the original one. 489 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order 490 | - name: optimize-operands-order 491 | severity: warning 492 | disabled: false 493 | 494 | # Packages should have comments. This rule warns on undocumented packages and when packages comments are detached to the package keyword. 495 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments 496 | - name: package-comments 497 | severity: warning 498 | disabled: false 499 | 500 | # This rule suggests a shorter way of writing ranges that do not use the second value. 501 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range 502 | - name: range 503 | severity: warning 504 | disabled: false 505 | 506 | # Range variables in a loop are reused at each iteration; therefore a goroutine created 507 | # in a loop will point to the range variable with from the upper scope. 508 | # This way, the goroutine could use the variable with an undesired value. 509 | # This rule warns when a range value (or index) is used inside a closure. 510 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure 511 | - name: range-val-in-closure 512 | severity: warning 513 | disabled: false 514 | 515 | # Range variables in a loop are reused at each iteration. This rule warns when assigning the address of the variable, 516 | # passing the address to append() or using it in a map. 517 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-address 518 | - name: range-val-address 519 | severity: warning 520 | disabled: false 521 | 522 | # By convention, receiver names in a method should reflect their identity. 523 | # For example, if the receiver is of type Parts, p is an adequate name for it. 524 | # Contrary to other languages, it is not idiomatic to name receivers as this or self. 525 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming 526 | - name: receiver-naming 527 | severity: warning 528 | disabled: false 529 | 530 | # Constant names like false, true, nil, function names like append, make, and basic type names like bool, 531 | # and byte are not reserved words of the language; therefore the can be redefined. 532 | # Even if possible, redefining these built in names can lead to bugs very difficult to detect. 533 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id 534 | - name: redefines-builtin-id 535 | severity: warning 536 | disabled: false 537 | 538 | # explicit type conversion string(i) where i has an integer type other than 539 | # rune might behave not as expected by the developer (e.g. string(42) is not "42"). 540 | # This rule spot that kind of suspicious conversions. 541 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int 542 | - name: string-of-int 543 | severity: warning 544 | disabled: false 545 | 546 | # This rule allows you to configure a list of regular expressions that string literals 547 | # in certain function calls are checked against. This is geared towards user facing applications 548 | # where string literals are often used for messages that will be presented to users, 549 | # so it may be desirable to enforce consistent formatting. 550 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format 551 | - name: string-format 552 | severity: warning 553 | disabled: true 554 | 555 | # Struct tags are not checked at compile time. 556 | # This rule, checks and warns if it finds errors in common struct tags types like: 557 | # asn1, default, json, protobuf, xml, yaml. 558 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag 559 | - name: struct-tag 560 | severity: warning 561 | disabled: false 562 | 563 | # To improve the readability of code, it is recommended to reduce the indentation as much as possible. 564 | # This rule highlights redundant else-blocks that can be eliminated from the code. 565 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#superfluous-else 566 | - name: superfluous-else 567 | severity: warning 568 | disabled: false 569 | 570 | # This rule warns when using == and != for equality check time.Time and suggest to time.time.Equal method, 571 | # for about information follow this link: https://pkg.go.dev/time#Time 572 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal 573 | - name: time-equal 574 | severity: warning 575 | disabled: false 576 | 577 | # Using unit-specific suffix like "Secs", "Mins", ... when naming variables of type time.Duration 578 | # can be misleading, this rule highlights those cases. 579 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming 580 | - name: time-naming 581 | severity: warning 582 | disabled: false 583 | 584 | # This rule warns when initialism, variable or package naming conventions are not followed. 585 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming 586 | - name: var-naming 587 | arguments: 588 | - [] 589 | - - ID 590 | - VM 591 | severity: warning 592 | disabled: false 593 | 594 | # This rule proposes simplifications of variable declarations. 595 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration 596 | - name: var-declaration 597 | severity: warning 598 | disabled: false 599 | 600 | # Unconditional recursive calls will produce infinite recursion, thus program stack overflow. 601 | # This rule detects and warns about unconditional (direct) recursive calls. 602 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion 603 | - name: unconditional-recursion 604 | severity: warning 605 | disabled: false 606 | 607 | # This rule warns on wrongly named un-exported symbols, i.e. un-exported symbols whose name 608 | # start with a capital letter. 609 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming 610 | - name: unexported-naming 611 | severity: warning 612 | disabled: false 613 | 614 | # This rule warns when an exported function or method returns a value of an un-exported type. 615 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return 616 | - name: unexported-return 617 | severity: warning 618 | disabled: false 619 | 620 | # This rule warns when errors returned by a function are not explicitly handled on the caller side. 621 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error 622 | - name: unhandled-error 623 | arguments: 624 | - fmt.Println 625 | - fmt.Printf 626 | severity: warning 627 | disabled: true 628 | 629 | # This rule suggests to remove redundant statements like a break at the end of a case block, 630 | # for improving the code's readability. 631 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt 632 | - name: unnecessary-stmt 633 | severity: warning 634 | disabled: false 635 | 636 | # This rule spots and proposes to remove unreachable code. 637 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code 638 | - name: unreachable-code 639 | severity: warning 640 | disabled: false 641 | 642 | # This rule warns on unused parameters. Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug. 643 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter 644 | - name: unused-parameter 645 | severity: warning 646 | disabled: false 647 | 648 | # This rule warns on unused method receivers. 649 | # Methods with unused receivers can be a symptom of an unfinished refactoring or a bug. 650 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver 651 | - name: unused-receiver 652 | severity: warning 653 | disabled: false 654 | 655 | # This rule warns on useless break statements in case clauses of switch and select statements. 656 | # GO, unlike other programming languages like C, only executes statements of the selected case 657 | # while ignoring the subsequent case clauses. 658 | # Therefore, inserting a break at the end of a case clause has no effect. 659 | # Because break statements are rarely used in case clauses, when switch or select statements 660 | # are inside a for-loop, the programmer might wrongly assume that a break in a case clause will 661 | # take the control out of the loop. The rule emits a specific warning for such cases. 662 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break 663 | - name: useless-break 664 | severity: warning 665 | disabled: false 666 | 667 | # Function parameters that are passed by value, are in fact a copy of the original argument. 668 | # Passing a copy of a sync.WaitGroup is usually not what the developer wants to do. 669 | # This rule warns when a sync.WaitGroup expected as a by-value parameter in a function or method. 670 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value 671 | - name: waitgroup-by-value 672 | severity: warning 673 | disabled: false 674 | 675 | # This rule warns on redundant import aliases. This happens when the alias used on the import 676 | # statement matches the imported package name. 677 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias 678 | - name: redundant-import-alias 679 | severity: warning 680 | disabled: true 681 | 682 | exclusions: 683 | generated: lax 684 | presets: 685 | - comments 686 | - common-false-positives 687 | - legacy 688 | - std-error-handling 689 | paths: 690 | - third_party$ 691 | - builtin$ 692 | - examples$ 693 | 694 | # Formatters configuration. 695 | formatters: 696 | enable: 697 | - gci # Checks if code and import statements are formatted, with additional rules. 698 | - gofmt # Checks if the code is formatted according to 'gofmt' command. 699 | - gofumpt # Checks if code and import statements are formatted, with additional rules. 700 | - goimports # Checks if the code and import statements are formatted according to the 'goimports' command. 701 | - golines # Checks if code is formatted, and fixes long lines. 702 | settings: 703 | gci: 704 | # Sections specifies the order of import sections. 705 | # Default: ["standard", "default", "blank", "dot"] 706 | sections: 707 | - standard 708 | - default 709 | - blank 710 | - dot 711 | # Custom order of sections. 712 | # Default: false 713 | custom-order: false 714 | 715 | golines: 716 | # Maximum line length. 717 | # Default: 120 718 | max-len: 120 719 | # Reformat comments. 720 | # Default: true 721 | reformat-tags: true 722 | 723 | exclusions: 724 | generated: lax 725 | paths: 726 | - third_party$ 727 | - builtin$ 728 | - examples$ 729 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (e) [2020] [Serhii Mariiekha ] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👩‍⚕️👨‍⚕️ hc 2 | 3 | `hc` is a tiny library for synchronization of mission critical concurrent health checks 4 | 5 | The `HealthChecker` interface is a heart of this small library. 6 | 7 | ```go 8 | // HealthChecker represents logic of making the health check. 9 | type HealthChecker interface { 10 | // Health takes the context and performs the health check. 11 | // Returns nil in case of success or an error in case 12 | // of a failure. 13 | Health(ctx context.Context) error 14 | } 15 | ``` 16 | 17 | ## Usage 18 | 19 | Let's say that we have a web application with some upstream services (database, remote storage etc.), 20 | Work of these services is critical for our application. So we need to check if they are reachable and healthy, 21 | to provide the overall service health check information to orchestrator or load balancer. 22 | 23 | With `hc` it is simple. You just need to implement the `HealthChecker` interface for you're downstream. 24 | 25 | ```go 26 | // PgUpstreamService holds logic of interaction 27 | // with Postgres database. 28 | type PgUpstreamService struct { 29 | db *pgxpool.Pool 30 | } 31 | 32 | func (s *PgUpstreamService) Health(ctx context.Context) error { 33 | conn, err := s.db.Acquire(ctx) 34 | if err != nil { 35 | return fmt.Errorf("unable to aquire connection from pool: %w", err) 36 | } 37 | 38 | defer conn.Release() 39 | 40 | q := `SELECT count(*) FROM information_schema.tables WHERE table_type='public';` 41 | 42 | var count int 43 | 44 | if err := conn.QueryRow(ctx, q).Scan(&count); err != nil { 45 | return fmt.Errorf("query failed: %w", err) 46 | } 47 | 48 | return nil 49 | } 50 | ``` 51 | 52 | Now in your http server health check endpoint you just need to gather information about all upstream health checks. 53 | 54 | ```go 55 | checker := hc.NewMultiChecker(pgUpstream, storageUpstream) 56 | 57 | mux := http.NewServeMux() 58 | 59 | mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { 60 | if err := checker.Health(r.Context()); err != nil { 61 | w.WriteHeader(http.StatusServiceUnavailable) 62 | return 63 | } 64 | 65 | w.WriteHeader(http.StatusOK) 66 | }) 67 | ``` 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/heartwilltell/hc 2 | 3 | go 1.24 4 | 5 | require golang.org/x/sync v0.13.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 2 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 3 | -------------------------------------------------------------------------------- /hc.go: -------------------------------------------------------------------------------- 1 | // Package hc represents logic of making the health check. 2 | package hc 3 | 4 | import ( 5 | "context" 6 | "maps" 7 | "sync" 8 | "time" 9 | 10 | "golang.org/x/sync/errgroup" 11 | ) 12 | 13 | // Compilation time checks for interface implementation. 14 | var ( 15 | _ HealthChecker = (*MultiChecker)(nil) 16 | _ HealthChecker = NopChecker{} 17 | _ HealthChecker = (*MultiServiceChecker)(nil) 18 | ) 19 | 20 | // HealthChecker represents logic of making the health check. 21 | type HealthChecker interface { 22 | // Health takes the context and performs the health check. 23 | // Returns nil in case of success or an error in case 24 | // of a failure. 25 | Health(ctx context.Context) error 26 | } 27 | 28 | // NewMultiChecker takes several health checkers and performs 29 | // health checks for each of them concurrently. 30 | func NewMultiChecker(hcs ...HealthChecker) *MultiChecker { 31 | c := MultiChecker{hcs: make([]HealthChecker, 0, len(hcs))} 32 | c.hcs = append(c.hcs, hcs...) 33 | return &c 34 | } 35 | 36 | // MultiChecker takes multiple health checker and performs 37 | // health checks for each of them concurrently. 38 | type MultiChecker struct{ hcs []HealthChecker } 39 | 40 | // Health takes the context and performs the health check. 41 | // Returns nil in case of success or an error in case 42 | // of a failure. 43 | func (c *MultiChecker) Health(ctx context.Context) error { 44 | g, gctx := errgroup.WithContext(ctx) 45 | 46 | for _, check := range c.hcs { 47 | g.Go(func() error { return check.Health(gctx) }) 48 | } 49 | 50 | return g.Wait() 51 | } 52 | 53 | // Add appends health HealthChecker to internal slice of HealthCheckers. 54 | func (c *MultiChecker) Add(hc HealthChecker) { c.hcs = append(c.hcs, hc) } 55 | 56 | // ServiceStatus represents the status of a service health check. 57 | type ServiceStatus struct { 58 | Error error 59 | Duration time.Duration 60 | CheckedAt time.Time 61 | } 62 | 63 | // ServiceReport contains the status of all services. 64 | type ServiceReport struct { 65 | mu sync.RWMutex 66 | st map[string]ServiceStatus 67 | } 68 | 69 | // NewServiceReport creates a new ServiceReport. 70 | func NewServiceReport() *ServiceReport { return &ServiceReport{st: make(map[string]ServiceStatus)} } 71 | 72 | // GetStatuses returns a copy of all service statuses. 73 | func (r *ServiceReport) GetStatuses() map[string]ServiceStatus { 74 | r.mu.RLock() 75 | defer r.mu.RUnlock() 76 | 77 | return maps.Clone(r.st) 78 | } 79 | 80 | // MultiServiceChecker implements the HealthChecker interface for checking multiple services. 81 | type MultiServiceChecker struct { 82 | services map[string]HealthChecker 83 | report *ServiceReport 84 | } 85 | 86 | // NewMultiServiceChecker creates a new MultiServiceChecker with the given services. 87 | func NewMultiServiceChecker(report *ServiceReport) *MultiServiceChecker { 88 | return &MultiServiceChecker{ 89 | services: make(map[string]HealthChecker), 90 | report: report, 91 | } 92 | } 93 | 94 | // Report returns a service report. 95 | func (c *MultiServiceChecker) Report() *ServiceReport { 96 | if c.report == nil { 97 | c.report = NewServiceReport() 98 | } 99 | 100 | return c.report 101 | } 102 | 103 | // AddService adds a service to be checked. 104 | func (c *MultiServiceChecker) AddService(name string, checker HealthChecker) { 105 | c.services[name] = checker 106 | } 107 | 108 | // Health implements the HealthChecker interface. 109 | func (c *MultiServiceChecker) Health(ctx context.Context) error { 110 | if len(c.services) == 0 { 111 | return nil 112 | } 113 | 114 | var g errgroup.Group 115 | 116 | for name, checker := range c.services { 117 | g.Go(func() error { 118 | startTime := time.Now() 119 | checkErr := checker.Health(ctx) 120 | duration := time.Since(startTime) 121 | 122 | c.report.mu.Lock() 123 | defer c.report.mu.Unlock() 124 | 125 | c.report.st[name] = ServiceStatus{ 126 | Error: checkErr, 127 | Duration: duration, 128 | CheckedAt: time.Now(), 129 | } 130 | 131 | return checkErr 132 | }) 133 | } 134 | 135 | return g.Wait() 136 | } 137 | 138 | // NopChecker represents nop health checker. 139 | type NopChecker struct{} 140 | 141 | // NewNopChecker returns new instance of NopChecker. 142 | func NewNopChecker() NopChecker { return NopChecker{} } 143 | 144 | func (NopChecker) Health(context.Context) error { return nil } 145 | 146 | // Synchronizer holds synchronization mechanics. 147 | // Deprecated: Use errgroup.Group instead. 148 | type Synchronizer struct { 149 | wg sync.WaitGroup 150 | so sync.Once 151 | err error 152 | cancel func() 153 | } 154 | 155 | // Error returns a string representation of underlying error. 156 | // Implements builtin error interface. 157 | func (s *Synchronizer) Error() string { return s.err.Error() } 158 | 159 | // SetError sets an error to the Synchronizer structure. 160 | // Uses sync.Once to set error only once. 161 | func (s *Synchronizer) SetError(err error) { s.so.Do(func() { s.err = err }) } 162 | 163 | // Do wrap the sync.Once Do method. 164 | func (s *Synchronizer) Do(f func()) { s.so.Do(f) } 165 | 166 | // Done wraps the sync.WaitGroup Done method. 167 | func (s *Synchronizer) Done() { s.wg.Done() } 168 | 169 | // Add wraps the sync.WaitGroup Add method. 170 | func (s *Synchronizer) Add(delta int) { s.wg.Add(delta) } 171 | 172 | // Wait wraps the sync.WaitGroup Wait method. 173 | func (s *Synchronizer) Wait() { s.wg.Wait() } 174 | 175 | // Cancel calls underlying cancel function to cancel context, 176 | // which passed to all health checks function. 177 | func (s *Synchronizer) Cancel() { s.cancel() } 178 | -------------------------------------------------------------------------------- /hc_test.go: -------------------------------------------------------------------------------- 1 | package hc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestMultiChecker_Health(t *testing.T) { 12 | errTest1 := errors.New("test error 1") 13 | errTest2 := errors.New("test error 2") 14 | 15 | type tcase struct { 16 | checkers []HealthChecker 17 | want error 18 | } 19 | 20 | tests := map[string]tcase{ 21 | "Nil checkers": { 22 | checkers: nil, 23 | want: nil, 24 | }, 25 | 26 | "Nil error": { 27 | checkers: []HealthChecker{ 28 | &testChecker{HealthFunc: func(ctx context.Context) error { return nil }}, 29 | &testChecker{HealthFunc: func(ctx context.Context) error { return nil }}, 30 | }, 31 | want: nil, 32 | }, 33 | 34 | "One non nil error": { 35 | checkers: []HealthChecker{ 36 | &testChecker{HealthFunc: func(ctx context.Context) error { return errTest1 }}, 37 | &testChecker{HealthFunc: func(ctx context.Context) error { return nil }}, 38 | }, 39 | want: errTest1, 40 | }, 41 | } 42 | 43 | for name, tc := range tests { 44 | t.Run(name, func(t *testing.T) { 45 | c := NewMultiChecker(tc.checkers...) 46 | if err := c.Health(context.Background()); err != tc.want { 47 | t.Errorf("Health() error = %v, want %v", err, tc.want) 48 | } 49 | }) 50 | } 51 | 52 | t.Run("Multiple non nil errors", func(t *testing.T) { 53 | checkers := []HealthChecker{ 54 | &testChecker{HealthFunc: func(ctx context.Context) error { return errTest1 }}, 55 | &testChecker{HealthFunc: func(ctx context.Context) error { return errTest2 }}, 56 | } 57 | 58 | err := NewMultiChecker(checkers...).Health(context.Background()) 59 | if err == nil { 60 | t.Errorf("Health() error = nil, want non-nil (either %v or %v)", errTest1, errTest2) 61 | } 62 | if !errors.Is(err, errTest1) && !errors.Is(err, errTest2) { 63 | t.Errorf("Health() error = %v, want %v or %v", err, errTest1, errTest2) 64 | } 65 | }) 66 | 67 | t.Run("Context cancellation", func(t *testing.T) { 68 | ctx, cancel := context.WithCancel(context.Background()) 69 | checker := &testChecker{HealthFunc: func(ctx context.Context) error { 70 | select { 71 | case <-ctx.Done(): 72 | return ctx.Err() 73 | case <-time.After(100 * time.Millisecond): 74 | return nil 75 | } 76 | }} 77 | mc := NewMultiChecker(checker) 78 | 79 | cancel() 80 | 81 | if err := mc.Health(ctx); err != context.Canceled { 82 | t.Errorf("Health() error = %v, want %v", err, context.Canceled) 83 | } 84 | }) 85 | } 86 | 87 | func TestMultiChecker_Add(t *testing.T) { 88 | tc1 := &testChecker{HealthFunc: func(ctx context.Context) error { return nil }} 89 | tc2 := &testChecker{HealthFunc: func(ctx context.Context) error { return errors.New("err2") }} 90 | 91 | mc1 := NewMultiChecker(tc1) 92 | if len(mc1.hcs) != 1 { 93 | t.Errorf("Initial add: expected len = 1, got = %d", len(mc1.hcs)) 94 | } 95 | if !reflect.DeepEqual(mc1.hcs[0], tc1) { 96 | t.Errorf("Initial add: expected = %v, got = %v", tc1, mc1.hcs[0]) 97 | } 98 | 99 | mc2 := NewMultiChecker(tc1, tc2) 100 | 101 | if len(mc2.hcs) != 2 { 102 | t.Fatalf("Add after init: expected len = 2, got = %d", len(mc2.hcs)) 103 | } 104 | if !reflect.DeepEqual(mc2.hcs[0], tc1) { 105 | t.Errorf("Add after init [0]: expected = %v, got = %v", tc1, mc2.hcs[0]) 106 | } 107 | if !reflect.DeepEqual(mc2.hcs[1], tc2) { 108 | t.Errorf("Add after init [1]: expected = %v, got = %v", tc2, mc2.hcs[1]) 109 | } 110 | } 111 | 112 | type testChecker struct { 113 | HealthFunc func(ctx context.Context) error 114 | } 115 | 116 | func (c *testChecker) Health(ctx context.Context) error { 117 | if c.HealthFunc == nil { 118 | return nil 119 | } 120 | 121 | return c.HealthFunc(ctx) 122 | } 123 | 124 | func TestMultiServiceChecker_Health(t *testing.T) { 125 | errTest := errors.New("service failed") 126 | ctx := context.Background() 127 | 128 | report := NewServiceReport() 129 | checker := NewMultiServiceChecker(report) 130 | 131 | // Test with no services 132 | if err := checker.Health(ctx); err != nil { 133 | t.Errorf("Health() with no services should return nil, got %v", err) 134 | } 135 | if len(report.GetStatuses()) != 0 { 136 | t.Errorf("Report should be empty when no services are checked, got %d statuses", len(report.GetStatuses())) 137 | } 138 | 139 | checker.AddService("ok_service", &testChecker{HealthFunc: func(ctx context.Context) error { return nil }}) 140 | checker.AddService("fail_service", &testChecker{HealthFunc: func(ctx context.Context) error { return errTest }}) 141 | checker.AddService("slow_service", &testChecker{HealthFunc: func(ctx context.Context) error { 142 | time.Sleep(50 * time.Millisecond) 143 | return nil 144 | }}) 145 | 146 | err := checker.Health(ctx) 147 | if err == nil { 148 | t.Errorf("Health() error = nil, want non-nil (expected error from fail_service)") 149 | } 150 | if !errors.Is(err, errTest) { 151 | t.Errorf("Health() error = %v, want %v", err, errTest) 152 | } 153 | 154 | statuses := report.GetStatuses() 155 | if len(statuses) != 3 { 156 | t.Fatalf("Expected 3 statuses in report, got %d", len(statuses)) 157 | } 158 | 159 | if status, ok := statuses["ok_service"]; !ok { 160 | t.Errorf("Status for 'ok_service' not found") 161 | } else { 162 | if status.Error != nil { 163 | t.Errorf("Expected 'ok_service' error to be nil, got %v", status.Error) 164 | } 165 | if status.CheckedAt.IsZero() { 166 | t.Errorf("'ok_service' CheckedAt should not be zero") 167 | } 168 | 169 | if status.Duration < 0 { 170 | t.Errorf("'ok_service' Duration should be non-negative, got %v", status.Duration) 171 | } 172 | } 173 | 174 | if status, ok := statuses["fail_service"]; !ok { 175 | t.Errorf("Status for 'fail_service' not found") 176 | } else { 177 | if status.Error != errTest { 178 | t.Errorf("Expected 'fail_service' error to be %v, got %v", errTest, status.Error) 179 | } 180 | if status.CheckedAt.IsZero() { 181 | t.Errorf("'fail_service' CheckedAt should not be zero") 182 | } 183 | if status.Duration < 0 { 184 | t.Errorf("'fail_service' Duration should be non-negative, got %v", status.Duration) 185 | } 186 | } 187 | 188 | if status, ok := statuses["slow_service"]; !ok { 189 | t.Errorf("Status for 'slow_service' not found") 190 | } else { 191 | if status.Error != nil { 192 | t.Errorf("Expected 'slow_service' error to be nil, got %v", status.Error) 193 | } 194 | if status.CheckedAt.IsZero() { 195 | t.Errorf("'slow_service' CheckedAt should not be zero") 196 | } 197 | if status.Duration < 50*time.Millisecond { 198 | t.Errorf("'slow_service' Duration should be at least 50ms, got %v", status.Duration) 199 | } 200 | } 201 | 202 | ctxCancel, cancel := context.WithCancel(context.Background()) 203 | cancel() 204 | 205 | checkerCancel := NewMultiServiceChecker(NewServiceReport()) 206 | checkerCancel.AddService("cancel_service", &testChecker{HealthFunc: func(ctx context.Context) error { 207 | select { 208 | case <-ctx.Done(): 209 | return ctx.Err() 210 | case <-time.After(100 * time.Millisecond): 211 | return errors.New("should have been cancelled") 212 | } 213 | }}) 214 | 215 | if err := checkerCancel.Health(ctxCancel); err != context.Canceled { 216 | t.Errorf("Health() with cancelled context error = %v, want %v", err, context.Canceled) 217 | } 218 | 219 | cancelStatuses := checkerCancel.Report().GetStatuses() 220 | if status, ok := cancelStatuses["cancel_service"]; !ok { 221 | t.Errorf("Status for 'cancel_service' not found") 222 | } else if status.Error != context.Canceled { 223 | t.Errorf("Expected 'cancel_service' error to be context.Canceled, got %v", status.Error) 224 | } 225 | } 226 | 227 | func TestMultiServiceChecker_Report(t *testing.T) { 228 | providedReport := NewServiceReport() 229 | checkerWithReport := NewMultiServiceChecker(providedReport) 230 | if checkerWithReport.Report() != providedReport { 231 | t.Error("Report() should return the provided report instance") 232 | } 233 | 234 | checkerNilReport := NewMultiServiceChecker(nil) 235 | report := checkerNilReport.Report() 236 | if report == nil { 237 | t.Fatal("Report() should create a new report if initialized with nil") 238 | } 239 | if len(report.st) != 0 { 240 | t.Error("Newly created report should be empty") 241 | } 242 | if checkerNilReport.Report() != report { 243 | t.Error("Subsequent calls to Report() should return the same created instance") 244 | } 245 | } 246 | 247 | func TestMultiServiceChecker_AddService(t *testing.T) { 248 | checker := NewMultiServiceChecker(nil) 249 | svc1 := &testChecker{} 250 | svc2 := &testChecker{} 251 | 252 | checker.AddService("service1", svc1) 253 | checker.AddService("service2", svc2) 254 | 255 | if len(checker.services) != 2 { 256 | t.Fatalf("Expected 2 services, got %d", len(checker.services)) 257 | } 258 | if checker.services["service1"] != svc1 { 259 | t.Errorf("Mismatch for service1") 260 | } 261 | if checker.services["service2"] != svc2 { 262 | t.Errorf("Mismatch for service2") 263 | } 264 | } 265 | 266 | func TestServiceReport_GetStatuses(t *testing.T) { 267 | report := NewServiceReport() 268 | report.st["service1"] = ServiceStatus{Error: nil, Duration: 1 * time.Second, CheckedAt: time.Now()} 269 | 270 | statuses := report.GetStatuses() 271 | 272 | if len(statuses) != 1 { 273 | t.Fatalf("Expected 1 status, got %d", len(statuses)) 274 | } 275 | if _, ok := statuses["service1"]; !ok { 276 | t.Fatal("Expected status for 'service1'") 277 | } 278 | 279 | statuses["service2"] = ServiceStatus{Error: errors.New("new error")} 280 | if len(report.st) != 1 { 281 | t.Error("Original report map should not be modified after modifying the copy") 282 | } 283 | if _, ok := report.st["service2"]; ok { 284 | t.Error("Original report map should not contain 'service2'") 285 | } 286 | } 287 | 288 | func TestNopChecker_Health(t *testing.T) { 289 | checker := NewNopChecker() 290 | if err := checker.Health(context.Background()); err != nil { 291 | t.Errorf("NopChecker.Health() error = %v, want nil", err) 292 | } 293 | } 294 | --------------------------------------------------------------------------------