david 0.3.0.pre → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +24 -0
  4. data/Gemfile +11 -7
  5. data/Gemfile.lock +155 -19
  6. data/README.md +21 -3
  7. data/Rakefile +5 -0
  8. data/TODO.md +73 -0
  9. data/benchmarks/Gemfile +4 -0
  10. data/benchmarks/Gemfile.lock +31 -0
  11. data/benchmarks/rps.rb +20 -0
  12. data/bin/david +9 -4
  13. data/config.ru +4 -0
  14. data/david.gemspec +10 -4
  15. data/experiments/mcast.rb +37 -0
  16. data/experiments/structs.rb +45 -0
  17. data/{test.rb → experiments/test.rb} +0 -0
  18. data/lib/david.rb +15 -4
  19. data/lib/david/actor.rb +18 -0
  20. data/lib/david/garbage_collector.rb +35 -0
  21. data/lib/david/observe.rb +102 -0
  22. data/lib/david/rails/action_controller/base.rb +11 -0
  23. data/lib/david/railties/config.rb +20 -1
  24. data/lib/david/railties/middleware.rb +18 -6
  25. data/lib/david/request.rb +80 -0
  26. data/lib/david/resource_discovery.rb +92 -0
  27. data/lib/david/resource_discovery_proxy.rb +13 -0
  28. data/lib/david/server.rb +72 -27
  29. data/lib/david/server/constants.rb +48 -0
  30. data/lib/david/server/deduplication.rb +21 -0
  31. data/lib/david/server/mapping.rb +64 -12
  32. data/lib/david/server/multicast.rb +54 -0
  33. data/lib/david/server/options.rb +32 -0
  34. data/lib/david/server/respond.rb +146 -0
  35. data/lib/david/server/utility.rb +1 -6
  36. data/lib/david/show_exceptions.rb +52 -0
  37. data/lib/david/version.rb +2 -1
  38. data/lib/rack/handler/david.rb +16 -6
  39. data/lib/rack/hello_world.rb +23 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/assets/images/.keep +0 -0
  42. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  43. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  44. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  45. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  46. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  47. data/spec/dummy/app/mailers/.keep +0 -0
  48. data/spec/dummy/app/models/.keep +0 -0
  49. data/spec/dummy/app/models/concerns/.keep +0 -0
  50. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  51. data/spec/dummy/bin/bundle +3 -0
  52. data/spec/dummy/bin/rails +4 -0
  53. data/spec/dummy/bin/rake +4 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +29 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/database.yml +25 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +37 -0
  60. data/spec/dummy/config/environments/production.rb +78 -0
  61. data/spec/dummy/config/environments/test.rb +39 -0
  62. data/spec/dummy/config/initializers/assets.rb +8 -0
  63. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  64. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  65. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  66. data/spec/dummy/config/initializers/inflections.rb +16 -0
  67. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  68. data/spec/dummy/config/initializers/session_store.rb +3 -0
  69. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  70. data/spec/dummy/config/locales/en.yml +23 -0
  71. data/spec/dummy/config/routes.rb +58 -0
  72. data/spec/dummy/config/secrets.yml +22 -0
  73. data/spec/dummy/db/test.sqlite3 +0 -0
  74. data/spec/dummy/lib/assets/.keep +0 -0
  75. data/spec/dummy/log/.keep +0 -0
  76. data/spec/dummy/public/404.html +67 -0
  77. data/spec/dummy/public/422.html +67 -0
  78. data/spec/dummy/public/500.html +66 -0
  79. data/spec/dummy/public/favicon.ico +0 -0
  80. data/spec/guerilla_rack_handler_spec.rb +16 -0
  81. data/spec/mapping_spec.rb +56 -0
  82. data/spec/observe_spec.rb +111 -0
  83. data/spec/perf/server_perf_spec.rb +15 -9
  84. data/spec/resource_discovery_spec.rb +65 -0
  85. data/spec/server_spec.rb +306 -0
  86. data/spec/spec_helper.rb +43 -1
  87. data/spec/utility_spec.rb +8 -0
  88. metadata +195 -38
  89. data/lib/david/server/response.rb +0 -124
  90. data/lib/david/well_known.rb +0 -59
data/config.ru CHANGED
@@ -1,4 +1,8 @@
1
+ #\ -o :: -p 5683 -O Log=debug
2
+
1
3
  require_relative 'lib/david'
2
4
  require_relative 'lib/rack/hello_world'
3
5
 
6
+ use Rack::ETag
7
+
4
8
  run Rack::HelloWorld.new
@@ -1,4 +1,5 @@
1
- require_relative 'lib/david/version'
1
+ $:.unshift File.expand_path('lib', File.dirname(__FILE__))
2
+ require 'david/version'
2
3
 
3
4
  Gem::Specification.new do |s|
4
5
  s.name = 'david'
@@ -19,8 +20,13 @@ Gem::Specification.new do |s|
19
20
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
21
  s.require_paths = ['lib']
21
22
 
22
- s.add_dependency 'cbor', '~> 0.5'
23
- s.add_dependency 'celluloid-io', '~> 0.16'
24
- s.add_dependency 'coap', '~> 0'
23
+ s.add_dependency 'celluloid-io', '~> 0.16', '>= 0.16.1'
24
+ s.add_dependency 'coap', '~> 0.1'
25
25
  s.add_dependency 'rack', '~> 1.5'
26
+
27
+ s.add_development_dependency 'rake'
28
+ s.add_development_dependency 'rspec', '~> 3.1'
29
+
30
+ s.add_development_dependency 'rspec-rails', '~> 3.1'
31
+ s.add_development_dependency 'rails', '~> 4.2'
26
32
  end
@@ -0,0 +1,37 @@
1
+ require 'celluloid/io'
2
+ require 'ipaddr'
3
+ require 'socket'
4
+
5
+ ifname, af = ARGV
6
+
7
+ ifname ||= 'lo'
8
+ af ||= 'inet6'
9
+
10
+ ifaddr = Socket.getifaddrs.select { |x| x.name == ifname }.first
11
+ ifindex = ifaddr.ifindex
12
+
13
+ if af == 'inet6'
14
+ maddr = 'ff02::fd'
15
+ # maddr = 'ff05::fd'
16
+
17
+ mreq = IPAddr.new(maddr).hton + [ifindex].pack('i_')
18
+
19
+ sock = Celluloid::IO::UDPSocket.new(Socket::AF_INET6)
20
+ sock.to_io.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_JOIN_GROUP, mreq)
21
+ else
22
+ maddr = '224.0.1.187'
23
+
24
+ # Ignore interface for now.
25
+ mreq = IPAddr.new(maddr).hton + IPAddr.new('0.0.0.0').hton
26
+
27
+ sock = Celluloid::IO::UDPSocket.new
28
+ sock.to_io.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, mreq)
29
+ end
30
+
31
+ puts `netstat -g`
32
+
33
+ if af == 'inet6'
34
+ sock.to_io.setsockopt(Socket::IPPROTO_IPV6, Socket::IPV6_LEAVE_GROUP, mreq)
35
+ else
36
+ sock.to_io.setsockopt(Socket::IPPROTO_IP, Socket::IP_DROP_MEMBERSHIP, mreq)
37
+ end
@@ -0,0 +1,45 @@
1
+ require 'benchmark'
2
+
3
+ Foo = Struct.new(:a, :b, :c)
4
+ Bar = Struct.new(:a, :b, :c)
5
+
6
+ data = []
7
+
8
+ arrays = {}
9
+ structs = {}
10
+
11
+ 1000000.times do
12
+ a, b, c = ([0]*3).map { rand(0..9999) }
13
+ data.push [a, b, c]
14
+ end
15
+
16
+ Benchmark.bm do |x|
17
+ x.report('fill arrays ') do
18
+ data.each do |d|
19
+ a, b, c = d
20
+ arrays[[a, b, c]] = [a, b, c]
21
+ end
22
+ end
23
+
24
+ x.report('fill structs') do
25
+ data.each do |d|
26
+ a, b, c = d
27
+ structs[Foo.new(a, b, c)] = Bar.new(a, b, c)
28
+ end
29
+ end
30
+
31
+
32
+ x.report('read arrays ') do
33
+ 100000.times do
34
+ a, b, c = data.sample
35
+ arrays[[a, b, c]]
36
+ end
37
+ end
38
+
39
+ x.report('read structs') do
40
+ 100000.times do
41
+ a, b, c = data.sample
42
+ structs[Foo.new(a, b, c)]
43
+ end
44
+ end
45
+ end
File without changes
@@ -2,10 +2,15 @@ module David
2
2
  end
3
3
 
4
4
  require 'bundler/setup'
5
- Bundler.require
5
+ Bundler.require(:default, :cbor)
6
6
 
7
7
  unless defined? JRuby
8
- require 'cbor'
8
+ begin
9
+ require 'cbor'
10
+ rescue LoadError
11
+ $stderr << "`gem install cbor` for transparent JSON/CBOR conversion "
12
+ $stderr << "support.\n"
13
+ end
9
14
  end
10
15
 
11
16
  require 'celluloid'
@@ -16,7 +21,7 @@ require 'rack'
16
21
 
17
22
  include CoRE
18
23
 
19
- $: << File.dirname(__FILE__)
24
+ $:.unshift(File.expand_path(File.dirname(__FILE__)))
20
25
 
21
26
  require 'rack/hello_world'
22
27
  require 'rack/handler/david'
@@ -24,10 +29,16 @@ require 'rack/handler/coap'
24
29
 
25
30
  require 'david/guerilla/rack/handler'
26
31
 
27
- require 'david/version'
32
+ require 'david/actor'
33
+ require 'david/garbage_collector'
34
+ require 'david/observe'
35
+ require 'david/request'
28
36
  require 'david/server'
37
+ require 'david/version'
29
38
 
30
39
  if defined? Rails
40
+ require 'david/rails/action_controller/base'
41
+
31
42
  require 'david/railties/config'
32
43
  require 'david/railties/middleware'
33
44
  end
@@ -0,0 +1,18 @@
1
+ module David
2
+ module Actor
3
+ def self.included(base)
4
+ base.send(:include, Celluloid)
5
+ end
6
+
7
+ protected
8
+
9
+ def log
10
+ @log ||= Celluloid.logger
11
+ @log ||= ::Logger.new(nil)
12
+ end
13
+
14
+ def server
15
+ Celluloid::Actor[:server]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module David
2
+ class GarbageCollector
3
+ include Actor
4
+
5
+ def initialize(tick_interval = 10, dedup_timeout = 5)
6
+ @tick_interval = tick_interval
7
+ @dedup_timeout = dedup_timeout
8
+
9
+ async.run
10
+
11
+ log.debug 'GarbageCollector initialized'
12
+ end
13
+
14
+ def clean_dedup_cache(now = Time.now.to_i)
15
+ server.dedup_cache.delete_if do |k, v|
16
+ now - v[1] >= @dedup_timeout
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def run
23
+ loop { sleep @tick_interval; tick }
24
+ end
25
+
26
+ def tick
27
+ unless server.dedup_cache.empty?
28
+ log.debug 'GarbageCollector tick'
29
+
30
+ clean_dedup_cache
31
+ log.debug server.dedup_cache
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,102 @@
1
+ module David
2
+ class Observe < Hash
3
+ include Actor
4
+
5
+ alias_method :_delete, :delete
6
+ alias_method :_include?, :include?
7
+
8
+ def initialize(tick_interval = 3)
9
+ @tick_interval = tick_interval
10
+ async.run
11
+
12
+ log.debug 'Observe initialized'
13
+ end
14
+
15
+ def add(request, env, etag)
16
+ request.message.mid = nil
17
+ request.message.options.delete(:observe)
18
+
19
+ # TODO Check if Array or Struct is more efficient.
20
+ self[[request.host, request.token]] ||=
21
+ [0, request, env, etag, Time.now.to_i]
22
+ end
23
+
24
+ def delete(request)
25
+ _delete([request.host, request.token])
26
+ end
27
+
28
+ def include?(request)
29
+ _include?([request.host, request.token])
30
+ end
31
+
32
+ def to_s
33
+ self.map { |k, v| [*k, v[2]['PATH_INFO'], v[0]].inspect }.join(', ')
34
+ end
35
+
36
+ private
37
+
38
+ def bump(key, n, response)
39
+ self[key][0] = n
40
+ self[key][3] = response.options[:etag]
41
+ self[key][4] = Time.now.to_i
42
+ end
43
+
44
+ # TODO If ETag did not change but max-age of last notification is expired,
45
+ # return empty 2.03.
46
+ def handle_update(key)
47
+ n, request, env, etag = self[key]
48
+ n += 1
49
+
50
+ response, options = server.respond(request, env)
51
+
52
+ return if response.nil?
53
+
54
+ if response.mcode[0] != 2
55
+ self.delete(request)
56
+ request(response, request.host, request.port, options)
57
+ return
58
+ end
59
+
60
+ if etag != response.options[:etag]
61
+ response.options[:observe] = n
62
+
63
+ answer = request(response, request.host, request.port, options)
64
+
65
+ if !answer.nil? && answer.tt == :rst
66
+ self.delete(request)
67
+ return
68
+ end
69
+
70
+ bump(key, n, response)
71
+ end
72
+ end
73
+
74
+ def request(message, host, port, options)
75
+ answer = nil
76
+
77
+ log.debug message.inspect
78
+
79
+ begin
80
+ options.merge!(retransmit: false, socket: server.socket)
81
+ answer = CoAP::Transmission.request(message, host, port, options).last
82
+ log.debug answer.inspect
83
+ rescue Timeout::Error, RuntimeError
84
+ end
85
+
86
+ answer
87
+ end
88
+
89
+ def run
90
+ loop { tick; sleep @tick_interval }
91
+ end
92
+
93
+ def tick
94
+ unless self.empty?
95
+ log.debug 'Observe tick'
96
+ log.debug self
97
+ end
98
+
99
+ self.each_key { |key| async.handle_update(key) }
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,11 @@
1
+ class ActionController::Base
2
+ def self.discoverable(options)
3
+ discovery_actor.register(self, options)
4
+ end
5
+
6
+ protected
7
+
8
+ def self.discovery_actor
9
+ Celluloid::Actor[:discovery]
10
+ end
11
+ end
@@ -2,8 +2,27 @@ module David
2
2
  module Railties
3
3
  class Config < Rails::Railtie
4
4
  config.coap = ActiveSupport::OrderedOptions.new
5
- config.coap.cbor = true
5
+
6
+ # Blockwise transfer
7
+ config.coap.block = true
8
+
9
+ # Transparent JSON<>CBOR conversion
10
+ config.coap.cbor = false
11
+
12
+ # Default Content-Type if HTTP_ACCEPT is empty
13
+ config.coap.default_format = nil
14
+
15
+ # Multicast
16
+ config.coap.multicast = true
17
+
18
+ # Observe
19
+ config.coap.observe = true
20
+
21
+ # david as default Rack handler (`rails s` starts david)
6
22
  config.coap.only = true
23
+
24
+ # Resource Discovery
25
+ config.coap.resource_discovery = true
7
26
  end
8
27
  end
9
28
  end
@@ -1,4 +1,5 @@
1
- require 'david/well_known'
1
+ require 'david/resource_discovery_proxy'
2
+ require 'david/show_exceptions'
2
3
 
3
4
  module David
4
5
  module Railties
@@ -10,21 +11,32 @@ module David
10
11
  ActionDispatch::RemoteIp,
11
12
  ActionDispatch::RequestId,
12
13
  ActionDispatch::Session::CookieStore,
13
- # ActionDispatch::ShowExceptions,
14
14
  Rack::ConditionalGet,
15
- # Rack::ETag,
16
15
  Rack::Head,
17
- Rack::Lock,
18
16
  Rack::MethodOverride,
19
17
  Rack::Runtime,
20
18
  ]
21
19
 
22
20
  initializer 'david.clear_out_middleware' do |app|
21
+ # Remove middleware not applicable to CoAP
23
22
  if config.coap.only
24
- UNWANTED.each { |klass| app.middleware.delete klass }
23
+ UNWANTED.each { |klass| app.middleware.delete(klass) }
25
24
  end
26
25
 
27
- app.middleware.insert_after(Rails::Rack::Logger, David::WellKnown)
26
+ # Enable multithreading for Rails
27
+ app.middleware.delete(Rack::Lock)
28
+
29
+ # Show exceptions as JSON
30
+ app.middleware.swap \
31
+ ActionDispatch::ShowExceptions,
32
+ David::ShowExceptions
33
+
34
+ # Include Resource Discovery middleware
35
+ if config.coap.resource_discovery
36
+ app.middleware.insert_after \
37
+ David::ShowExceptions,
38
+ David::ResourceDiscoveryProxy
39
+ end
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,80 @@
1
+ class Request < Struct.new(:host, :port, :message, :ancillary, :options)
2
+ def accept
3
+ message.options[:accept]
4
+ end
5
+
6
+ def block
7
+ @block ||= if message.options[:block2].nil?
8
+ CoAP::Block.new(0, false, 1024)
9
+ else
10
+ CoAP::Block.new(message.options[:block2]).decode
11
+ end
12
+ end
13
+
14
+ def con?
15
+ message.tt == :con
16
+ end
17
+
18
+ def delete?
19
+ message.mcode == :delete
20
+ end
21
+
22
+ def etag
23
+ message.options[:etag]
24
+ end
25
+
26
+ def get_etag?
27
+ message.options[:etag].nil? && get?
28
+ end
29
+
30
+ def get?
31
+ message.mcode == :get
32
+ end
33
+
34
+ def idempotent?
35
+ get? || put? || delete?
36
+ end
37
+
38
+ def mid
39
+ message.mid
40
+ end
41
+
42
+ def multicast?
43
+ a = ancillary
44
+ return false if a.nil?
45
+
46
+ return @multicast unless @multicast.nil?
47
+
48
+ @multicast =
49
+ a.cmsg_is?(:IP, :PKTINFO) && a.ip_pktinfo[0].ipv4_multicast? ||
50
+ a.cmsg_is?(:IPV6, :PKTINFO) && a.ipv6_pktinfo[0].ipv6_multicast?
51
+ end
52
+
53
+ def non?
54
+ message.tt == :non
55
+ end
56
+
57
+ def observe?
58
+ !message.options[:observe].nil?
59
+ end
60
+
61
+ def post?
62
+ message.mcode == :post
63
+ end
64
+
65
+ def proxy?
66
+ !(message.options[:proxy_uri].nil? && message.options[:proxy_scheme].nil?)
67
+ end
68
+
69
+ def put?
70
+ message.mcode == :put
71
+ end
72
+
73
+ def token
74
+ message.options[:token]
75
+ end
76
+
77
+ def valid_method?
78
+ CoAP::METHODS.include?(message.mcode)
79
+ end
80
+ end