postburner 1.0.0.pre.3 → 1.0.0.pre.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f7e4e766eb885d932fe5949dc935b1efcb927894117cc427b7582e6807942f6
4
- data.tar.gz: d9b2ed2f5fe0aa61cf0a28d1560f861619c3049aa0bb44fb43b6a6d662914ce8
3
+ metadata.gz: 38e2982c4478f99aa0d19c58ac7455cdf82ff1c3707a154d1dbd0c3a1546ceb8
4
+ data.tar.gz: 0d17bb9da882d8e1cf2cce7c1cddd508ddbc7531c037dd56b7e0c6d34bd40f5b
5
5
  SHA512:
6
- metadata.gz: 4cb2922ca8f02625680f73232e4cc15590deecd1e6e7f792fda4b547cbb3b300de44107a055247a72ab5b56d0706631323149151708780dd9d719140255973f4
7
- data.tar.gz: c749613be57e8ca5bf14671a2f8c4743e34e9839f4432305c12bfb35e34e4718b1319096ed7336fe5d083994b5da7047b706c55f3a1b735439be4bb6a28c4a55
6
+ metadata.gz: 28421eb19f5e288e43e9d455364906dabe0ee3ab024a230efb9aca19842ac3677c4b602788d8f0f7c67253f64e1b382d12a4f23756ff01ef2e43a5b51d478d55
7
+ data.tar.gz: f13d5452e5394c3e6d8b9468d8863344f35049972fbe5eac6c20cbede12ddc7e9eb2a17538bccb626a7a00a744c567d33e59558f5e7309ab3ecf775bea3db5b9
data/README.md CHANGED
@@ -3,13 +3,58 @@
3
3
  Fast Beanstalkd-backed job queue with **optional PostgreSQL records** for ActiveJob.
4
4
 
5
5
  Postburner provides dual-mode job execution:
6
- - **Default jobs**: Fast execution via Beanstalkd only (no PostgreSQL overhead)
7
- - **Tracked jobs**: Full audit trail with logs, timing, errors, and statistics
6
+ - **Fast jobs**: Fast execution via Beanstalkd
7
+ - **Tracked jobs**: Audited jobs logs, timing, errors, and statistics
8
8
 
9
9
  Built for production environments where you want fast background processing for most jobs, but comprehensive auditing for critical operations.
10
10
 
11
11
  Depends on Beanstalkd, ActiveRecord, ActiveJob, Postgres.
12
12
 
13
+ ```ruby
14
+ # Default job (fast, no PostgreSQL overhead)
15
+ class SendSms < ApplicationJob
16
+ def perform(user_id)
17
+ user = User.find(user_id)
18
+ TextMessage.welcome(
19
+ to: user.phone_number,
20
+ body: "Welcome to our app!"
21
+ ).deliver_now
22
+ end
23
+ end
24
+
25
+ # Default job with Beanstalkd configuration
26
+ class DoSomething < ApplicationJob
27
+ include Postburner::Beanstalkd # optional, allow access to beanstalkd
28
+
29
+ priority 5000 # 0=highest, 65536=default, can set per job
30
+ ttr 30 # 30 second timeout
31
+
32
+ def perform(user_id)
33
+ # Do something
34
+ end
35
+ end
36
+
37
+ # Tracked job (full audit trail, includes Beanstalkd automatically)
38
+ class ProcessPayment < ApplicationJob
39
+ include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
40
+
41
+ priority 0 # Highest priority
42
+ ttr 600 # 10 minute timeout
43
+
44
+ def perform(payment_id)
45
+ log "Processing payment #{payment_id}"
46
+ Payment.find(payment_id).process!
47
+ log! "Payment processed successfully"
48
+ end
49
+ end
50
+
51
+ # Run worker (bin/postburner)
52
+ bundle exec postburner --worker default
53
+
54
+ # Or with rake task
55
+ bundle exec rake postburner:work WORKER=default
56
+ ```
57
+
13
58
  ## Why
14
59
 
15
60
  Postburner supports Async, Queues, Delayed, Priorities, Timeouts, and Retries from the [Backend Matrix](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). But uniquely, priorities are per job, in addition to the class level. Timeouts are per job and class level as well, and can be extended dynamically.
@@ -23,6 +68,7 @@ Thus old-school [beanstalkd](https://beanstalkd.github.io/) is used with Postgre
23
68
  - Store the jobs outside of the database, but also persist them to disk for disaster recovery (beanstalkd binlogs)
24
69
  - Introspect the jobs either with ActiveRecord or Beanstalkd.
25
70
  - Only one worker type, that can be single/multi-process, with optional threading, and optional GC (Garbage Collection) limits (kill fork after processing N jobs).
71
+ - Easy testing.
26
72
 
27
73
  ## Features
28
74
 
@@ -36,35 +82,17 @@ Thus old-school [beanstalkd](https://beanstalkd.github.io/) is used with Postgre
36
82
 
37
83
  ## Quick Start
38
84
 
39
- ```bash
40
- sudo apt-get install beanstalkd # OR brew install beanstalkd
41
-
42
- # Start beanstalkd (in-memory only)
43
- beanstalkd -l 127.0.0.1 -p 11300
44
-
45
- # OR with persistence (recommended for production)
46
- mkdir -p /var/lib/beanstalkd
47
- beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
48
- ```
49
-
50
85
  ```ruby
51
86
  # Gemfile
52
- gem 'postburner', '~> 1.0.0.pre.2'
53
- ```
87
+ gem 'postburner', '~> 1.0.0.pre.3'
54
88
 
55
- ```bash
56
- rails generate postburner:install
57
- rails db:migrate
58
- ```
59
-
60
- ```ruby
61
89
  # config/application.rb
62
90
  config.active_job.queue_adapter = :postburner
63
91
  ```
64
92
 
65
93
  ```yaml
66
94
  # config/postburner.yml
67
- production: # <- environment config, i.e. defaults
95
+ development: # <- environment config, i.e. defaults
68
96
  beanstalk_url: <%= ENV['BEANSTALK_URL'] || 'beanstalk://localhost:11300' %>
69
97
  default_forks: 2
70
98
  default_threads: 10
@@ -91,48 +119,16 @@ production: # <- environment config, i.e. defaults
91
119
  - video
92
120
  ```
93
121
 
94
- **Usage:**
95
-
96
- ```ruby
97
- # Default job (fast, no PostgreSQL overhead)
98
- class SendSms < ApplicationJob
99
- def perform(user_id)
100
- user = User.find(user_id)
101
- TextMessage.welcome(
102
- to: user.phone_number,
103
- body: "Welcome to our app!"
104
- ).deliver_now
105
- end
106
- end
107
-
108
- # Default job with Beanstalkd configuration
109
- class DoSomething < ApplicationJob
110
- include Postburner::Beanstalkd # optional, allow access to beanstalkd
111
-
112
- priority 5000 # 0=highest, 65536=default, can set per job
113
- ttr 30 # 30 second timeout
114
-
115
- def perform(user_id)
116
- # Do something
117
- end
118
- end
122
+ ```bash
123
+ sudo apt-get install beanstalkd # OR brew install beanstalkd
119
124
 
120
- # Tracked job (full audit trail, includes Beanstalkd automatically)
121
- class ProcessPayment < ApplicationJob
122
- include Postburner::Tracked # ← Opt-in to tracking (includes Beanstalkd)
125
+ beanstalkd -l 127.0.0.1 -p 11300 # Start beanstalkd
123
126
 
124
- priority 0 # Highest priority
125
- ttr 600 # 10 minute timeout
127
+ bundle exec rails generate postburner:install
128
+ bundle exec rails db:migrate
126
129
 
127
- def perform(payment_id)
128
- log "Processing payment #{payment_id}"
129
- Payment.find(payment_id).process!
130
- log! "Payment processed successfully"
131
- end
132
- end
133
-
134
- # Run worker
135
- bundle exec postburner --worker default
130
+ bundle exec postburner # start with bin/postburner
131
+ bundle exec rake postburner:work # or with rake task
136
132
  ```
137
133
 
138
134
  ## Table of Contents
@@ -159,11 +155,14 @@ The [protocol](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol
159
155
 
160
156
  Here is a picture of the typical job lifecycle:
161
157
 
158
+ ```
162
159
  put reserve delete
163
- -----> [READY] ---------> [RESERVED] --------> *poof*
160
+ -----> [READY] ---------> [RESERVED] --------> *poof*`
161
+ ```
164
162
 
165
163
  Here is a picture with more possibilities:
166
164
 
165
+ ```
167
166
  put with delay release with delay
168
167
  ----------------> [DELAYED] <------------.
169
168
  | |
@@ -182,13 +181,12 @@ Here is a picture with more possibilities:
182
181
  |
183
182
  | delete
184
183
  `--------> *poof*
184
+ ```
185
185
 
186
186
  ### Binlogs
187
187
 
188
188
  Beanstalkd lets you persist jobs to disk in case of a crash or restart. Just restart beanstalkd and the jobs will be back in the queue.
189
189
 
190
- **Setup:**
191
-
192
190
  ```bash
193
191
  # Create binlog directory
194
192
  sudo mkdir -p /var/lib/beanstalkd
@@ -198,7 +196,7 @@ sudo chown beanstalkd:beanstalkd /var/lib/beanstalkd # If running as service
198
196
  beanstalkd -l 127.0.0.1 -p 11300 -b /var/lib/beanstalkd
199
197
  ```
200
198
 
201
- **Configuration options:**
199
+ **Other options:**
202
200
 
203
201
  ```bash
204
202
  # Basic persistence
@@ -214,7 +212,7 @@ beanstalkd -b /var/lib/beanstalkd -f 200 # fsync at most every 200ms (default:
214
212
  beanstalkd -b /var/lib/beanstalkd -F
215
213
  ```
216
214
 
217
- **Options:**
215
+ **Beanstalkd switches:**
218
216
  - `-b <dir>` - Enable binlog persistence in specified directory
219
217
  - `-s <bytes>` - Maximum size of each binlog file (requires `-b`)
220
218
  - `-f <ms>` - Call fsync at most once every N milliseconds (requires `-b`, default: 50ms)
@@ -855,6 +853,52 @@ bin/postburner --worker general # Run the 'general' worker
855
853
  bin/postburner --worker general --queues default,mailers # Only process specific queues
856
854
  ```
857
855
 
856
+ **Rake task:**
857
+ ```bash
858
+ bundle exec rake postburner:work # Auto-select worker
859
+ bundle exec rake postburner:work WORKER=general # Specific worker
860
+ bundle exec rake postburner:work WORKER=general QUEUES=default,mailers # Filter queues
861
+ bundle exec rake postburner:work CONFIG=config/custom.yml # Custom config
862
+ ```
863
+
864
+ ### Programmatic Worker Control
865
+
866
+ Use `Postburner::Runner` to start workers programmatically from Ruby code:
867
+
868
+ ```ruby
869
+ # Basic usage
870
+ runner = Postburner::Runner.new(
871
+ config: 'config/postburner.yml',
872
+ env: 'production',
873
+ worker: 'default'
874
+ )
875
+ runner.run
876
+
877
+ # With queue filtering
878
+ runner = Postburner::Runner.new(
879
+ worker: 'general',
880
+ queues: ['default', 'mailers']
881
+ )
882
+ runner.run
883
+
884
+ # From environment variables (Rake task pattern)
885
+ runner = Postburner::Runner.new(
886
+ config: ENV['CONFIG'] || 'config/postburner.yml',
887
+ env: Rails.env,
888
+ worker: ENV['WORKER'],
889
+ queues: ENV['QUEUES']&.split(',')
890
+ )
891
+ runner.run
892
+ ```
893
+
894
+ **Options:**
895
+ - `:config` - Path to YAML configuration file (default: `'config/postburner.yml'`)
896
+ - `:env` - Environment name (default: `RAILS_ENV` or `'development'`)
897
+ - `:worker` - Worker name from config (required if multiple workers defined)
898
+ - `:queues` - Array of queue names to filter (overrides config queues)
899
+
900
+ This provides a unified interface used by both `bin/postburner` and `rake postburner:work`, making it easy to integrate Postburner workers into custom deployment scripts or process managers.
901
+
858
902
  ### Running Workers in Separate Processes
859
903
 
860
904
  For production deployments, run different workers in separate OS processes for isolation and resource allocation:
@@ -1296,7 +1340,7 @@ Direct access to Beanstalkd for advanced operations:
1296
1340
  job.bkid # => 12345
1297
1341
 
1298
1342
  # Access Beaneater job object
1299
- job.beanstalk_job.stats
1343
+ job.bk.stats
1300
1344
  # => {"id"=>12345, "tube"=>"critical", "state"=>"ready", ...}
1301
1345
 
1302
1346
  # Connection management
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing Beanstalkd command methods for Postburner jobs.
5
+ #
6
+ # Provides methods for interacting with Beanstalkd queue operations such as
7
+ # deleting, kicking, and extending TTR. All methods handle connection retries
8
+ # and gracefully handle missing bkid (e.g., in test mode).
9
+ #
10
+ # @example Basic Beanstalkd operations
11
+ # class MyJob < Postburner::Job
12
+ # def perform(args)
13
+ # # ... work ...
14
+ # extend! # Extend TTR during long operation
15
+ # end
16
+ # end
17
+ #
18
+ # job.delete! # Remove from Beanstalkd
19
+ # job.kick! # Retry buried job
20
+ #
21
+ module Commands
22
+ extend ActiveSupport::Concern
23
+
24
+ # Kicks a buried job back into the ready queue in Beanstalkd.
25
+ #
26
+ # This is a Beanstalkd operation used to retry jobs that were buried due to
27
+ # repeated failures or explicit burial. Does not modify the ActiveRecord model.
28
+ #
29
+ # Automatically retries with fresh connection if Beanstalkd connection is stale.
30
+ #
31
+ # @return [void]
32
+ #
33
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
34
+ #
35
+ # @note Does nothing if job has no bkid (e.g., in test mode)
36
+ # @note Only works on buried jobs - see Beanstalkd documentation
37
+ #
38
+ # @example
39
+ # job.kick! # Moves buried job back to ready queue
40
+ #
41
+ # @see #delete!
42
+ # @see #bk
43
+ #
44
+ def kick!
45
+ return unless self.bk
46
+ begin
47
+ self.bk.kick
48
+ rescue Beaneater::NotConnected => e
49
+ self.bk!.kick
50
+ end
51
+ end
52
+
53
+ # Extends the job's time-to-run (TTR) in Beanstalkd.
54
+ #
55
+ # Calls touch on the Beanstalkd job, extending the TTR by the original
56
+ # TTR value. Use this during long-running operations to prevent the job
57
+ # from timing out.
58
+ #
59
+ # @return [void]
60
+ #
61
+ # @note Does nothing if job has no bkid (e.g., in test mode)
62
+ #
63
+ # @example Process large file line by line
64
+ # def perform(args)
65
+ # file = File.find(args['file_id'])
66
+ # file.each_line do |line|
67
+ # # ... process line ...
68
+ # extend! # Extend TTR to prevent timeout
69
+ # end
70
+ # end
71
+ #
72
+ # @see #bk
73
+ #
74
+ def extend!
75
+ return unless self.bk
76
+ begin
77
+ self.bk.touch
78
+ rescue Beaneater::NotConnected => e
79
+ self.bk!.touch
80
+ end
81
+ end
82
+
83
+ # Deletes the job from the Beanstalkd queue.
84
+ #
85
+ # This is a Beanstalkd operation that removes the job from the queue but
86
+ # does NOT destroy the ActiveRecord model. Use {#destroy} or {#remove!} to
87
+ # also update the database record.
88
+ #
89
+ # Automatically retries with fresh connection if Beanstalkd connection is stale.
90
+ # Called automatically by before_destroy callback when using {#destroy}.
91
+ #
92
+ # @return [void]
93
+ #
94
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails after retry
95
+ #
96
+ # @note Does nothing if job has no bkid (e.g., in test mode)
97
+ # @note Does not modify ActiveRecord model - only affects Beanstalkd
98
+ #
99
+ # @example
100
+ # job.delete! # Removes from Beanstalkd queue only
101
+ # job.reload # Job still exists in database
102
+ #
103
+ # @see #remove!
104
+ # @see #destroy
105
+ # @see #bk
106
+ #
107
+ def delete!
108
+ return unless self.bk
109
+ begin
110
+ self.bk.delete
111
+ rescue Beaneater::NotConnected => e
112
+ self.bk!.delete
113
+ end
114
+ end
115
+
116
+ # Soft-deletes the job by removing from Beanstalkd and setting removed_at timestamp.
117
+ #
118
+ # Unlike {#destroy}, this preserves the job record in the database for audit trails
119
+ # while removing it from the Beanstalkd queue and marking it as removed.
120
+ #
121
+ # @return [void]
122
+ #
123
+ # @raise [Beaneater::NotConnected] if Beanstalkd connection fails
124
+ #
125
+ # @note Idempotent - does nothing if already removed
126
+ # @note Does not destroy ActiveRecord model - only soft deletes
127
+ #
128
+ # @example
129
+ # job.remove!
130
+ # job.removed_at # => 2025-10-31 12:34:56 UTC
131
+ # job.persisted? # => true (still in database)
132
+ #
133
+ # @see #delete!
134
+ # @see #destroy
135
+ #
136
+ def remove!
137
+ return if self.removed_at
138
+ self.delete!
139
+ self.update_column(:removed_at, Time.zone.now)
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Postburner
4
+ # Concern providing job execution methods for Postburner jobs.
5
+ #
6
+ # Handles the full job execution lifecycle including validation, callbacks,
7
+ # timing tracking, error handling, and state management. Provides both the
8
+ # class-level entry point for workers and the instance-level execution logic.
9
+ #
10
+ # @example Direct execution
11
+ # Postburner::Job.perform(job.id)
12
+ #
13
+ # @example Instance execution
14
+ # job.perform!(job.args)
15
+ #
16
+ module Execution
17
+ extend ActiveSupport::Concern
18
+
19
+ class_methods do
20
+ # Executes a job by ID, delegating to the current queue strategy.
21
+ #
22
+ # Loads the job from database by ID and delegates execution to the current
23
+ # queue strategy's {handle_perform!} method. This provides a unified API
24
+ # for job execution regardless of strategy (async, test, or null).
25
+ #
26
+ # Called automatically by Backburner workers in production. Can also be
27
+ # called manually for test/null strategies to trigger execution.
28
+ #
29
+ # @param id [Integer] Job ID to execute
30
+ # @param _ [Hash] Unused Backburner metadata parameter
31
+ #
32
+ # @return [void]
33
+ #
34
+ # @example Backburner automatic execution (production)
35
+ # # Jobs execute in tube: backburner.worker.queue.backburner-jobs
36
+ # # Backburner calls: Postburner::Job.perform(job_id)
37
+ #
38
+ # @example Manual execution with NullQueue
39
+ # Postburner.null_strategy!
40
+ # job = MyJob.create!(args: {})
41
+ # job.queue!(delay: 1.hour)
42
+ # Postburner::Job.perform(job.id) # Time travels and executes
43
+ #
44
+ # @note Strategy-aware: delegates to Postburner.queue_strategy.handle_perform!
45
+ # @note For NullQueue, automatically handles time travel for scheduled jobs
46
+ #
47
+ # @see #perform!
48
+ # @see Queue.handle_perform!
49
+ # @see NullQueue.handle_perform!
50
+ #
51
+ def perform(id, _={})
52
+ job = nil
53
+ begin
54
+ job = self.find(id)
55
+ rescue ActiveRecord::RecordNotFound => e
56
+ Rails.logger.warn <<-MSG
57
+ [Postburner::Job] [#{id}] Not Found.
58
+ MSG
59
+ end
60
+ #job.perform!(job.args)
61
+ Postburner.queue_strategy.handle_perform!(job)
62
+ end
63
+ end
64
+
65
+ # Executes the job with full lifecycle management and error handling.
66
+ #
67
+ # This is the main execution method called by Backburner workers or test strategies.
68
+ # Performs validation checks, executes callbacks, calls the subclass {#perform} method,
69
+ # tracks timing and statistics, and handles errors.
70
+ #
71
+ # Execution flow:
72
+ # 1. Runs attempt callbacks (fires on every retry)
73
+ # 2. Updates attempting metadata (attempting_at, attempts, lag)
74
+ # 3. Validates job state (queued, not processed, not removed, not premature)
75
+ # 4. Runs processing callbacks
76
+ # 5. Calls subclass {#perform} method
77
+ # 6. Runs processed callbacks (only on success)
78
+ # 7. Updates completion metadata (processed_at, duration)
79
+ # 8. Logs and tracks any exceptions
80
+ #
81
+ # @param args [Hash] Arguments to pass to {#perform} method
82
+ #
83
+ # @return [void]
84
+ #
85
+ # @raise [Exception] Any exception raised by {#perform} is logged and re-raised
86
+ #
87
+ # @note Does not execute if queued_at is nil, in the future, already processed, or removed
88
+ # @note Premature execution (before run_at) is delegated to queue strategy
89
+ #
90
+ # @see #perform
91
+ # @see Postburner.queue_strategy
92
+ # @see Callbacks
93
+ #
94
+ def perform!(args={})
95
+ run_callbacks :attempt do
96
+ self.attempting
97
+
98
+ self.update_columns(
99
+ attempting_at: self.attempting_at,
100
+ attempts: self.attempts,
101
+ attempt_count: self.attempts.length,
102
+ lag: self.lag,
103
+ processing_at: Time.zone.now,
104
+ )
105
+
106
+ begin
107
+ if self.queued_at.nil?
108
+ self.log! "Not Queued", level: :error
109
+ return
110
+ end
111
+
112
+ if self.queued_at > Time.zone.now
113
+ self.log! "Future Queued", level: :error
114
+ return
115
+ end
116
+
117
+ if self.processed_at.present?
118
+ self.log! "Already Processed", level: :error
119
+ self.delete!
120
+ return
121
+ end
122
+
123
+ if self.removed_at.present?
124
+ self.log! "Removed", level: :error
125
+ return
126
+ end
127
+
128
+ if self.run_at && self.run_at.to_i > Time.zone.now.to_i
129
+ Postburner.queue_strategy.handle_premature_perform(self)
130
+ return
131
+ end
132
+
133
+ self.log!("START (bkid #{self.bkid})")
134
+
135
+ run_callbacks :processing do
136
+ begin
137
+ self.perform(args)
138
+ rescue Exception => exception
139
+ self.persist_metadata!
140
+ self.log! '[Postburner] Exception raised during perform prevented completion.'
141
+ raise exception
142
+ end
143
+ end
144
+
145
+ self.log!("DONE (bkid #{self.bkid})")
146
+
147
+ begin
148
+ now = Time.zone.now
149
+ _duration = (now - self.processing_at) * 1000 rescue nil
150
+
151
+ run_callbacks :processed do
152
+ persist_metadata!(
153
+ processed_at: now,
154
+ duration: _duration,
155
+ )
156
+ end
157
+ rescue Exception => e
158
+ self.log_exception!(e)
159
+ self.log! '[Postburner] Could not set data after processing.'
160
+ # TODO README doesn't retry if Postburner is to blame
161
+ end
162
+
163
+ rescue Exception => exception
164
+ self.log_exception!(exception)
165
+ raise exception
166
+ end
167
+ end # run_callbacks :attempt
168
+
169
+ end
170
+
171
+ private
172
+
173
+ # Records an attempt and calculates execution lag.
174
+ #
175
+ # Appends current time to attempts array, sets attempting_at on first attempt,
176
+ # and calculates lag (delay between intended and actual execution time).
177
+ #
178
+ # @return [Time] Current timestamp
179
+ #
180
+ # @api private
181
+ #
182
+ def attempting
183
+ now = Time.zone.now
184
+ self.attempts << now
185
+ self.attempting_at ||= now
186
+ self.lag ||= (self.attempting_at - self.intended_at) * 1000 rescue nil
187
+ now
188
+ end
189
+ end
190
+ end