postburner 0.9.0.rc.1 → 1.0.0.pre.1

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.
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Configuration system for Postburner workers and connections.
5
+ #
6
+ # Supports both programmatic configuration and YAML file loading.
7
+ # Configuration can be set per-environment and controls worker behavior,
8
+ # Beanstalkd connection, and execution strategies.
9
+ #
10
+ # @example Programmatic configuration
11
+ # Postburner.configure do |config|
12
+ # config.beanstalk_url = 'beanstalk://localhost:11300'
13
+ # config.worker_type = :threads_on_fork
14
+ # config.logger = Rails.logger
15
+ # end
16
+ #
17
+ # @example Loading from YAML
18
+ # config = Postburner::Configuration.load_yaml('config/postburner.yml', 'production')
19
+ #
20
+ class Configuration
21
+ attr_accessor :beanstalk_url, :logger, :queues, :default_queue, :default_priority, :default_ttr, :default_threads, :default_forks, :default_gc_limit
22
+
23
+ # @param options [Hash] Configuration options
24
+ # @option options [String] :beanstalk_url Beanstalkd URL (default: ENV['BEANSTALK_URL'] or localhost)
25
+ # @option options [Logger] :logger Logger instance (default: Rails.logger)
26
+ # @option options [Hash] :queues Queue configurations
27
+ # @option options [String] :default_queue Default queue name (default: 'default')
28
+ # @option options [Integer] :default_priority Default job priority (default: 65536, lower = higher priority)
29
+ # @option options [Integer] :default_ttr Default time-to-run in seconds (default: 300)
30
+ # @option options [Integer] :default_threads Default thread count per queue (default: 1)
31
+ # @option options [Integer] :default_forks Default fork count per queue (default: 0, single process)
32
+ # @option options [Integer] :default_gc_limit Default GC limit for worker restarts (default: nil, no limit)
33
+ #
34
+ def initialize(options = {})
35
+ @beanstalk_url = options[:beanstalk_url] || ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300'
36
+ @logger = options[:logger] || (defined?(Rails) ? Rails.logger : Logger.new(STDOUT))
37
+ @queues = options[:queues] || { 'default' => {} }
38
+ @default_queue = options[:default_queue] || 'default'
39
+ @default_priority = options[:default_priority] || 65536
40
+ @default_ttr = options[:default_ttr] || 300
41
+ @default_threads = options[:default_threads] || 1
42
+ @default_forks = options[:default_forks] || 0
43
+ @default_gc_limit = options[:default_gc_limit]
44
+ end
45
+
46
+ # Loads configuration from a YAML file.
47
+ #
48
+ # @param path [String] Path to YAML configuration file
49
+ # @param env [String] Environment name (e.g., 'production', 'development')
50
+ # @param worker_name [String, nil] Worker name to load (nil = auto-select if single worker)
51
+ #
52
+ # @return [Configuration] Configured instance
53
+ #
54
+ # @raise [Errno::ENOENT] if file doesn't exist
55
+ # @raise [ArgumentError] if environment not found in YAML
56
+ # @raise [ArgumentError] if multiple workers defined but worker_name not specified
57
+ # @raise [ArgumentError] if worker_name specified but not found
58
+ #
59
+ # @example Single worker (auto-selected)
60
+ # config = Postburner::Configuration.load_yaml('config/postburner.yml', 'production')
61
+ #
62
+ # @example Multiple workers (must specify)
63
+ # config = Postburner::Configuration.load_yaml('config/postburner.yml', 'production', 'imports')
64
+ #
65
+ def self.load_yaml(path, env = 'development', worker_name = nil)
66
+ yaml = YAML.load_file(path)
67
+ env_config = yaml[env.to_s] || yaml[env.to_sym]
68
+
69
+ raise ArgumentError, "Environment '#{env}' not found in #{path}" unless env_config
70
+
71
+ workers = env_config['workers']
72
+ raise ArgumentError, "No 'workers:' section found in #{path} for environment '#{env}'" unless workers
73
+
74
+ # Auto-select single worker or validate worker_name
75
+ if worker_name.nil?
76
+ if workers.size == 1
77
+ worker_name = workers.keys.first
78
+ else
79
+ raise ArgumentError, "Configuration has multiple workers, but --worker not specified\nAvailable workers: #{workers.keys.join(', ')}\nUsage: bin/postburner --worker <name>"
80
+ end
81
+ else
82
+ unless workers.key?(worker_name)
83
+ raise ArgumentError, "Worker '#{worker_name}' not found in #{path}\nAvailable workers: #{workers.keys.join(', ')}"
84
+ end
85
+ end
86
+
87
+ worker_config = workers[worker_name]
88
+
89
+ # Convert queue array to hash format expected by rest of system
90
+ queue_list = worker_config['queues'] || []
91
+ queues_hash = {}
92
+ queue_list.each do |queue_name|
93
+ queues_hash[queue_name] = {}
94
+ end
95
+
96
+ options = {
97
+ beanstalk_url: env_config['beanstalk_url'],
98
+ queues: queues_hash,
99
+ default_queue: worker_config['default_queue'] || env_config['default_queue'],
100
+ default_priority: worker_config['default_priority'] || env_config['default_priority'],
101
+ default_ttr: worker_config['default_ttr'] || env_config['default_ttr'],
102
+ default_threads: worker_config['default_threads'] || env_config['default_threads'],
103
+ default_forks: worker_config['default_forks'] || env_config['default_forks'],
104
+ default_gc_limit: worker_config['default_gc_limit'] || env_config['default_gc_limit']
105
+ }
106
+
107
+ new(options)
108
+ end
109
+
110
+ # Returns queue configuration for a specific queue name.
111
+ #
112
+ # @param queue_name [String, Symbol] Name of the queue
113
+ #
114
+ # @return [Hash] Queue configuration with threads, gc_limit, etc.
115
+ #
116
+ # @example
117
+ # config.queue_config('critical') # => { threads: 1, gc_limit: 100 }
118
+ #
119
+ def queue_config(queue_name)
120
+ @queues[queue_name.to_s] || @queues[queue_name.to_sym] || {}
121
+ end
122
+
123
+ # Returns array of all configured queue names.
124
+ #
125
+ # @return [Array<String>] Queue names
126
+ #
127
+ # @example
128
+ # config.queue_names # => ['default', 'critical', 'mailers']
129
+ #
130
+ def queue_names
131
+ @queues.keys.map(&:to_s)
132
+ end
133
+
134
+ # Expands queue name to full tube name with environment prefix.
135
+ #
136
+ # Converts a simple queue name (e.g., 'default', 'critical') to the full
137
+ # Beanstalkd tube name with environment namespace (e.g.,
138
+ # 'postburner.production.critical').
139
+ #
140
+ # @param queue_name [String, Symbol, nil] Base queue name (defaults to configured default_queue)
141
+ # @param env [String, Symbol, nil] Environment name (defaults to Rails.env or 'development')
142
+ #
143
+ # @return [String] Full tube name with environment prefix
144
+ #
145
+ # @example
146
+ # config.expand_tube_name('critical', 'production')
147
+ # # => "postburner.production.critical"
148
+ #
149
+ # @example With configured default
150
+ # config.default_queue = 'background'
151
+ # config.expand_tube_name
152
+ # # => "postburner.development.background"
153
+ #
154
+ def expand_tube_name(queue_name = nil, env = nil)
155
+ env ||= defined?(Rails) ? Rails.env : nil
156
+ queue_name ||= @default_queue
157
+ [
158
+ 'postburner',
159
+ env,
160
+ queue_name,
161
+ ].compact.join('.')
162
+ end
163
+
164
+
165
+ # Returns array of expanded tube names with environment prefix.
166
+ #
167
+ # @param env [String, Symbol, nil] Environment name (defaults to Rails.env or 'development')
168
+ #
169
+ # @return [Array<String>] Array of expanded tube names
170
+ #
171
+ # @example
172
+ # config.expanded_tube_names('production') # => ['postburner.production.default', 'postburner.production.critical']
173
+ def expanded_tube_names(env = nil)
174
+ queue_names.map { |q| expand_tube_name(q, env) }
175
+ end
176
+ end
177
+
178
+ # Returns the global configuration instance.
179
+ #
180
+ # @return [Configuration] Global configuration
181
+ #
182
+ def self.configuration
183
+ @configuration ||= Configuration.new
184
+ end
185
+
186
+ # Configures Postburner via block.
187
+ #
188
+ # @yield [config] Configuration instance
189
+ # @yieldparam config [Configuration] The configuration to modify
190
+ #
191
+ # @return [void]
192
+ #
193
+ # @example
194
+ # Postburner.configure do |config|
195
+ # config.beanstalk_url = 'beanstalk://localhost:11300'
196
+ # config.worker_type = :threads_on_fork
197
+ # end
198
+ #
199
+ def self.configure
200
+ yield(configuration)
201
+ end
202
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Wrapper around Beaneater connection with automatic reconnection.
5
+ #
6
+ # Provides a simplified interface to Beanstalkd via Beaneater, with
7
+ # automatic connection management and reconnection on failures.
8
+ #
9
+ # @example Direct usage
10
+ # conn = Postburner::Connection.new
11
+ # conn.tubes['default'].put('job data')
12
+ #
13
+ # @example With reconnection
14
+ # conn = Postburner::Connection.new
15
+ # conn.reconnect! # Force fresh connection
16
+ #
17
+ class Connection
18
+ attr_reader :url
19
+
20
+ # @param url [String] Beanstalkd URL (e.g., 'beanstalk://localhost:11300')
21
+ #
22
+ def initialize(url = nil)
23
+ @url = url || Postburner.configuration.beanstalk_url
24
+ @pool = nil
25
+ connect!
26
+ end
27
+
28
+ # Returns the tubes interface for Beanstalkd operations.
29
+ #
30
+ # Automatically ensures connection is active before returning.
31
+ #
32
+ # @return [Beaneater::Tubes] Tubes interface
33
+ #
34
+ # @raise [Beaneater::NotConnected] if connection fails
35
+ #
36
+ # @example
37
+ # conn.tubes['critical'].put('{"job": "data"}')
38
+ # conn.tubes.watch!('default', 'critical')
39
+ #
40
+ def tubes
41
+ ensure_connected!
42
+ @pool.tubes
43
+ end
44
+
45
+ # Returns the underlying Beaneater pool.
46
+ #
47
+ # @return [Beaneater::Pool] Beaneater connection pool
48
+ #
49
+ # @example
50
+ # conn.beanstalk.stats
51
+ #
52
+ def beanstalk
53
+ ensure_connected!
54
+ @pool
55
+ end
56
+
57
+ # Checks if currently connected to Beanstalkd.
58
+ #
59
+ # @return [Boolean] true if connected, false otherwise
60
+ #
61
+ def connected?
62
+ @pool && @pool.respond_to?(:connection) && @pool.connection
63
+ rescue
64
+ false
65
+ end
66
+
67
+ # Forces reconnection to Beanstalkd.
68
+ #
69
+ # Closes existing connection (if any) and establishes a fresh one.
70
+ # Use this when connection has gone stale or after network issues.
71
+ #
72
+ # @return [void]
73
+ #
74
+ # @example
75
+ # conn.reconnect!
76
+ #
77
+ def reconnect!
78
+ close
79
+ connect!
80
+ end
81
+
82
+ # Closes the Beanstalkd connection.
83
+ #
84
+ # @return [void]
85
+ #
86
+ def close
87
+ @pool&.close rescue nil
88
+ @pool = nil
89
+ end
90
+
91
+ private
92
+
93
+ # Establishes connection to Beanstalkd.
94
+ #
95
+ # @return [void]
96
+ #
97
+ # @raise [Beaneater::NotConnected] if connection fails
98
+ #
99
+ def connect!
100
+ # Beaneater expects 'host:port' format, not 'beanstalk://host:port'
101
+ clean_url = @url.sub(%r{^beanstalk://}, '')
102
+ @pool = Beaneater.new(clean_url)
103
+ end
104
+
105
+ # Ensures connection is active, reconnecting if necessary.
106
+ #
107
+ # @return [void]
108
+ #
109
+ def ensure_connected!
110
+ reconnect! unless connected?
111
+ end
112
+ end
113
+ end
@@ -1,4 +1,4 @@
1
- require 'backburner'
1
+ require 'beaneater'
2
2
  require 'haml-rails'
3
3
 
4
4
  module Postburner
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Queue configuration module for Postburner::Job classes.
5
+ #
6
+ # Provides DSL methods for configuring queue behavior (name, priority, TTR, retries).
7
+ # Replaces Backburner::Queue with cleaner implementation that doesn't interfere
8
+ # with ActiveSupport::Callbacks.
9
+ #
10
+ # @example Basic usage
11
+ # class ProcessPayment < Postburner::Job
12
+ # queue 'critical'
13
+ # queue_priority 0
14
+ # queue_ttr 300
15
+ # queue_max_job_retries 3
16
+ #
17
+ # def perform(args)
18
+ # # ...
19
+ # end
20
+ # end
21
+ #
22
+ module QueueConfig
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ class_attribute :postburner_queue_name, default: 'default'
27
+ class_attribute :postburner_priority, default: nil
28
+ class_attribute :postburner_ttr, default: nil
29
+ class_attribute :postburner_max_retries, default: nil
30
+ class_attribute :postburner_retry_delay, default: nil
31
+ end
32
+
33
+ class_methods do
34
+ # Sets or returns the queue name.
35
+ #
36
+ # @param name [String, Symbol, nil] Queue name to set, or nil to get current value
37
+ #
38
+ # @return [String, nil] Current queue name when getting, nil when setting
39
+ #
40
+ # @example Set queue
41
+ # queue 'critical'
42
+ #
43
+ # @example Get queue
44
+ # ProcessPayment.queue # => 'critical'
45
+ #
46
+ def queue(name = nil)
47
+ if name
48
+ self.postburner_queue_name = name.to_s
49
+ nil # Return nil to avoid callback interference
50
+ else
51
+ postburner_queue_name
52
+ end
53
+ end
54
+
55
+ # Sets or returns the queue priority.
56
+ #
57
+ # Lower numbers = higher priority in Beanstalkd.
58
+ #
59
+ # @param pri [Integer, nil] Priority to set (0-4294967295), or nil to get current value
60
+ #
61
+ # @return [Integer, nil] Current priority when getting, nil when setting
62
+ #
63
+ # @example Set priority
64
+ # queue_priority 0 # Highest priority
65
+ #
66
+ # @example Get priority
67
+ # ProcessPayment.queue_priority # => 0
68
+ #
69
+ def queue_priority(pri = nil)
70
+ if pri
71
+ self.postburner_priority = pri
72
+ nil
73
+ else
74
+ postburner_priority
75
+ end
76
+ end
77
+
78
+ # Sets or returns the queue TTR (time-to-run).
79
+ #
80
+ # Number of seconds Beanstalkd will wait for job completion before
81
+ # making it available again.
82
+ #
83
+ # @param ttr [Integer, nil] Timeout in seconds, or nil to get current value
84
+ #
85
+ # @return [Integer, nil] Current TTR when getting, nil when setting
86
+ #
87
+ # @example Set TTR
88
+ # queue_ttr 300 # 5 minutes
89
+ #
90
+ # @example Get TTR
91
+ # ProcessPayment.queue_ttr # => 300
92
+ #
93
+ def queue_ttr(ttr = nil)
94
+ if ttr
95
+ self.postburner_ttr = ttr
96
+ nil
97
+ else
98
+ postburner_ttr
99
+ end
100
+ end
101
+
102
+ # Sets or returns maximum number of job retries.
103
+ #
104
+ # @param retries [Integer, nil] Max retries, or nil to get current value
105
+ #
106
+ # @return [Integer, nil] Current max retries when getting, nil when setting
107
+ #
108
+ # @example Set max retries
109
+ # queue_max_job_retries 3
110
+ #
111
+ # @example Get max retries
112
+ # ProcessPayment.queue_max_job_retries # => 3
113
+ #
114
+ def queue_max_job_retries(retries = nil)
115
+ if retries
116
+ self.postburner_max_retries = retries
117
+ nil
118
+ else
119
+ postburner_max_retries
120
+ end
121
+ end
122
+
123
+ # Sets or returns the retry delay.
124
+ #
125
+ # Can accept either a fixed delay (Integer) or a proc for dynamic calculation.
126
+ # The proc receives the retry count and returns delay in seconds.
127
+ #
128
+ # @param delay [Integer, Proc, nil] Delay in seconds, proc, or nil to get current value
129
+ #
130
+ # @return [Integer, Proc, nil] Current delay when getting, nil when setting
131
+ #
132
+ # @example Set fixed retry delay
133
+ # queue_retry_delay 10
134
+ #
135
+ # @example Set exponential backoff with proc
136
+ # queue_retry_delay ->(retries) { 2 ** retries }
137
+ #
138
+ # @example Get retry delay
139
+ # ProcessPayment.queue_retry_delay # => 10 or #<Proc>
140
+ #
141
+ def queue_retry_delay(delay = nil)
142
+ if delay
143
+ self.postburner_retry_delay = delay
144
+ nil
145
+ else
146
+ postburner_retry_delay
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -66,19 +66,33 @@ module Postburner
66
66
  # @option options [Integer] :pri Priority (lower = higher priority)
67
67
  # @option options [Integer] :ttr Time-to-run before job times out
68
68
  #
69
- # @return [Hash] Backburner response with :id and :status
69
+ # @return [Hash] Response with :id (beanstalkd job id) and :status
70
70
  #
71
71
  # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
72
72
  #
73
73
  # @api private
74
74
  #
75
75
  def insert(job, options = {})
76
+ #debugger
76
77
  Postburner::Job.transaction do
77
- Backburner::Worker.enqueue(
78
- Postburner::Job,
79
- job.id,
80
- options
81
- )
78
+ Postburner.connected do |conn|
79
+ tube_name = job.tube_name
80
+ data = { class: job.class.name, args: [job.id] }
81
+
82
+ # Get priority, TTR from job instance (respects instance overrides) or options
83
+ pri = options[:pri] || job.queue_priority || Postburner.configuration.default_priority
84
+ delay = options[:delay] || 0
85
+ ttr = options[:ttr] || job.queue_ttr || Postburner.configuration.default_ttr
86
+
87
+ response = conn.tubes[tube_name].put(
88
+ JSON.generate(data),
89
+ pri: pri,
90
+ delay: delay,
91
+ ttr: ttr
92
+ )
93
+
94
+ response
95
+ end
82
96
  end
83
97
  end
84
98
 
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern for ActiveJob classes to opt-in to PostgreSQL tracking.
5
+ #
6
+ # Simply include this module in your ActiveJob class to enable full audit
7
+ # trail persistence in PostgreSQL. Without this, jobs execute as "default" jobs
8
+ # (Beanstalkd only, no PostgreSQL overhead).
9
+ #
10
+ # @example Opt-in to tracking
11
+ # class ProcessPayment < ApplicationJob
12
+ # include Postburner::Tracked # ← Enables PostgreSQL audit trail
13
+ #
14
+ # def perform(payment_id)
15
+ # log "Processing payment #{payment_id}"
16
+ # # ... work ...
17
+ # log! "Payment processed successfully"
18
+ # end
19
+ # end
20
+ #
21
+ # @example Without tracking (default mode)
22
+ # class SendEmail < ApplicationJob
23
+ # # No Postburner::Tracked - executes as default job
24
+ #
25
+ # def perform(email)
26
+ # # Fast execution, no PostgreSQL overhead
27
+ # end
28
+ # end
29
+ #
30
+ module Tracked
31
+ extend ActiveSupport::Concern
32
+
33
+ included do
34
+ # Include Beanstalkd configuration DSL automatically
35
+ include Postburner::Beanstalkd
36
+
37
+ # Reference to Postburner::Job during execution (set by worker)
38
+ attr_accessor :postburner_job
39
+ end
40
+
41
+ # Appends a log message to the job's audit trail.
42
+ #
43
+ # Only available for tracked jobs. Logs are stored in the
44
+ # Postburner::Job record's `logs` JSONB array.
45
+ #
46
+ # @param message [String] Log message
47
+ # @param level [Symbol] Log level (:debug, :info, :warning, :error)
48
+ #
49
+ # @return [void]
50
+ #
51
+ # @example
52
+ # def perform(user_id)
53
+ # log "Starting user processing"
54
+ # # ... work ...
55
+ # log "Completed successfully", level: :info
56
+ # end
57
+ #
58
+ def log(message, level: :info)
59
+ postburner_job&.log(message, level: level)
60
+ end
61
+
62
+ # Appends a log message and immediately persists to database.
63
+ #
64
+ # Use this for important log messages that should be saved immediately
65
+ # rather than batched with other updates.
66
+ #
67
+ # @param message [String] Log message
68
+ # @param level [Symbol] Log level (:debug, :info, :warning, :error)
69
+ #
70
+ # @return [void]
71
+ #
72
+ # @example
73
+ # def perform(payment_id)
74
+ # log! "Critical: Processing high-value payment #{payment_id}"
75
+ # end
76
+ #
77
+ def log!(message, level: :info)
78
+ postburner_job&.log!(message, level: level)
79
+ end
80
+
81
+ # Tracks an exception in the job's errata array.
82
+ #
83
+ # Appends exception details (class, message, backtrace) to the
84
+ # in-memory errata array. Does NOT persist immediately.
85
+ #
86
+ # @param exception [Exception] The exception to track
87
+ #
88
+ # @return [void]
89
+ #
90
+ # @example
91
+ # def perform(user_id)
92
+ # process_user(user_id)
93
+ # rescue => e
94
+ # log_exception(e)
95
+ # raise
96
+ # end
97
+ #
98
+ def log_exception(exception)
99
+ postburner_job&.log_exception(exception)
100
+ end
101
+
102
+ # Tracks an exception and immediately persists to database.
103
+ #
104
+ # @param exception [Exception] The exception to track
105
+ #
106
+ # @return [void]
107
+ #
108
+ # @example
109
+ # def perform(user_id)
110
+ # process_user(user_id)
111
+ # rescue CriticalError => e
112
+ # log_exception!(e)
113
+ # raise
114
+ # end
115
+ #
116
+ def log_exception!(exception)
117
+ postburner_job&.log_exception!(exception)
118
+ end
119
+
120
+ # Returns the Beanstalkd job object for direct queue operations.
121
+ #
122
+ # Provides access to the underlying Beaneater job object through the
123
+ # TrackedJob wrapper. Use this to perform Beanstalkd operations like
124
+ # touch, bury, release, etc.
125
+ #
126
+ # @return [Beaneater::Job, nil] Beanstalkd job object or nil if not available
127
+ #
128
+ # @example Extend TTR during long operation
129
+ # def perform(file_id)
130
+ # file = File.find(file_id)
131
+ # file.each_line do |line|
132
+ # # ... process line ...
133
+ # bk&.touch # Extend TTR
134
+ # end
135
+ # end
136
+ #
137
+ # @example Other Beanstalkd operations
138
+ # bk&.bury # Bury the job
139
+ # bk&.release(pri: 0) # Release with priority
140
+ # bk&.stats # Get job statistics
141
+ #
142
+ # @see #extend!
143
+ #
144
+ def bk
145
+ postburner_job&.bk
146
+ end
147
+
148
+ # Extends the job's time-to-run (TTR) in Beanstalkd.
149
+ #
150
+ # Convenience method that calls touch on the Beanstalkd job, extending
151
+ # the TTR by the original TTR value. Use this during long-running operations
152
+ # to prevent the job from timing out.
153
+ #
154
+ # @return [void]
155
+ #
156
+ # @example Process large file line by line
157
+ # def perform(file_id)
158
+ # file = File.find(file_id)
159
+ # file.each_line do |line|
160
+ # # ... process line ...
161
+ # extend! # Extend TTR to prevent timeout
162
+ # end
163
+ # end
164
+ #
165
+ # @see #bk
166
+ #
167
+ def extend!
168
+ postburner_job&.extend!
169
+ end
170
+ end
171
+ end
@@ -1,3 +1,3 @@
1
1
  module Postburner
2
- VERSION = '0.9.0.rc.1'
2
+ VERSION = '1.0.0.pre.1'
3
3
  end