david 0.3.0.pre → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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