From ef7c0bebbd0c425c1cf118dc13328508371e9dc3 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 26 Jun 2024 15:33:08 -0700 Subject: [PATCH 1/5] Add `watchdog_interval` method and tests This adds a `watchdog_interval` method as well as tests for it and for `watchdog?` (which previously had no tests). I also took the liberty of revising the docs for `watchdog?` to (I hope) be a little clearer. Fixes #6. --- lib/sd_notify.rb | 38 ++++++++++++++++++++++++-------------- test/sd_notify_test.rb | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/lib/sd_notify.rb b/lib/sd_notify.rb index 66d73ed..cce66ba 100644 --- a/lib/sd_notify.rb +++ b/lib/sd_notify.rb @@ -59,9 +59,13 @@ def self.fdstore(unset_env=false) notify(FDSTORE, unset_env) end - # If the $WATCHDOG_USEC environment variable is set, - # and the $WATCHDOG_PID variable is unset or set to the PID of the current - # process + # Determine whether the systemd's watchdog is enabled for your program. If + # enabled, systemd will restart (or take some configured action) on your + # service when it goes N seconds without receiving a `WATCHDOG` notification + # from your program. + # + # See #watchdog for sending notifications and #watchdog_interval for how + # frequently to send them. # # @return [Boolean] true if the service manager expects watchdog keep-alive # notification messages to be sent from this process. @@ -69,23 +73,29 @@ def self.fdstore(unset_env=false) # @note Unlike sd_watchdog_enabled(3), this method does not mutate the # environment. def self.watchdog? - wd_usec = ENV["WATCHDOG_USEC"] - wd_pid = ENV["WATCHDOG_PID"] - - return false if !wd_usec - - begin - wd_usec = Integer(wd_usec) - rescue - return false - end + return false if watchdog_interval == 0.0 - return false if wd_usec <= 0 + wd_pid = ENV["WATCHDOG_PID"] return true if !wd_pid || wd_pid == $$.to_s false end + # Get the expected number of seconds between watchdog notifications. If + # systemd's watchdog manager is enabled, it will take action if it does not + # receive notifications at least this often from your program. + # + # @return [Float] the frequency (in seconds) at which the service manager + # expects watchdog keep-alive notification messages from this process. + # + # @note Unlike sd_watchdog_enabled(3), this returns seconds, not microseconds. + def self.watchdog_interval + wd_usec = Integer(ENV["WATCHDOG_USEC"]) + wd_usec.positive? ? wd_usec / 1e6 : 0.0 + rescue StandardError + 0.0 + end + # Notify systemd with the provided state, via the notification socket, if # any. # diff --git a/test/sd_notify_test.rb b/test/sd_notify_test.rb index 535fb39..3254534 100644 --- a/test/sd_notify_test.rb +++ b/test/sd_notify_test.rb @@ -30,11 +30,50 @@ def test_sd_notify_ready_unset assert_nil(ENV["NOTIFY_SOCKET"]) end + def test_sd_notify_watchdog_disabled + setup_socket + + assert_equal(false, SdNotify.watchdog?) + end + + def test_sd_notify_watchdog_enabled + ENV["WATCHDOG_USEC"] = "5_000_000" + ENV["WATCHDOG_PID"] = $$.to_s + setup_socket + + assert_equal(true, SdNotify.watchdog?) + end + + def test_sd_notify_watchdog_enabled_for_a_different_process + ENV["WATCHDOG_USEC"] = "5_000_000" + ENV["WATCHDOG_PID"] = ($$ + 1).to_s + setup_socket + + assert_equal(false, SdNotify.watchdog?) + end + + def test_sd_notify_watchdog_interval_disabled + setup_socket + + assert_equal(0.0, SdNotify.watchdog_interval) + end + + def test_sd_notify_watchdog_interval_enabled + ENV["WATCHDOG_USEC"] = "5_000_000" + ENV["WATCHDOG_PID"] = $$.to_s + setup_socket + + assert_equal(5.0, SdNotify.watchdog_interval) + end + def teardown @socket.close if @socket File.unlink(@sockaddr) if @sockaddr @socket = nil @sockaddr = nil + ENV.delete("NOTIFY_SOCKET") + ENV.delete("WATCHDOG_USEC") + ENV.delete("WATCHDOG_PID") end private From 131eaee014224c05713f33c0f23cc13653b0b279 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 26 Jun 2024 15:34:49 -0700 Subject: [PATCH 2/5] Add watchdog example to README --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.md b/README.md index b0b239f..77f1a16 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,58 @@ sleep 2 puts "Bye" ``` +If you are operating a long-running program and want to use systemd's watchdog service manager to monitor your program: + +```ruby +require "sd_notify" + +puts "Hello! Booting..." + +# doing some initialization work... +sleep 2 + +# notify systemd that we're ready +SdNotify.ready + +# You might have a more complicated method of keeping an eye on the internal +# health of your program, although you will usually want to do this on a +# separate thread so notifications are not held up by especially long chunks of +# work in your main working thread. +watchdog_thread = if SdNotify.watchdog? + Thread.new do + loop do + # Systemd recommends pinging the watchdog at half the configured interval + # to make sure notifications always arrive in time. + sleep SdNotify.watchdog_interval / 2 + if service_is_healthy + SdNotify.watchdog + else + break + end + end +end + +# Do our main work... +loop do + sleep 10 + sum += 1 + break +end + +puts "Finished working. Shutting down..." + +# Stop watchdog +watchdog_thread.exit + +# notify systemd we're shutting down +SdNotify.stopping + +# doing some cleanup work... +sleep 2 + +puts "Bye" +``` + ## License ruby-sdnotify is licensed under MIT. See [LICENSE](LICENSE). From 4644653e8ae529a814411c1a0ae1c62b32ca38e4 Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 26 Jun 2024 15:35:06 -0700 Subject: [PATCH 3/5] Update Rubocop config for current Rubocop Running Rubocop printed out a huge number of new warnings and errors. For the most part, I just silenced them since this gem has been pretty stable and I assume you don't want to modify things just to match newer recommendations. I did add `required_ruby_version` to the gemspec, though, since that just seems like a good idea. It's set to 2.3.0, based on the GitHub Actions CI job. --- .rubocop.yml | 378 ++++++++++++++++++++++++++++++++++++++++++++++ sd_notify.gemspec | 1 + 2 files changed, 379 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index 9b7fff4..f0e8684 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,9 @@ require: - rubocop-performance +AllCops: + TargetRubyVersion: 2.3 + Layout/SpaceAroundEqualsInParameterDefault: EnforcedStyle: no_space @@ -12,3 +15,378 @@ Style/SafeNavigation: Style/NegatedIf: Enabled: false + +Metrics/MethodLength: + Max: 20 + +Style/OptionalBooleanParameter: + Enabled: false + +Style/SpecialGlobalVars: + Enabled: false + +Gemspec/DeprecatedAttributeAssignment: # new in 1.30 + Enabled: false + +Gemspec/DevelopmentDependencies: # new in 1.44 + Enabled: false + +Gemspec/RequireMFA: # new in 1.23 + Enabled: false + +Layout/LineContinuationLeadingSpace: # new in 1.31 + Enabled: false + +Layout/LineContinuationSpacing: # new in 1.31 + Enabled: false + +Layout/LineEndStringConcatenationIndentation: # new in 1.18 + Enabled: false + +Layout/SpaceBeforeBrackets: # new in 1.7 + Enabled: false + +Lint/AmbiguousAssignment: # new in 1.7 + Enabled: false + +Lint/AmbiguousOperatorPrecedence: # new in 1.21 + Enabled: false + +Lint/AmbiguousRange: # new in 1.19 + Enabled: false + +Lint/ConstantOverwrittenInRescue: # new in 1.31 + Enabled: false + +Lint/DeprecatedConstants: # new in 1.8 + Enabled: false + +Lint/DuplicateBranch: # new in 1.3 + Enabled: false + +Lint/DuplicateMagicComment: # new in 1.37 + Enabled: false + +Lint/DuplicateMatchPattern: # new in 1.50 + Enabled: false + +Lint/DuplicateRegexpCharacterClassElement: # new in 1.1 + Enabled: false + +Lint/EmptyBlock: # new in 1.1 + Enabled: false + +Lint/EmptyClass: # new in 1.3 + Enabled: false + +Lint/EmptyInPattern: # new in 1.16 + Enabled: false + +Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 + Enabled: false + +Lint/ItWithoutArgumentsInBlock: # new in 1.59 + Enabled: false + +Lint/LambdaWithoutLiteralBlock: # new in 1.8 + Enabled: false + +Lint/LiteralAssignmentInCondition: # new in 1.58 + Enabled: false + +Lint/MixedCaseRange: # new in 1.53 + Enabled: false + +Lint/NoReturnInBeginEndBlocks: # new in 1.2 + Enabled: false + +Lint/NonAtomicFileOperation: # new in 1.31 + Enabled: false + +Lint/NumberedParameterAssignment: # new in 1.9 + Enabled: false + +Lint/OrAssignmentToConstant: # new in 1.9 + Enabled: false + +Lint/RedundantDirGlobSort: # new in 1.8 + Enabled: false + +Lint/RedundantRegexpQuantifiers: # new in 1.53 + Enabled: false + +Lint/RefinementImportMethods: # new in 1.27 + Enabled: false + +Lint/RequireRangeParentheses: # new in 1.32 + Enabled: false + +Lint/RequireRelativeSelfPath: # new in 1.22 + Enabled: false + +Lint/SymbolConversion: # new in 1.9 + Enabled: false + +Lint/ToEnumArguments: # new in 1.1 + Enabled: false + +Lint/TripleQuotes: # new in 1.9 + Enabled: false + +Lint/UnexpectedBlockArity: # new in 1.5 + Enabled: false + +Lint/UnmodifiedReduceAccumulator: # new in 1.1 + Enabled: false + +Lint/UselessRescue: # new in 1.43 + Enabled: false + +Lint/UselessRuby2Keywords: # new in 1.23 + Enabled: false + +Metrics/CollectionLiteralLength: # new in 1.47 + Enabled: false + +Naming/BlockForwarding: # new in 1.24 + Enabled: false + +Security/CompoundHash: # new in 1.28 + Enabled: false + +Security/IoMethods: # new in 1.22 + Enabled: false + +Style/ArgumentsForwarding: # new in 1.1 + Enabled: false + +Style/ArrayIntersect: # new in 1.40 + Enabled: false + +Style/CollectionCompact: # new in 1.2 + Enabled: false + +Style/ComparableClamp: # new in 1.44 + Enabled: false + +Style/ConcatArrayLiterals: # new in 1.41 + Enabled: false + +Style/DataInheritance: # new in 1.49 + Enabled: false + +Style/DirEmpty: # new in 1.48 + Enabled: false + +Style/DocumentDynamicEvalDefinition: # new in 1.1 + Enabled: false + +Style/EmptyHeredoc: # new in 1.32 + Enabled: false + +Style/EndlessMethod: # new in 1.8 + Enabled: false + +Style/EnvHome: # new in 1.29 + Enabled: false + +Style/ExactRegexpMatch: # new in 1.51 + Enabled: false + +Style/FetchEnvVar: # new in 1.28 + Enabled: false + +Style/FileEmpty: # new in 1.48 + Enabled: false + +Style/FileRead: # new in 1.24 + Enabled: false + +Style/FileWrite: # new in 1.24 + Enabled: false + +Style/HashConversion: # new in 1.10 + Enabled: false + +Style/HashExcept: # new in 1.7 + Enabled: false + +Style/IfWithBooleanLiteralBranches: # new in 1.9 + Enabled: false + +Style/InPatternThen: # new in 1.16 + Enabled: false + +Style/MagicCommentFormat: # new in 1.35 + Enabled: false + +Style/MapCompactWithConditionalBlock: # new in 1.30 + Enabled: false + +Style/MapIntoArray: # new in 1.63 + Enabled: false + +Style/MapToHash: # new in 1.24 + Enabled: false + +Style/MapToSet: # new in 1.42 + Enabled: false + +Style/MinMaxComparison: # new in 1.42 + Enabled: false + +Style/MultilineInPatternThen: # new in 1.16 + Enabled: false + +Style/NegatedIfElseCondition: # new in 1.2 + Enabled: false + +Style/NestedFileDirname: # new in 1.26 + Enabled: false + +Style/NilLambda: # new in 1.3 + Enabled: false + +Style/NumberedParameters: # new in 1.22 + Enabled: false + +Style/NumberedParametersLimit: # new in 1.22 + Enabled: false + +Style/ObjectThen: # new in 1.28 + Enabled: false + +Style/OpenStructUse: # new in 1.23 + Enabled: false + +Style/OperatorMethodCall: # new in 1.37 + Enabled: false + +Style/QuotedSymbols: # new in 1.16 + Enabled: false + +Style/RedundantArgument: # new in 1.4 + Enabled: false + +Style/RedundantArrayConstructor: # new in 1.52 + Enabled: false + +Style/RedundantConstantBase: # new in 1.40 + Enabled: false + +Style/RedundantCurrentDirectoryInPath: # new in 1.53 + Enabled: false + +Style/RedundantDoubleSplatHashBraces: # new in 1.41 + Enabled: false + +Style/RedundantEach: # new in 1.38 + Enabled: false + +Style/RedundantFilterChain: # new in 1.52 + Enabled: false + +Style/RedundantHeredocDelimiterQuotes: # new in 1.45 + Enabled: false + +Style/RedundantInitialize: # new in 1.27 + Enabled: false + +Style/RedundantLineContinuation: # new in 1.49 + Enabled: false + +Style/RedundantRegexpArgument: # new in 1.53 + Enabled: false + +Style/RedundantRegexpConstructor: # new in 1.52 + Enabled: false + +Style/RedundantSelfAssignmentBranch: # new in 1.19 + Enabled: false + +Style/RedundantStringEscape: # new in 1.37 + Enabled: false + +Style/ReturnNilInPredicateMethodDefinition: # new in 1.53 + Enabled: false + +Style/SelectByRegexp: # new in 1.22 + Enabled: false + +Style/SendWithLiteralMethodName: # new in 1.64 + Enabled: false + +Style/SingleLineDoEndBlock: # new in 1.57 + Enabled: false + +Style/StringChars: # new in 1.12 + Enabled: false + +Style/SuperArguments: # new in 1.64 + Enabled: false + +Style/SuperWithArgsParentheses: # new in 1.58 + Enabled: false + +Style/SwapValues: # new in 1.1 + Enabled: false + +Style/YAMLFileRead: # new in 1.53 + Enabled: false + +Performance/AncestorsInclude: # new in 1.7 + Enabled: false + +Performance/BigDecimalWithNumericArgument: # new in 1.7 + Enabled: false + +Performance/BlockGivenWithExplicitBlock: # new in 1.9 + Enabled: false + +Performance/CollectionLiteralInLoop: # new in 1.8 + Enabled: false + +Performance/ConcurrentMonotonicTime: # new in 1.12 + Enabled: false + +Performance/ConstantRegexp: # new in 1.9 + Enabled: false + +Performance/MapCompact: # new in 1.11 + Enabled: false + +Performance/MapMethodChain: # new in 1.19 + Enabled: false + +Performance/MethodObjectAsBlock: # new in 1.9 + Enabled: false + +Performance/RedundantEqualityComparisonBlock: # new in 1.10 + Enabled: false + +Performance/RedundantSortBlock: # new in 1.7 + Enabled: false + +Performance/RedundantSplitRegexpArgument: # new in 1.10 + Enabled: false + +Performance/RedundantStringChars: # new in 1.7 + Enabled: false + +Performance/ReverseFirst: # new in 1.7 + Enabled: false + +Performance/SortReverse: # new in 1.7 + Enabled: false + +Performance/Squeeze: # new in 1.7 + Enabled: false + +Performance/StringIdentifierArgument: # new in 1.13 + Enabled: false + +Performance/StringInclude: # new in 1.7 + Enabled: false + +Performance/Sum: # new in 1.8 + Enabled: false diff --git a/sd_notify.gemspec b/sd_notify.gemspec index 46dae4f..113ceb2 100644 --- a/sd_notify.gemspec +++ b/sd_notify.gemspec @@ -11,6 +11,7 @@ Gem::Specification.new do |s| s.files = ["lib/sd_notify.rb", "LICENSE", "README.md", "CHANGELOG.md"] s.homepage = "https://github.com/agis/ruby-sdnotify" s.license = "MIT" + s.required_ruby_version = ">= 2.3.0" s.add_development_dependency "minitest" s.add_development_dependency "rubocop" From 744ac7d71a94fb77264e7b8706cef1e3a771684c Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 26 Jun 2024 15:52:30 -0700 Subject: [PATCH 4/5] Oops, I missed the separate Ruby 2.2 job --- .rubocop.yml | 2 +- sd_notify.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index f0e8684..6f3f6f2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,7 @@ require: - rubocop-performance AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.2 Layout/SpaceAroundEqualsInParameterDefault: EnforcedStyle: no_space diff --git a/sd_notify.gemspec b/sd_notify.gemspec index 113ceb2..cf44be0 100644 --- a/sd_notify.gemspec +++ b/sd_notify.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.files = ["lib/sd_notify.rb", "LICENSE", "README.md", "CHANGELOG.md"] s.homepage = "https://github.com/agis/ruby-sdnotify" s.license = "MIT" - s.required_ruby_version = ">= 2.3.0" + s.required_ruby_version = ">= 2.2.0" s.add_development_dependency "minitest" s.add_development_dependency "rubocop" From 22940b4a01a916a7b07e58dcf0cf660bfa7bdfee Mon Sep 17 00:00:00 2001 From: Rob Brackett Date: Wed, 26 Jun 2024 15:56:07 -0700 Subject: [PATCH 5/5] `positive?` does not exist in Ruby 2.2 --- lib/sd_notify.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sd_notify.rb b/lib/sd_notify.rb index cce66ba..7d4f087 100644 --- a/lib/sd_notify.rb +++ b/lib/sd_notify.rb @@ -91,7 +91,7 @@ def self.watchdog? # @note Unlike sd_watchdog_enabled(3), this returns seconds, not microseconds. def self.watchdog_interval wd_usec = Integer(ENV["WATCHDOG_USEC"]) - wd_usec.positive? ? wd_usec / 1e6 : 0.0 + wd_usec > 0 ? wd_usec / 1e6 : 0.0 rescue StandardError 0.0 end