litestack 0.4.2 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -1
  3. data/BENCHMARKS.md +3 -3
  4. data/CAVEATS.md +20 -0
  5. data/CHANGELOG.md +40 -1
  6. data/FILESYSTEMS.md +55 -0
  7. data/Gemfile +2 -0
  8. data/README.md +8 -4
  9. data/ROADMAP.md +6 -6
  10. data/assets/litestack_advantage.png +0 -0
  11. data/bench/bench.rb +2 -0
  12. data/bench/bench_cache_rails.rb +33 -2
  13. data/bench/bench_cache_raw.rb +36 -12
  14. data/bench/bench_jobs_rails.rb +3 -3
  15. data/bench/bench_jobs_raw.rb +3 -3
  16. data/bin/liteboard +16 -13
  17. data/gemfiles/rails70.gemfile +5 -0
  18. data/gemfiles/rails71.gemfile +5 -0
  19. data/gemfiles/rails71.gemfile.lock +264 -0
  20. data/lib/active_job/queue_adapters/litejob_adapter.rb +11 -3
  21. data/lib/active_record/connection_adapters/litedb_adapter.rb +8 -0
  22. data/lib/active_support/cache/litecache.rb +40 -7
  23. data/lib/generators/litestack/install/install_generator.rb +2 -2
  24. data/lib/generators/litestack/install/templates/cable.yml +0 -3
  25. data/lib/litestack/liteboard/liteboard.rb +15 -19
  26. data/lib/litestack/liteboard/views/litecable.erb +1 -1
  27. data/lib/litestack/litecable.rb +1 -1
  28. data/lib/litestack/litecache.rb +51 -19
  29. data/lib/litestack/litecache.sql.yml +7 -5
  30. data/lib/litestack/litedb.rb +5 -1
  31. data/lib/litestack/litejob.rb +1 -1
  32. data/lib/litestack/litejobqueue.rb +24 -14
  33. data/lib/litestack/litemetric.rb +7 -6
  34. data/lib/litestack/litemetric.sql.yml +1 -1
  35. data/lib/litestack/litemetric_collector.sql.yml +1 -1
  36. data/lib/litestack/litequeue.rb +17 -2
  37. data/lib/litestack/litequeue.sql.yml +38 -5
  38. data/lib/litestack/litescheduler.rb +9 -4
  39. data/lib/litestack/litesearch/index.rb +11 -10
  40. data/lib/litestack/litesearch/model.rb +61 -3
  41. data/lib/litestack/litesearch/schema.rb +7 -2
  42. data/lib/litestack/litesearch/schema_adapters/backed_adapter.rb +69 -25
  43. data/lib/litestack/litesearch/schema_adapters.rb +4 -4
  44. data/lib/litestack/litesearch.rb +2 -2
  45. data/lib/litestack/litesupport.rb +9 -7
  46. data/lib/litestack/railtie.rb +4 -2
  47. data/lib/litestack/version.rb +1 -1
  48. data/lib/litestack.rb +15 -15
  49. data/lib/railties/rails/commands/dbconsole.rb +5 -5
  50. data/lib/sequel/adapters/litedb.rb +9 -1
  51. data/lib/sequel/adapters/shared/litedb.rb +2 -2
  52. data/scripts/build_metrics.rb +2 -2
  53. data/scripts/test_cable.rb +1 -1
  54. metadata +105 -56
  55. data/Gemfile.lock +0 -92
@@ -25,20 +25,24 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
25
25
 
26
26
  def create_primary_triggers_sql(active = false)
27
27
  when_stmt = "TRUE"
28
- cols = active_cols_names
28
+ cols = active_cols_names.select{|n| !n.nil?}
29
29
  if (filter = @schema[:filter_column])
30
30
  when_stmt = "NEW.#{filter} = TRUE"
31
31
  cols << filter
32
32
  end
33
+ update_filter = ""
34
+ if cols.length > 0
35
+ " OF #{cols.join(', ')} "
36
+ end
33
37
 
34
38
  <<-SQL
35
39
  CREATE TRIGGER #{name}_insert AFTER INSERT ON #{table} WHEN #{when_stmt} BEGIN
36
40
  INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
37
41
  END;
38
- CREATE TRIGGER #{name}_update AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN #{when_stmt} BEGIN
42
+ CREATE TRIGGER #{name}_update AFTER UPDATE #{update_filter} ON #{table} WHEN #{when_stmt} BEGIN
39
43
  INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
40
44
  END;
41
- CREATE TRIGGER #{name}_update_not AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN NOT #{when_stmt} BEGIN
45
+ CREATE TRIGGER #{name}_update_not AFTER UPDATE #{update_filter} ON #{table} WHEN NOT #{when_stmt} BEGIN
42
46
  DELETE FROM #{name} WHERE rowid = NEW.rowid;
43
47
  END;
44
48
  CREATE TRIGGER #{name}_delete AFTER DELETE ON #{table} BEGIN
@@ -51,19 +55,41 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
51
55
  "DROP TRIGGER IF EXISTS #{target_table}_#{target_col}_#{col}_#{name}_update;"
52
56
  end
53
57
 
58
+ def drop_secondary_trigger_poly_sql(target_table, target_col, col)
59
+ "DROP TRIGGER IF EXISTS #{target_table}_#{target_col}_#{name}_update;"
60
+ end
61
+
54
62
  def create_secondary_trigger_sql(target_table, target_col, col)
55
63
  <<~SQL
56
- CREATE TRIGGER #{target_table}_#{target_col}_#{col}_#{name}_update AFTER UPDATE OF #{target_col} ON #{target_table} BEGIN
64
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{col}_#{name}_update AFTER UPDATE OF #{target_col} ON #{target_table} BEGIN
57
65
  #{rebuild_sql} AND #{table}.#{col} = NEW.id;
58
66
  END;
59
67
  SQL
60
68
  end
61
69
 
70
+ def create_secondary_trigger_poly_sql(target_table, target_col, col, conditions)
71
+ conditions_sql = conditions.collect{|k, v| "NEW.#{k} = '#{v}'"}.join(" AND ")
72
+ <<~SQL
73
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{name}_insert AFTER INSERT ON #{target_table} WHEN #{conditions_sql} BEGIN
74
+ #{rebuild_sql};
75
+ END;
76
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{name}_update AFTER UPDATE ON #{target_table} WHEN #{conditions_sql} BEGIN
77
+ #{rebuild_sql};
78
+ END;
79
+ SQL
80
+ end
81
+
82
+
83
+
62
84
  def drop_secondary_triggers_sql
63
85
  sql = ""
64
86
  @schema[:fields].each do |name, field|
65
87
  if field[:trigger_sql]
66
- sql << drop_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
88
+ if field[:col]
89
+ sql << drop_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
90
+ elsif field[:source]
91
+ sql << drop_secondary_trigger_poly_sql(field[:target_table], field[:target_col], name)
92
+ end
67
93
  end
68
94
  end
69
95
  sql.empty? ? nil : sql
@@ -73,18 +99,18 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
73
99
  sql = ""
74
100
  @schema[:fields].each do |name, field|
75
101
  if field[:trigger_sql]
76
- sql << create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
102
+ if field[:col]
103
+ sql << create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
104
+ elsif field[:source]
105
+ sql << create_secondary_trigger_poly_sql(field[:target_table], field[:target_col], name, field[:conditions])
106
+ end
77
107
  end
78
108
  end
79
109
  sql.empty? ? nil : sql
80
110
  end
81
111
 
82
112
  def rebuild_sql
83
- conditions = ""
84
- jcs = join_conditions_sql
85
- fs = filter_sql
86
- conditions = " ON #{jcs} #{fs}" unless jcs.empty? && fs.empty?
87
- "INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) SELECT #{table}.id, #{select_cols_sql} FROM #{join_tables_sql} #{conditions}"
113
+ "INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) SELECT #{table}.id, #{select_cols_sql} FROM #{joins_sql} #{filter_sql}"
88
114
  end
89
115
 
90
116
  def enrich_schema
@@ -92,12 +118,23 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
92
118
  if field[:target] && !field[:target].start_with?("#{table}.")
93
119
  field[:target] = field[:target].downcase
94
120
  target_table, target_col = field[:target].split(".")
95
- field[:col] = "#{name}_id".to_sym unless field[:col]
121
+ field[:col] = :"#{name}_id" unless field[:col]
96
122
  field[:target_table] = target_table.to_sym
97
123
  field[:target_col] = target_col.to_sym
98
124
  field[:sql] = "(SELECT #{field[:target_col]} FROM #{field[:target_table]} WHERE id = NEW.#{field[:col]})"
99
125
  field[:trigger_sql] = true # create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
100
126
  field[:target_table_alias] = "#{field[:target_table]}_#{name}"
127
+ elsif field[:source]
128
+ field[:source] = field[:source].downcase
129
+ target_table, target_col = field[:source].split(".")
130
+ field[:target_table] = target_table.to_sym
131
+ field[:target_col] = target_col.to_sym
132
+ field[:conditions_sql] = field[:conditions].collect{|k, v| "#{k} = '#{v}'"}.join(" AND ") if field[:conditions]
133
+ field[:sql] = "SELECT #{field[:target_col]} FROM #{field[:target_table]} WHERE #{field[:reference]} = NEW.id"
134
+ field[:sql] += " AND #{field[:conditions_sql]}" if field[:conditions_sql]
135
+ field[:sql] = "(#{field[:sql]})"
136
+ field[:trigger_sql] = true
137
+ field[:target_table_alias] = "#{field[:target_table]}_#{name}"
101
138
  else
102
139
  field[:col] = name unless field[:col]
103
140
  field[:sql] = field[:col]
@@ -109,7 +146,7 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
109
146
 
110
147
  def filter_sql
111
148
  sql = ""
112
- sql << " AND #{@schema[:filter_column]} = TRUE " if @schema[:filter_column]
149
+ sql << " WHERE #{@schema[:filter_column]} = TRUE " if @schema[:filter_column]
113
150
  sql
114
151
  end
115
152
 
@@ -124,20 +161,27 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
124
161
  (!field[:trigger_sql].nil?) ? "#{field[:target_table_alias]}.#{field[:target_col]}" : field[:target]
125
162
  end.join(", ")
126
163
  end
127
-
128
- def join_tables_sql
129
- tables = [@schema[:table]]
164
+
165
+ def joins_sql
166
+ joins = [@schema[:table]]
130
167
  active_fields.each do |name, field|
131
- tables << "#{field[:target_table]} AS #{field[:target_table_alias]}" if field[:trigger_sql]
168
+ if field[:trigger_sql]
169
+ join_table = ""
170
+ join_table << "#{field[:target_table]} AS #{field[:target_table_alias]} ON "
171
+ if field[:col]
172
+ join_table << "#{field[:target_table_alias]}.id = #{@schema[:table]}.#{field[:col]}" if field[:col]
173
+ elsif field[:source]
174
+ join_table << "#{field[:target_table_alias]}.#{field[:reference]} = #{@schema[:table]}.id"
175
+ if field[:conditions]
176
+ join_table << " AND "
177
+ join_table << field[:conditions].collect{|k, v| "#{field[:target_table_alias]}.#{k} = '#{v}'"}.join(" AND ")
178
+ end
179
+ end
180
+ joins << join_table
181
+ end
132
182
  end
133
- tables.uniq.join(", ")
183
+ joins.join(" LEFT JOIN ")
134
184
  end
135
185
 
136
- def join_conditions_sql
137
- conditions = []
138
- active_fields.each do |name, field|
139
- conditions << "#{field[:target_table_alias]}.id = #{@schema[:table]}.#{field[:col]}" if field[:trigger_sql]
140
- end
141
- conditions.join(" AND ")
142
- end
186
+
143
187
  end
@@ -1,4 +1,4 @@
1
- require_relative "./schema_adapters/basic_adapter"
2
- require_relative "./schema_adapters/standalone_adapter"
3
- require_relative "./schema_adapters/contentless_adapter"
4
- require_relative "./schema_adapters/backed_adapter"
1
+ require_relative "schema_adapters/basic_adapter"
2
+ require_relative "schema_adapters/standalone_adapter"
3
+ require_relative "schema_adapters/contentless_adapter"
4
+ require_relative "schema_adapters/backed_adapter"
@@ -4,8 +4,8 @@ module Litesearch
4
4
  class Schema; end
5
5
  end
6
6
 
7
- require_relative "./litesearch/index"
8
- require_relative "./litesearch/model"
7
+ require_relative "litesearch/index"
8
+ require_relative "litesearch/model"
9
9
 
10
10
  module Litesearch
11
11
  def litesearch_index_cache
@@ -6,15 +6,16 @@ require "oj"
6
6
  require "yaml"
7
7
  require "pathname"
8
8
  require "fileutils"
9
+ require "erb"
9
10
 
10
- require_relative "./litescheduler"
11
+ require_relative "litescheduler"
11
12
 
12
13
  module Litesupport
13
14
  class Error < StandardError; end
14
15
 
15
16
  # Detect the Rack or Rails environment.
16
17
  def self.detect_environment
17
- if defined? Rails
18
+ if defined?(Rails) && Rails.respond_to?(:env)
18
19
  Rails.env
19
20
  elsif ENV["RACK_ENV"]
20
21
  ENV["RACK_ENV"]
@@ -174,7 +175,7 @@ module Litesupport
174
175
  end
175
176
 
176
177
  def configure(options = {})
177
- # detect enviornment (production, development, etc.)
178
+ # detect environment (production, development, etc.)
178
179
  defaults = begin
179
180
  self.class::DEFAULT_OPTIONS
180
181
  rescue
@@ -182,11 +183,11 @@ module Litesupport
182
183
  end
183
184
  @options = defaults.merge(options)
184
185
  config = begin
185
- YAML.load_file(@options[:config_path])
186
+ YAML.load(ERB.new(File.read(@options[:config_path])).result)
186
187
  rescue
187
188
  {}
188
189
  end # an empty hash won't hurt
189
- config = config[Litesupport.environment] if config[Litesupport.environment] # if there is a config for the current enviornment defined then use it, otherwise use the top level declaration
190
+ config = config[Litesupport.environment] if config[Litesupport.environment] # if there is a config for the current environment defined then use it, otherwise use the top level declaration
190
191
  config.keys.each do |k| # symbolize keys
191
192
  config[k.to_sym] = config[k]
192
193
  config.delete k
@@ -196,7 +197,7 @@ module Litesupport
196
197
  end
197
198
 
198
199
  def setup
199
- @conn = create_pooled_connection
200
+ @conn = create_pooled_connection(@options[:connection_count])
200
201
  @logger = create_logger
201
202
  @running = true
202
203
  end
@@ -231,7 +232,8 @@ module Litesupport
231
232
  end
232
233
 
233
234
  def create_pooled_connection(count = 1)
234
- Litesupport::Pool.new(1) { create_connection }
235
+ count = 1 unless count&.is_a?(Integer)
236
+ Litesupport::Pool.new(count) { create_connection }
235
237
  end
236
238
 
237
239
  # common db object options
@@ -3,8 +3,10 @@ require "rails/railtie"
3
3
  module Litestack
4
4
  class Railtie < ::Rails::Railtie
5
5
  initializer :disable_production_sqlite_warning do |app|
6
- # The whole point of this gem is to use sqlite3 in production.
7
- app.config.active_record.sqlite3_production_warning = false
6
+ if config.active_record.key?(:sqlite3_production_warning)
7
+ # The whole point of this gem is to use sqlite3 in production.
8
+ app.config.active_record.sqlite3_production_warning = false
9
+ end
8
10
  end
9
11
  end
10
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Litestack
4
- VERSION = "0.4.2"
4
+ VERSION = "0.4.4"
5
5
  end
data/lib/litestack.rb CHANGED
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # load core classes
4
- require_relative "./litestack/version"
5
- require_relative "./litestack/litescheduler"
6
- require_relative "./litestack/litesupport"
7
- require_relative "./litestack/litemetric"
8
- require_relative "./litestack/litedb"
9
- require_relative "./litestack/litecache"
10
- require_relative "./litestack/litejob"
11
- require_relative "./litestack/litecable"
4
+ require_relative "litestack/version"
5
+ require_relative "litestack/litescheduler"
6
+ require_relative "litestack/litesupport"
7
+ require_relative "litestack/litemetric"
8
+ require_relative "litestack/litedb"
9
+ require_relative "litestack/litecache"
10
+ require_relative "litestack/litejob"
11
+ require_relative "litestack/litecable"
12
12
 
13
13
  # conditionally load integration with other libraries
14
- require_relative "./sequel/adapters/litedb" if defined? Sequel
15
- require_relative "./active_record/connection_adapters/litedb_adapter" if defined? ActiveRecord
16
- require_relative "./railties/rails/commands/dbconsole" if defined?(Rails) && defined?(ActiveRecord)
17
- require_relative "./active_support/cache/litecache" if defined? ActiveSupport
18
- require_relative "./active_job/queue_adapters/litejob_adapter" if defined? ActiveJob
19
- require_relative "./action_cable/subscription_adapter/litecable" if defined? ActionCable
20
- require_relative "./litestack/railtie" if defined? Rails::Railtie
14
+ require_relative "sequel/adapters/litedb" if defined? Sequel
15
+ require_relative "active_record/connection_adapters/litedb_adapter" if defined? ActiveRecord
16
+ require_relative "railties/rails/commands/dbconsole" if defined?(Rails) && defined?(ActiveRecord)
17
+ require_relative "active_support/cache/litecache" if defined? ActiveSupport
18
+ require_relative "active_job/queue_adapters/litejob_adapter" if defined? ActiveJob
19
+ require_relative "action_cable/subscription_adapter/litecable" if defined? ActionCable
20
+ require_relative "litestack/railtie" if defined? Rails::Railtie
21
21
 
22
22
  module Litestack
23
23
  class NotImplementedError < RuntimeError; end
@@ -27,7 +27,7 @@ module Rails
27
27
 
28
28
  args << db_config.database
29
29
 
30
- find_cmd_and_exec(["mysql", "mysql5"], *args)
30
+ ActiveRecord::Base.connection.class.find_cmd_and_exec(["mysql", "mysql5"], *args)
31
31
 
32
32
  when /^postgres|^postgis/
33
33
  ENV["PGUSER"] = config[:username] if config[:username]
@@ -38,7 +38,7 @@ module Rails
38
38
  ENV["PGSSLCERT"] = config[:sslcert].to_s if config[:sslcert]
39
39
  ENV["PGSSLKEY"] = config[:sslkey].to_s if config[:sslkey]
40
40
  ENV["PGSSLROOTCERT"] = config[:sslrootcert].to_s if config[:sslrootcert]
41
- find_cmd_and_exec("psql", db_config.database)
41
+ ActiveRecord::Base.connection.class.find_cmd_and_exec("psql", db_config.database)
42
42
 
43
43
  when "sqlite3", "litedb"
44
44
  args = []
@@ -47,7 +47,7 @@ module Rails
47
47
  args << "-header" if @options[:header]
48
48
  args << File.expand_path(db_config.database, Rails.respond_to?(:root) ? Rails.root : nil)
49
49
 
50
- find_cmd_and_exec("sqlite3", *args)
50
+ ActiveRecord::Base.connection.class.find_cmd_and_exec("sqlite3", *args)
51
51
 
52
52
  when "oracle", "oracle_enhanced"
53
53
  logon = ""
@@ -58,7 +58,7 @@ module Rails
58
58
  logon << "@#{db_config.database}" if db_config.database
59
59
  end
60
60
 
61
- find_cmd_and_exec("sqlplus", logon)
61
+ ActiveRecord::Base.connection.class.find_cmd_and_exec("sqlplus", logon)
62
62
 
63
63
  when "sqlserver"
64
64
  args = []
@@ -73,7 +73,7 @@ module Rails
73
73
  args += ["-S", host_arg]
74
74
  end
75
75
 
76
- find_cmd_and_exec("sqlcmd", *args)
76
+ ActiveRecord::Base.connection.class.find_cmd_and_exec("sqlcmd", *args)
77
77
 
78
78
  else
79
79
  abort "Unknown command-line client for #{db_config.database}."
@@ -19,6 +19,7 @@ module Sequel
19
19
  sqlite3_opts = {}
20
20
  sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
21
21
  db = ::Litedb.new(opts[:database].to_s, sqlite3_opts)
22
+ @raw_db = db
22
23
 
23
24
  self.transaction_mode = :immediate
24
25
 
@@ -33,12 +34,19 @@ module Sequel
33
34
  end
34
35
 
35
36
  db.instance_variable_set(:@prepared_statements, {})
36
- @raw_db = db
37
37
  db
38
38
  end
39
+
40
+ def sqlite_version
41
+ @raw_db.sqlite_version
42
+ end
43
+
39
44
  end
40
45
 
41
46
  class Dataset < Sequel::SQLite::Dataset
47
+ def supports_insert_select?
48
+ true
49
+ end
42
50
  end
43
51
  end
44
52
  end
@@ -715,7 +715,7 @@ module Sequel
715
715
 
716
716
  # Handle uniqueness violations when inserting, by using a specified
717
717
  # resolution algorithm. With no options, uses INSERT OR REPLACE. SQLite
718
- # supports the following conflict resolution algoriths: ROLLBACK, ABORT,
718
+ # supports the following conflict resolution algorithms: ROLLBACK, ABORT,
719
719
  # FAIL, IGNORE and REPLACE.
720
720
  #
721
721
  # On SQLite 3.24.0+, you can pass a hash to use an ON CONFLICT clause.
@@ -877,7 +877,7 @@ module Sequel
877
877
  end
878
878
 
879
879
  # Use from_self for aggregate dataset using VALUES.
880
- def aggreate_dataset_use_from_self?
880
+ def aggregate_dataset_use_from_self?
881
881
  super || @opts[:values]
882
882
  end
883
883
 
@@ -17,14 +17,14 @@ Litejobqueue.new({
17
17
  log: nil
18
18
  })
19
19
 
20
- $time = Time.now.to_i #- 10800
20
+ $time = Time.now.to_i # - 10800
21
21
  $start_time = Time.now.to_i
22
22
 
23
23
  class NormalJob
24
24
  include Litejob
25
25
  self.queue = "normal"
26
26
  def perform(time)
27
- $time = time #-= (rand * 100).to_i #10 seconds in the past
27
+ $time = time # -= (rand * 100).to_i #10 seconds in the past
28
28
  sleep 0.001
29
29
  warn "performing some normal action"
30
30
  end
@@ -11,7 +11,7 @@ class Client
11
11
  end
12
12
 
13
13
  def call(*args)
14
- warn "[#{Process.pid}]:#{object_id} recieved #{args} from #{@channel}"
14
+ warn "[#{Process.pid}]:#{object_id} received #{args} from #{@channel}"
15
15
  end
16
16
  end
17
17