blender-serf 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cfaa34818e2ebbe4ed6327b625c5927d49291901
4
- data.tar.gz: 167c55fac333e5205c4f1599aad75e68c50c2575
3
+ metadata.gz: 6a360de1c7367a78e1ae4cb4a5a1bb2095f11e3d
4
+ data.tar.gz: 61df6de3ba420247a8e0169005212c1f74baef39
5
5
  SHA512:
6
- metadata.gz: 2cb10ede968b4329a18917e32e9ece5f974f4f1d3a7db8cdcef971641d6d2224014ad37bfe234670dd05cddd69b8294a8c8db597671c3cd48c9c62fc641f6ac7
7
- data.tar.gz: ecab1c61c7e6c29c9906239558a8f81efcbb06360e6e4d88503b564d44b9eaf8fd07a26f3056659d6ce32292eb7e93df97a7ff6c179664f9f46d9e1c77853b5a
6
+ metadata.gz: 43bef41ba273f03c8ebe522e8de7820b215a2fd2606f587cb8916544b2d4d30819e55f8f552d4fbfd1b0a4655aa4ea7183e057aaee58c1503d3769abba33ccac
7
+ data.tar.gz: 4870e0f7baf6977b6ec519d3a997317b9df659cb723eb457be3f1311b4fa68a261d51841b50fb9338185f7cf4a527d2ee2b7f680ccc91cdac246c51b65a2df39
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ gem 'pd-blender', github: 'PagerDuty/blender'
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Blender-Serf
2
+
3
+ Provides [Serf](https://serfdom.io/) based host discovery and job dispatch
4
+ mechanism for [Blender](https://github.com/PagerDuty/blender)
5
+
6
+ ## Installation
7
+ ```sh
8
+ gem install blender-serf
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Config
14
+ Blender-serf uses [Serf's RPC](https://serfdom.io/docs/agent/rpc.html)
15
+ interface for communication between serf and ruby. Following serf RPC connection
16
+ specific configuration options are allowed
17
+ - host (defaults to localhost)
18
+ - port (defaults to 7373)
19
+ - authkey (rpc authenticatioin key, default is nil)
20
+
21
+ Example
22
+ ```ruby
23
+ config(:serf, host: '127.0.0.1', port: 7373, authkey: 'foobar')
24
+ ```
25
+ ### Using serf for host discovery
26
+ Serf provides cluster membership list via `members` and `members_filtered` RPC
27
+ calls, among them Blender-Serf uses `members_filtered` RPC command for host discovery.
28
+ Following is an example of obtaining all live members of a cluster
29
+ ```ruby
30
+ require 'blender/serf'
31
+ members(search(:serf, status: 'alive'))
32
+ ruby_task 'test' do
33
+ execute do |host|
34
+ Blender::Log.info(host)
35
+ end
36
+ end
37
+ ```
38
+ Member list can be obtained based on serf tags or name.
39
+
40
+ Filter by tag expect tags to be supplied as hash (key value pair), which
41
+ allows grouping hosts by multiple tags
42
+ ```ruby
43
+ members(search(:serf, tags: {'role'=>'db'}))
44
+ ruby_task 'test' do
45
+ execute do |host|
46
+ Blender::Log.info(host)
47
+ end
48
+ end
49
+ ```
50
+
51
+ Filter by name, the specification string can be a pattern as well
52
+ ```ruby
53
+ members(search(:serf, name: 'foo-baar-*'))
54
+ ruby_task 'test' do
55
+ execute do |host|
56
+ Blender::Log.info(host)
57
+ end
58
+ end
59
+ ```
60
+
61
+ Combinations of all three filtering mechanism is valid as well
62
+ ```ruby
63
+ members(search(:serf, name: 'foo-baar-*', status: 'alive', tags:{'datacenter' => 'eu-east-2a'}))
64
+ ruby_task 'test'
65
+ execute do |host|
66
+ Blender::Log.info(host)
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### Using serf for job dispatch
72
+
73
+ Serf agents allow job execution via [event handlers](https://serfdom.io/docs/agent/event-handlers.html),
74
+ where the execution details is captured by the handler, while trigger mechanism is controlled by
75
+ serf event. _Blender-Serf_ uses [serf query](https://serfdom.io/docs/commands/query.html) event type
76
+ to dispatch event and check for successfull response. The handler itself, and associated serf' configyration
77
+ needs to be setup externally (bake it in your image, or use a configuration managemenet system like
78
+ chef, puppet, ansible or salt for this).
79
+
80
+ Following example will trigger serf query event `test` against 3 nodes, one by one
81
+ ```ruby
82
+ extend Blender::SerfDSL
83
+ members(['node_1', 'node_2', 'node_3'])
84
+ serf_task 'test'
85
+ ```
86
+ A more elaborate example
87
+ ```ruby
88
+ extend Blender::SerfDSL
89
+ members(['node_1', 'node_2', 'node_3'])
90
+ serf_task 'start_nginx' do
91
+ query 'nginx'
92
+ payload 'start'
93
+ timeout 3
94
+ end
95
+ ```
96
+
97
+ Which might be accmoplished by the following handler script (needs to be present aprior)
98
+ ```ruby
99
+ require 'serfx/utils/handler'
100
+
101
+ include Serfx::Utils::Handler
102
+
103
+
104
+ on :query, 'nginx' do |event|
105
+ case event.payload
106
+ when 'start'
107
+ %x{/etc/init.d/nginx start}
108
+ when 'stop'
109
+ # ...
110
+ when 'check'
111
+ # ...
112
+ end
113
+ end
114
+
115
+ run
116
+
117
+ ```
118
+ ## Supported ruby versions
119
+
120
+ Blender-serf currently support the following MRI versions:
121
+
122
+ * *Ruby 1.9.3*
123
+ * *Ruby 2.1*
124
+
125
+ ## License
126
+
127
+ [Apache 2](http://www.apache.org/licenses/LICENSE-2.0)
128
+
129
+ ## Contributing
130
+
131
+ 1. Fork it ( https://github.com/PagerDuty/blender-serf/fork )
132
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
133
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
134
+ 4. Push to the branch (`git push origin my-new-feature`)
135
+ 5. Create a new Pull Request
data/blender-serf.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'blender-serf'
7
- spec.version = '0.0.1'
7
+ spec.version = '0.1.0'
8
8
  spec.authors = ['Ranjib Dey']
9
9
  spec.email = ['ranjib@pagerduty.com']
10
10
  spec.summary = %q{Serf backend for blender}
@@ -19,6 +19,7 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_dependency 'pd-blender'
21
21
  spec.add_dependency 'serfx'
22
+ spec.add_dependency 'retriable'
22
23
 
23
24
  spec.add_development_dependency 'bundler'
24
25
  spec.add_development_dependency 'rake'
@@ -21,20 +21,38 @@ require 'blender/log'
21
21
  module Blender
22
22
  module Discovery
23
23
  class Serf
24
+ Property = Struct.new(:key, :hook)
25
+ attr_reader :attribute
26
+
24
27
  def initialize(opts = {})
25
- @config = opts
28
+ @config = {}
29
+ @config[:host] = opts[:host] if opts.key?(:host)
30
+ @config[:port] = opts[:port] if opts.key?(:port)
31
+ @config[:authkey] = opts[:authkey] if opts.key?(:authkey)
32
+ @attribute = opts[:attribute] || 'name'
26
33
  end
34
+
27
35
  def search(opts = {})
36
+ opts = {} if opts.nil?
28
37
  tags = opts[:tags] || {}
29
38
  status = opts[:status] || 'alive'
30
39
  name = opts[:name]
31
40
  hosts = []
41
+ Blender::Log.debug("Using Serf agents '#{attribute}' as discovery return values")
42
+ case attribute
43
+ when 'name'
44
+ property = Property.new('Name', lambda{|data| data})
45
+ when 'ipaddress'
46
+ property = Property.new('Addr', lambda{|data| data.unpack('CCCC').join('.')})
47
+ else
48
+ raise ArgumentError, "Attribute can be either 'name' or 'ipaddress', you have provided '#{attribute}'"
49
+ end
32
50
  Blender::Log.debug("Serf memeber list call with arguments: Tags=#{tags}")
33
51
  Blender::Log.debug("Serf memeber list call with arguments: status=#{status}")
34
52
  Blender::Log.debug("Serf memeber list call with arguments: Name=#{name}")
35
53
  Serfx.connect(@config) do |conn|
36
54
  conn.members_filtered(tags, status, name).body['Members'].map do |m|
37
- hosts << m['Name']
55
+ hosts << property.hook.call(m[property.key])
38
56
  end
39
57
  end
40
58
  hosts
@@ -56,7 +56,7 @@ module Blender
56
56
  if command.process
57
57
  command.process.call(responses)
58
58
  end
59
- ExecOutput.new(exit_status(responses, nodes), responses.inspect, '')
59
+ exit_status(responses, nodes)
60
60
  rescue StandardError => e
61
61
  ExecOutput.new( -1, '', e.message)
62
62
  end
@@ -65,9 +65,13 @@ module Blender
65
65
  def exit_status(responses, nodes)
66
66
  case filter_by
67
67
  when :host
68
- responses.size == nodes.size ? 0 : -1
68
+ if responses.size == nodes.size
69
+ ExecOutput.new(0, responses.inspect, '')
70
+ else
71
+ ExecOutput.new(-1, '', "Insufficient number of responses. Expected:#{nodes.size}, Got:#{responses.size}")
72
+ end
69
73
  when :tag, :none
70
- 0
74
+ ExecOutput.new(0, responses.inspect, '')
71
75
  else
72
76
  raise ArgumentError, "Unknown filter_by option: #{filter_by}"
73
77
  end
@@ -20,61 +20,39 @@ require 'blender/log'
20
20
  require 'blender/drivers/serf'
21
21
  require 'json'
22
22
  require 'blender/tasks/serf'
23
+ require 'retriable'
23
24
 
24
25
  module Blender
25
26
  module Driver
26
- class SerfAsync < Serf
27
-
28
- def dup_command(cmd, payload)
29
- Blender::Task::Serf::SerfQuery.new(
30
- cmd.query,
31
- payload,
32
- cmd.timeout,
33
- cmd.noack
34
- )
35
- end
36
-
37
- def start!(cmd, host)
38
- resps = serf_query(dup_command(cmd, 'start'), host)
39
- status = extract_status!(resps.first)
40
- unless status == 'success'
41
- raise RuntimeError, "Failed to start async serf job. Status = #{status}"
42
- end
43
- end
44
-
45
- def finished?(cmd, host)
46
- Blender::Log.debug("Checking for status")
47
- resps = serf_query(dup_command(cmd, 'check'), host)
48
- Blender::Log.debug("Responses: #{resps.inspect}")
49
- Blender::Log.debug("Status:#{extract_status!(resps.first)}")
50
- extract_status!(resps.first) == 'finished'
27
+ class SerfAsync < Blender::Driver::Base
28
+ def initialize(config = {})
29
+ super
30
+ @driver = Blender::Driver::Serf.new(config)
51
31
  end
52
32
 
53
- def reap!(cmd, host)
54
- resps = serf_query(dup_command(cmd, 'reap'), host)
55
- extract_status!(resps.first) == 'success'
56
- end
57
-
58
- def run_command(command, host)
59
- begin
60
- start!(command, host)
61
- until finished?(command, host)
62
- sleep 10
33
+ def execute(tasks, hosts)
34
+ Log.debug("Serf Async query on #{@driver.filter_by}s [#{hosts.inspect}]")
35
+ tasks.each do |task|
36
+ retry_options = task.metadata[:retry_options]
37
+ hosts.each_slice(@driver.concurrency) do |nodes|
38
+ Blender::Log.debug("Start query: #{task.start_query.inspect}")
39
+ start = @driver.run_command(task.start_query.command, nodes)
40
+ if start.exitstatus != 0 and !task.metadata[:ignore_failure]
41
+ raise ExecutionFailed, start.stderr
42
+ end
43
+ Blender::Log.debug("Using check retry options:#{retry_options}")
44
+ Retriable.retriable(retry_options) do
45
+ cmd = @driver.run_command(task.check_query.command, nodes)
46
+ if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
47
+ raise ExecutionFailed, cmd.stderr
48
+ end
49
+ end
50
+ cmd = @driver.run_command(task.stop_query.command, nodes)
51
+ if cmd.exitstatus != 0 and !task.metadata[:ignore_failure]
52
+ raise ExecutionFailed, cmd.stderr
53
+ end
63
54
  end
64
- reap!(command, host)
65
- ExecOutput.new(0, '', '')
66
- rescue StandardError => e
67
- ExecOutput.new( -1, '', e.message + e.backtrace.join("\n"))
68
- end
69
- end
70
-
71
- def extract_status!(res)
72
- payload = JSON.parse(res['Payload'])
73
- unless payload['code'].zero?
74
- raise RuntimeError, "non zero query response code: #{res}"
75
55
  end
76
- Blender::Log.debug("Payload: #{payload['result'].inspect}")
77
- payload['result']['status']
78
56
  end
79
57
  end
80
58
  end
data/lib/blender/serf.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'blender/discoveries/serf'
2
2
  require 'blender/drivers/serf'
3
+ require 'blender/drivers/serf_async'
3
4
  require 'blender/tasks/serf'
5
+ require 'blender/tasks/serf_async'
4
6
  require 'blender/serf_dsl'
@@ -1,3 +1,20 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2015 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
1
18
  module Blender
2
19
  module SerfDSL
3
20
  def serf_task(name, &block)
@@ -5,5 +22,10 @@ module Blender
5
22
  task.instance_eval(&block) if block_given?
6
23
  append_task(:serf, task)
7
24
  end
25
+ def serf_async_task(name, &block)
26
+ task = build_task(name, :serf_async)
27
+ task.instance_eval(&block) if block_given?
28
+ append_task(:serf_async, task)
29
+ end
8
30
  end
9
31
  end
@@ -19,17 +19,12 @@ require 'blender/tasks/base'
19
19
  module Blender
20
20
  module Task
21
21
  class Serf < Blender::Task::Base
22
-
23
22
  SerfQuery = Struct.new(:query, :payload, :timeout, :noack, :process)
24
23
 
25
24
  def initialize(name, metadata = {})
26
25
  super
27
26
  @command = SerfQuery.new
28
- @command.query = name
29
- end
30
-
31
- def execute(&block)
32
- @command.instance_eval(&block)
27
+ @command.query= name
33
28
  end
34
29
 
35
30
  def query(q)
@@ -48,8 +43,12 @@ module Blender
48
43
  @command.noack = bool
49
44
  end
50
45
 
51
- def process(callback)
52
- @command.process = callback
46
+ def process(&block)
47
+ @command.process = block if block
48
+ end
49
+
50
+ def execute(&block)
51
+ @command.instance_eval(&block)
53
52
  end
54
53
 
55
54
  def command
@@ -0,0 +1,81 @@
1
+ #
2
+ # Author:: Ranjib Dey (<ranjib@pagerduty.com>)
3
+ # Copyright:: Copyright (c) 2015 PagerDuty, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ require 'blender/tasks/serf'
18
+
19
+ module Blender
20
+ module Task
21
+ class SerfAsync < Blender::Task::Base
22
+ attr_reader :start_query, :stop_query, :check_query
23
+
24
+ def initialize(name, options = {})
25
+ super
26
+ @start_query = Blender::Task::Serf.new(name)
27
+ @start_query.payload(metadata[:start])
28
+ @start_query.timeout(metadata[:timeout])
29
+ @start_query.no_ack(metadata[:no_ack])
30
+ @start_query.process(&metadata[:process])
31
+
32
+ @stop_query = Blender::Task::Serf.new(name)
33
+ @stop_query.payload(metadata[:stop])
34
+ @stop_query.timeout(metadata[:timeout])
35
+ @stop_query.no_ack(metadata[:no_ack])
36
+ @stop_query.process(&metadata[:process])
37
+
38
+ @check_query = Blender::Task::Serf.new(name)
39
+ @check_query.payload(metadata[:check])
40
+ @check_query.timeout(metadata[:timeout])
41
+ @check_query.no_ack(metadata[:no_ack])
42
+ @check_query.process(&metadata[:process])
43
+ end
44
+
45
+ def start(&block)
46
+ @start_query.instance_eval(&block)
47
+ end
48
+
49
+ def stop(&block)
50
+ @stop_query.instance_eval(&block)
51
+ end
52
+
53
+ def check(&block)
54
+ @check_query.instance_eval(&block)
55
+ end
56
+
57
+ def execute
58
+ end
59
+
60
+ def command
61
+ end
62
+
63
+ def retry_options(options = {})
64
+ @metadata[:retry_options].merge!(options)
65
+ end
66
+
67
+ def default_metadata
68
+ {
69
+ ignore_failure: false,
70
+ start: 'start',
71
+ stop: 'stop',
72
+ check: 'check',
73
+ timeout: 5,
74
+ no_ack: false,
75
+ process: nil,
76
+ retry_options: { max_elapsed_time: 180 }
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'blender/handlers/base'
3
+ require 'blender/cli'
3
4
 
4
5
  describe Blender::Driver::Serf do
5
6
  let(:driver) do
@@ -46,4 +47,26 @@ describe Blender::Driver::Serf do
46
47
  expect(Serfx).to receive(:connect).with(conn_opts).and_yield(conn).exactly(3).times
47
48
  tag_driver.execute(tasks, hosts)
48
49
  end
50
+ context '#CLI' do
51
+ it '#sync' do
52
+ conn = double('serf connection')
53
+ ev = double('serf event')
54
+ allow(conn).to receive(:query).with('test', 'start', Timeout: 7000000000, FilterNodes: ['node1']).and_yield(ev)
55
+ allow(conn).to receive(:query).with('test', 'start', Timeout: 7000000000, FilterNodes: ['node2']).and_yield(ev)
56
+ allow(conn).to receive(:query).with('test', 'start', Timeout: 7000000000, FilterNodes: ['node3']).and_yield(ev)
57
+ allow(Serfx).to receive(:connect).with(authkey: 'foobar').and_yield(conn)
58
+ Blender::CLI.start(%w{-f spec/data/example.rb})
59
+ end
60
+ it '#async' do
61
+ conn = double('serf connection')
62
+ ev = double('serf event')
63
+ allow(ev).to receive(:body).and_return({'success' => true})
64
+ %w{start stop check}.each do |payload|
65
+ allow(conn).to receive(:query).with('chef', payload , Timeout: 5000000000, FilterNodes: ['node1']).and_yield(ev)
66
+ end
67
+ allow(Serfx).to receive(:connect).with(authkey: 'asyncexample').and_yield(conn)
68
+ expect(Retriable).to receive(:retriable).with(max_elapsed_time: 300).and_yield
69
+ Blender::CLI.start(%w{-f spec/data/async_example.rb})
70
+ end
71
+ end
49
72
  end
@@ -0,0 +1,8 @@
1
+ require 'blender/serf'
2
+ extend SerfDSL
3
+ config(:serf_async, authkey: 'asyncexample')
4
+
5
+ serf_async_task 'chef' do
6
+ members ['node1']
7
+ retry_options(max_elapsed_time: 300)
8
+ end
@@ -0,0 +1,10 @@
1
+ require 'blender/serf'
2
+ extend SerfDSL
3
+ config(:serf, authkey: 'foobar')
4
+
5
+ serf_task 'example serf task' do
6
+ query 'test'
7
+ payload 'start'
8
+ timeout 7
9
+ members ['node1', 'node2', 'node3']
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blender-serf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ranjib Dey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-10 00:00:00.000000000 Z
11
+ date: 2015-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pd-blender
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: retriable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -146,6 +160,7 @@ files:
146
160
  - ".gitignore"
147
161
  - ".travis.yml"
148
162
  - Gemfile
163
+ - README.md
149
164
  - Rakefile
150
165
  - blender-serf.gemspec
151
166
  - lib/blender/discoveries/serf.rb
@@ -154,8 +169,11 @@ files:
154
169
  - lib/blender/serf.rb
155
170
  - lib/blender/serf_dsl.rb
156
171
  - lib/blender/tasks/serf.rb
172
+ - lib/blender/tasks/serf_async.rb
157
173
  - spec/blender/discoveries/serf_spec.rb
158
174
  - spec/blender/drivers/serf_spec.rb
175
+ - spec/data/async_example.rb
176
+ - spec/data/example.rb
159
177
  - spec/spec_helper.rb
160
178
  homepage: http://github.com/PagerDuty/blender-serf
161
179
  licenses:
@@ -184,5 +202,7 @@ summary: Serf backend for blender
184
202
  test_files:
185
203
  - spec/blender/discoveries/serf_spec.rb
186
204
  - spec/blender/drivers/serf_spec.rb
205
+ - spec/data/async_example.rb
206
+ - spec/data/example.rb
187
207
  - spec/spec_helper.rb
188
208
  has_rdoc: