SNMP-Get blocks
paddor opened this issue · 7 comments
I expect the SNMP-GET request not to block:
require 'bundler/setup'
require 'snmp'
require 'async'
require 'async/io'
require 'async/io/udp_socket'
OID_SYSNAME = "1.3.6.1.2.1.1.5.0"
OID_SYSLOCATION = "1.3.6.1.2.1.1.6.0"
module SNMP
include Async::IO
end
Async do |task|
task.async do
puts "BEGIN"
puts "Ruby version: #{RUBY_VERSION}"
end
task.async do |t|
t.sleep 0.5
manager = SNMP::Manager.new(host: '127.0.0.1', port: 1024, community: 'linksys-system') #do |manager|
puts "Created manager."
response = manager.get([OID_SYSNAME, OID_SYSLOCATION])
puts "Got response."
response.each_varbind do |vb|
puts "#{vb.name.to_s} #{vb.value.to_s} #{vb.value.asn1_type}"
end
end
task.async do
puts "MIDDLE"
end
task.async do |t|
5.times do
puts "X"
t.sleep 0.5
end
puts "END"
end
end
To run an agent locally:
$ pip install snmpsim
$ mkdir snmp-data
$ cat > snmp-data/linksys-system.snmprec
1.3.6.1.2.1.1.1.0|4|BEFSX41
1.3.6.1.2.1.1.2.0|6|1.3.6.1.4.1.3955.1.1
1.3.6.1.2.1.1.3.0|67|638239
1.3.6.1.2.1.1.4.0|4|Linksys
1.3.6.1.2.1.1.5.0|4|isp-gw
1.3.6.1.2.1.1.6.0|4|4, Petersburger strasse, Berlin, Germany
1.3.6.1.2.1.1.8.0|67|4
^D
$ snmpsimd.py --data-dir=snmp-data/ --agent-udpv4-endpoint=127.0.0.1:1024 --log-level=debug --v2c-arch
When the SNMP agent is running:
$ ruby snmp.rb
BEGIN
Ruby version: 3.0.0
MIDDLE
X
Created manager.
Got response.
SNMPv2-MIB::sysName.0 isp-gw OCTET STRING
SNMPv2-MIB::sysLocation.0 4, Petersburger strasse, Berlin, Germany OCTET STRING
X
X
X
X
END
When it's not running:
$ ruby snmp.rb
BEGIN
Ruby version: 3.0.0
MIDDLE
X
Created manager.
0.0s error: Async::Task [oid=0xca8] [pid=17081] [2020-12-10 13:50:35 +0100]
| SNMP::RequestTimeout: host 127.0.0.1 not responding
| → /home/user/dev/oss/ruby-snmp/lib/snmp/manager.rb:246 in `get'
| snmp.rb:24 in `block (2 levels) in <main>'
| /home/user/dev/oss/async/lib/async/task.rb:265 in `block in make_fiber'
X
X
X
X
END
I expect something like:
$ ruby snmp.rb
BEGIN
Ruby version: 3.0.0
MIDDLE
X
Created manager.
X
X
X
X
END
0.0s error: Async::Task [oid=0xca8] [pid=17081] [2020-12-10 13:50:35 +0100]
| SNMP::RequestTimeout: host 127.0.0.1 not responding
| → /home/user/dev/oss/ruby-snmp/lib/snmp/manager.rb:246 in `get'
| snmp.rb:24 in `block (2 levels) in <main>'
| /home/user/dev/oss/async/lib/async/task.rb:265 in `block in make_fiber'
Ruby-SNMP uses stdlib's Timeout.timeout
, which uses Kernel#sleep
. AFAIK in Ruby 3.0 Kernel#sleep
should be aware of the Fiber scheduler?
Ruby: 3.0.0preview2
async: HEAD
async-io: HEAD
ruby-snmp: 1.3.2
Yes, this seems reasonable. However, I believe Timeout.timeout
uses separate thread with sleep, so the scheduler is not active in that thread. Then, it uses Thread.raise
? It will cause problems in scheduler unless there is some way for us to hook that operation.. Timeout.timeout
is bad design. However, perhaps we can add hook for Thread.raise
.
We could probably make Timeout.timeout
delegate to the scheduler.... hmmmm...
Good idea. I tried to implement that. It seems to work fine if the host is unreachable. But if it's reachable, I get #<LocalJumpError: unexpected return>
from the return
statement in SNMP::Manager#try_request
.
Here's timeout.rb for Async-IO:
require 'timeout'
module Async
module IO
module Timeout
Error = ::Timeout::Error
module_function
def timeout(sec, klass = nil, message = nil) #:yield: +sec+
return yield(sec) if sec == nil or sec.zero?
message ||= "execution expired".freeze
klass ||= Error
stopper = nil
task = Async annotation: 'timeout task' do
# warn "#{self.class}: starting task with timeout"
yield.tap do
# warn "#{self.class}: task with timeout finished in time"
stopper.stop if stopper
end
end
stopper = Async annotation: 'stopper' do |t|
t.sleep sec
if task&.running?
# warn "#{self.class}: expired. stopping task"
task.stop
raise klass, message
end
end
task.wait
rescue
# warn "#{self.class}: raising error #{$!.inspect}"
raise
end
end
end
end
Any ideas?
Also, in my test script, I had to include Async::IO
directly into SNMP::Manager
for some reason:
module SNMP
include Async::IO
class Manager
include Async::IO
end
end
Instead of:
task.stop
raise klass, message
I think you need:
task.raise(klass, message)
or something like that.
Async does not define Async::Task#raise
. I just tried calling it and it raised NoMethodError
because that's Kernel#raise
.
I tried to use Async::Task#fail!
to do this, which looked okay, but when the SNMP simulator is running, I still get LocalJumpError: unexpected return
from SNMP::Manager#try_request
. That method looks like this:
def try_request(request, community=@community, host=@host, port=@port)
(@retries + 1).times do |n|
send_request(request, community, host, port)
begin
Timeout.timeout(@timeout) do
return get_response(request)
end
rescue Timeout::Error
# no action - try again
rescue => e
warn e.to_s
end
end
raise RequestTimeout, "host #{config[:host]} not responding", caller
end
Any way to make this work without modifying ruby-snmp?
I can confirm this now works without any modifications to the normal usage of SNMP::Manager
.
#! /usr/bin/env ruby
require 'bundler/setup'
require 'snmp'
require 'async'
OID_SYSNAME = "1.3.6.1.2.1.1.5.0"
OID_SYSLOCATION = "1.3.6.1.2.1.1.6.0"
Async do |task|
task.async do
puts "BEGIN"
puts "Ruby version: #{RUBY_VERSION}"
end
task.async do |t|
t.sleep 0.5
manager = SNMP::Manager.new(host: '127.0.0.1', port: 1024, community: 'linksys-system') #do |manager|
puts "Created manager."
response = manager.get([OID_SYSNAME, OID_SYSLOCATION])
puts "Got response."
response.each_varbind do |vb|
puts "#{vb.name.to_s} #{vb.value.to_s} #{vb.value.asn1_type}"
end
end
task.async do
puts "MIDDLE"
end
task.async do |t|
5.times do
puts "X"
t.sleep 0.5
end
puts "END"
end
end
This works as expected whether the SNMP agent is running or not.
Wow thanks for the follow up. Glad to see it’s all working now!