rb_snowflake_client 0.1.2 → 0.2.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
  SHA256:
3
- metadata.gz: 4aa627645364f92fc0e0d9ef949519a7649088e43de1e9ca7480ba284a9efba5
4
- data.tar.gz: e18d9a6a1f297cbbb5a10aafcac9dce440c43e0ca430f0c6310c31cea28b4507
3
+ metadata.gz: 2a93f0b1e503e2d219db8b1d1133395ca484b45aaa1f91ceef013a50a116b973
4
+ data.tar.gz: c292c5b0d25a921f8deba6882fe288ed084aa07919fa0d056899d20b921612fc
5
5
  SHA512:
6
- metadata.gz: 80ac86515ff08d723c5eff134ac090b60c079695da9c68cf39b06f8db5d1661f700b78930f5da2c19b3f4686563d4dbcda47df70f9ad35200af9e7582b09ea8b
7
- data.tar.gz: d4c10ff8bba434ccd0ee380bbaaf9d14bfc7aed22543c34397fd6e1f83611daab65efeacf8caf241a98fd5e2a0f3591592b47f7c76de4997cfe2ea2fb951a5b8
6
+ metadata.gz: 28428d4cf4bd70a11fefad327a2ab14deff17e95bf28b999a6b84d67756b35d3207c8224d6a028b9998ca85b62a7226f4df2beddea50f30e9a91350d7df73b4b
7
+ data.tar.gz: ad55068885a29f1cae0181e52d66764651cb4f7d541e46680bd15644ed8c17f10dda8cb9356ae4948cdf9b249e0c726abc540e26b03e1b76f9f2c538a8251cce
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rb_snowflake_client (0.1.2)
4
+ rb_snowflake_client (0.2.0)
5
5
  concurrent-ruby (>= 1.2)
6
6
  connection_pool (>= 2.4)
7
7
  dotenv (>= 2.8)
data/README.md CHANGED
@@ -13,6 +13,8 @@ This library is implemented in ruby and while it leverages some libraries that h
13
13
 
14
14
  # Usage
15
15
 
16
+ ## Create a client
17
+
16
18
  Add to your Gemfile or use `gem install rb-snowflake-client`
17
19
  ```ruby
18
20
  gem "rb-snowflake-client"
@@ -27,7 +29,7 @@ require "rb_snowflake_client"
27
29
  # see: https://github.com/rinsed-org/pure-ruby-snowflake-client/blob/master/lib/ruby_snowflake/client.rb#L43
28
30
  client = RubySnowflake::Client.new(
29
31
  "https://yourinstance.region.snowflakecomputing.com", # insert your URL here
30
- File.read("secrets/my_key.pem"), # path to your private key
32
+ File.read("secrets/my_key.pem"), # your private key in PEM format (scroll down for instructions)
31
33
  "snowflake-organization", # your account name (doesn't match your URL)
32
34
  "snowflake-account", # typically your subdomain
33
35
  "snowflake-user", # Your snowflake user
@@ -41,21 +43,22 @@ client = RubySnowflake::Client.new(
41
43
  RubySnowflake::Client.from_env
42
44
  ```
43
45
  Available ENV variables (see below in the config section for details)
44
- `SNOWFLAKE_URI`
45
- `SNOWFLAKE_PRIVATE_KEY_PATH`
46
- or (use either the key, or the path, key takes precedence if both are provided)
47
- `SNOWFLAKE_PRIVATE_KEY`
48
- `SNOWFLAKE_ORGANIZATION`
49
- `SNOWFLAKE_ACCOUNT`
50
- `SNOWFLAKE_USER`
51
- `SNOWFLAKE_DEFAULT_WAREHOUSE`
52
- `SNOWFLAKE_DEFAULT_DATABASE`
53
- `SNOWFLAKE_JWT_TOKEN_TTL`
54
- `SNOWFLAKE_CONNECTION_TIMEOUT`
55
- `SNOWFLAKE_MAX_CONNECTIONS`
56
- `SNOWFLAKE_MAX_THREADS_PER_QUERY`
57
- `SNOWFLAKE_THREAD_SCALE_FACTOR`
58
- `SNOWFLAKE_HTTP_RETRIES`
46
+ - `SNOWFLAKE_URI`
47
+ - `SNOWFLAKE_PRIVATE_KEY_PATH` or `SNOWFLAKE_PRIVATE_KEY`
48
+ - Use either the key or the path. Key takes precedence if both are provided.
49
+ - `SNOWFLAKE_ORGANIZATION`
50
+ - `SNOWFLAKE_ACCOUNT`
51
+ - `SNOWFLAKE_USER`
52
+ - `SNOWFLAKE_DEFAULT_WAREHOUSE`
53
+ - `SNOWFLAKE_DEFAULT_DATABASE`
54
+ - `SNOWFLAKE_JWT_TOKEN_TTL`
55
+ - `SNOWFLAKE_CONNECTION_TIMEOUT`
56
+ - `SNOWFLAKE_MAX_CONNECTIONS`
57
+ - `SNOWFLAKE_MAX_THREADS_PER_QUERY`
58
+ - `SNOWFLAKE_THREAD_SCALE_FACTOR`
59
+ - `SNOWFLAKE_HTTP_RETRIES`
60
+
61
+ ## Make queries
59
62
 
60
63
  Once you have a client, make queries
61
64
  ```ruby
@@ -68,44 +71,58 @@ result.each do |row|
68
71
  puts row["name"] # or case insensitive strings
69
72
  puts row.to_h # and can produce a hash with keys/values
70
73
  end
74
+ ```
75
+
76
+ ## Stream results
71
77
 
72
- # You can also stream results and not hold them all in memory.
73
- # The client will prefetch the next data partition only. If you
74
- # have some IO in your processing there should usually be data
75
- # available for you.
78
+ You can also stream results and not hold them all in memory. The client will prefetch the next data partition only. If you have some IO in your processing there should usually be data available for you.
79
+
80
+ ```ruby
76
81
  result = client.query("SELECT * FROM HUGETABLE", streaming: true)
77
82
  result.each do |row|
78
83
  puts row
79
84
  end
85
+ ```
80
86
 
87
+ ## Switching databases
81
88
 
82
- # You can also overwrite the database specified in the initializer, and run
83
- # your query with a different context.
89
+ You can also overwrite the database specified in the initializer, and run your query with a different context.
90
+
91
+ ```ruby
84
92
  result = client.query("SELECT * FROM SECRET_TABLE", database: "OTHER_DB")
85
93
  result.each do |row|
86
94
  puts row
87
95
  end
88
96
  ```
89
97
 
98
+ ## Switching warehouses
99
+
100
+ Clients are not warehouse specific, you can override the default warehouse per query
101
+
102
+ ```ruby
103
+ client.query("SELECT * FROM BIGTABLE", warehouse: "FAST_WH")
104
+ ```
105
+
90
106
  # Configuration Options
91
107
 
92
- The client supports the following configuration options, each with their own getter/setter except connection pool options which must be set at construction. Additionally, all except logger can be configured with environment variables (see above, but the pattern is like: "SNOWFLAKE_HTTP_RETRIES".
108
+ The client supports the following configuration options, each with their own getter/setter except connection pool options which must be set at construction. Additionally, all except logger can be configured with environment variables (see above, but the pattern is like: "SNOWFLAKE_HTTP_RETRIES". Configuration options can only be set on initiialization through `new` or `from_env`.
93
109
 
94
- `logger` - takes any ruby logger (by default it's a std lib Logger.new(STDOUT), set at DEBUG level. Not available as an ENV variable config option
95
- `log_level` - takes a log level, type is dependent on logger, for the default ruby Logger, use a level like `Logger::WARN`. Not available as an ENV variable config option.
96
- `jwt_token_ttl` - The time to live set on JWT token in seconds, defaults to 3540 (59 minutes, the longest Snowflake supports is 60)
97
- `connection_timeout` - The amount of time in seconds that the client's connection pool will wait before erroring in handing out a valid connection, defaults to 60 seconds
98
- `max_connections` - The maximum number of http connections to hold open in the connection pool. If you use the client in a threaded context, you may need to increase this to be threads * client.max_threads_per_query, defaults to 16. Can only be set on initialization.
99
- `max_threads_per_query` - The maximum number of threads the client should use to retreive data, per query, defaults to 8. If you want the client to act in a single threaded way, set this to 1
100
- `thread_scale_factor` - When downloading a result set into memory, thread count is calculated by dividing a query's partition count by this number. For details on implementation see the code in `client.rb`.
101
- `http_retries` - By default the client will retry common typically transient errors (http responses) twice, you can change the number of retries with this.
110
+ - `logger` - takes any ruby logger (by default it's a std lib Logger.new(STDOUT), set at DEBUG level. Not available as an ENV variable config option
111
+ - `log_level` - takes a log level, type is dependent on logger, for the default ruby Logger, use a level like `Logger::WARN`. Not available as an ENV variable config option.
112
+ - `jwt_token_ttl` - The time to live set on JWT token in seconds, defaults to 3540 (59 minutes, the longest Snowflake supports is 60).
113
+ - `connection_timeout` - The amount of time in seconds that the client's connection pool will wait before erroring in handing out a valid connection, defaults to 60 seconds
114
+ - `max_connections` - The maximum number of http connections to hold open in the connection pool. If you use the client in a threaded context, you may need to increase this to be threads * client.max_threads_per_query, defaults to 16.
115
+ - `max_threads_per_query` - The maximum number of threads the client should use to retreive data, per query, defaults to 8. If you want the client to act in a single threaded way, set this to 1
116
+ - `thread_scale_factor` - When downloading a result set into memory, thread count is calculated by dividing a query's partition count by this number. For details on implementation see the code in `client.rb`.
117
+ - `http_retries` - By default the client will retry common typically transient errors (http responses) twice, you can change the number of retries with this.
102
118
 
103
119
  Example configuration:
104
120
  ```ruby
105
- client = RubySnowflake::Client.from_env
106
- client.logger = Rails.logger
107
- client.max_connections = 24
108
- client.http_retries = 1
121
+ client = RubySnowflake::Client.from_env(
122
+ logger: Rails.logger
123
+ max_connections: 24
124
+ http_retries 1
125
+ )
109
126
  end
110
127
  ```
111
128
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RubySnowflake
2
4
  class Client
3
5
  class HttpConnectionWrapper
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require "openssl"
5
+ require "concurrent"
6
+
7
+ module RubySnowflake
8
+ class Client
9
+ class KeyPairJwtAuthManager
10
+ # requires text of a PEM formatted RSA private key
11
+ def initialize(organization, account, user, private_key, jwt_token_ttl)
12
+ @organization = organization
13
+ @account = account
14
+ @user = user
15
+ @private_key_pem = private_key
16
+ @jwt_token_ttl = jwt_token_ttl
17
+
18
+ # start with an expired value to force creation
19
+ @token_expires_at = Time.now.to_i - 1
20
+ @token_semaphore = Concurrent::Semaphore.new(1)
21
+ end
22
+
23
+ def jwt_token
24
+ return @token unless jwt_token_expired?
25
+
26
+ @token_semaphore.acquire do
27
+ now = Time.now.to_i
28
+ @token_expires_at = now + @jwt_token_ttl
29
+
30
+ private_key = OpenSSL::PKey.read(@private_key_pem)
31
+
32
+ payload = {
33
+ :iss => "#{@organization.upcase}-#{@account.upcase}.#{@user}.#{public_key_fingerprint}",
34
+ :sub => "#{@organization.upcase}-#{@account.upcase}.#{@user}",
35
+ :iat => now,
36
+ :exp => @token_expires_at
37
+ }
38
+
39
+ @token = JWT.encode payload, private_key, "RS256"
40
+ end
41
+ end
42
+
43
+ private
44
+ def jwt_token_expired?
45
+ Time.now.to_i > @token_expires_at
46
+ end
47
+
48
+ def public_key_fingerprint
49
+ reutrn @public_key_fingerprint unless @public_key_fingerprint.nil?
50
+
51
+ public_key_der = OpenSSL::PKey::RSA.new(@private_key_pem).public_key.to_der
52
+ digest = OpenSSL::Digest::SHA256.new.digest(public_key_der)
53
+ fingerprint = Base64.strict_encode64(digest)
54
+
55
+ @public_key_fingerprint = "SHA256:#{fingerprint}"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,22 +5,21 @@ require "benchmark"
5
5
  require "concurrent"
6
6
  require "connection_pool"
7
7
  require "json"
8
- require "jwt"
9
8
  require "logger"
10
9
  require "net/http"
11
10
  require "oj"
12
- require "openssl"
13
11
  require "retryable"
14
12
  require "securerandom"
15
13
  require "uri"
16
14
 
17
15
 
18
- require_relative "result"
19
- require_relative "streaming_result"
20
16
  require_relative "client/http_connection_wrapper"
17
+ require_relative "client/key_pair_jwt_auth_manager"
21
18
  require_relative "client/single_thread_in_memory_strategy"
22
19
  require_relative "client/streaming_result_strategy"
23
20
  require_relative "client/threaded_in_memory_strategy"
21
+ require_relative "result"
22
+ require_relative "streaming_result"
24
23
 
25
24
  module RubySnowflake
26
25
  class Error < StandardError
@@ -55,11 +54,19 @@ module RubySnowflake
55
54
  # how many times to retry common retryable HTTP responses (i.e. 429, 504)
56
55
  DEFAULT_HTTP_RETRIES = 2
57
56
 
58
- # can't be set after initialization
59
- attr_reader :connection_timeout, :max_connections
60
- attr_accessor :logger, :jwt_token_ttl, :max_threads_per_query, :thread_scale_factor, :http_retries
57
+ OJ_OPTIONS = { :bigdecimal_load => :bigdecimal }.freeze
61
58
 
62
- def self.from_env
59
+ # can't be set after initialization
60
+ attr_reader :connection_timeout, :max_connections, :logger, :max_threads_per_query, :thread_scale_factor, :http_retries
61
+
62
+ def self.from_env(logger: DEFAULT_LOGGER,
63
+ log_level: DEFAULT_LOG_LEVEL,
64
+ jwt_token_ttl: env_option("SNOWFLAKE_JWT_TOKEN_TTL", DEFAULT_JWT_TOKEN_TTL),
65
+ connection_timeout: env_option("SNOWFLAKE_CONNECTION_TIMEOUT", DEFAULT_CONNECTION_TIMEOUT ),
66
+ max_connections: env_option("SNOWFLAKE_MAX_CONNECTIONS", DEFAULT_MAX_CONNECTIONS ),
67
+ max_threads_per_query: env_option("SNOWFLAKE_MAX_THREADS_PER_QUERY", DEFAULT_MAX_THREADS_PER_QUERY),
68
+ thread_scale_factor: env_option("SNOWFLAKE_THREAD_SCALE_FACTOR", DEFAULT_THREAD_SCALE_FACTOR),
69
+ http_retries: env_option("SNOWFLAKE_HTTP_RETRIES", DEFAULT_HTTP_RETRIES))
63
70
  private_key = ENV["SNOWFLAKE_PRIVATE_KEY"] || File.read(ENV["SNOWFLAKE_PRIVATE_KEY_PATH"])
64
71
 
65
72
  new(
@@ -70,12 +77,14 @@ module RubySnowflake
70
77
  ENV["SNOWFLAKE_USER"],
71
78
  ENV["SNOWFLAKE_DEFAULT_WAREHOUSE"],
72
79
  ENV["SNOWFLAKE_DEFAULT_DATABASE"],
73
- jwt_token_ttl: env_option("SNOWFLAKE_JWT_TOKEN_TTL", DEFAULT_JWT_TOKEN_TTL),
74
- connection_timeout: env_option("SNOWFLAKE_CONNECTION_TIMEOUT", DEFAULT_CONNECTION_TIMEOUT ),
75
- max_connections: env_option("SNOWFLAKE_MAX_CONNECTIONS", DEFAULT_MAX_CONNECTIONS ),
76
- max_threads_per_query: env_option("SNOWFLAKE_MAX_THREADS_PER_QUERY", DEFAULT_MAX_THREADS_PER_QUERY),
77
- thread_scale_factor: env_option("SNOWFLAKE_THREAD_SCALE_FACTOR", DEFAULT_THREAD_SCALE_FACTOR),
78
- http_retries: env_option("SNOWFLAKE_HTTP_RETRIES", DEFAULT_HTTP_RETRIES),
80
+ logger: logger,
81
+ log_level: log_level,
82
+ jwt_token_ttl: jwt_token_ttl,
83
+ connection_timeout: connection_timeout,
84
+ max_connections: max_connections,
85
+ max_threads_per_query: max_threads_per_query,
86
+ thread_scale_factor: thread_scale_factor,
87
+ http_retries: http_retries,
79
88
  )
80
89
  end
81
90
 
@@ -91,27 +100,19 @@ module RubySnowflake
91
100
  http_retries: DEFAULT_HTTP_RETRIES
92
101
  )
93
102
  @base_uri = uri
94
- @private_key_pem = private_key
95
- @organization = organization
96
- @account = account
97
- @user = user
103
+ @key_pair_jwt_auth_manager =
104
+ KeyPairJwtAuthManager.new(organization, account, user, private_key, jwt_token_ttl)
98
105
  @default_warehouse = default_warehouse
99
- @public_key_fingerprint = public_key_fingerprint(@private_key_pem)
100
106
  @default_database = default_database
101
107
 
102
108
  # set defaults for config settings
103
109
  @logger = logger
104
110
  @logger.level = log_level
105
- @jwt_token_ttl = jwt_token_ttl
106
111
  @connection_timeout = connection_timeout
107
112
  @max_connections = max_connections
108
113
  @max_threads_per_query = max_threads_per_query
109
114
  @thread_scale_factor = thread_scale_factor
110
115
  @http_retries = http_retries
111
-
112
- # start with an expired value to force creation
113
- @token_expires_at = Time.now.to_i - 1
114
- @token_semaphore = Concurrent::Semaphore.new(1)
115
116
  end
116
117
 
117
118
  def query(query, warehouse: nil, streaming: false, database: nil)
@@ -157,36 +158,19 @@ module RubySnowflake
157
158
  @port ||= URI.parse(@base_uri).port
158
159
  end
159
160
 
160
- def jwt_token
161
- return @token unless jwt_token_expired?
162
-
163
- @token_semaphore.acquire do
164
- now = Time.now.to_i
165
- @token_expires_at = now + @jwt_token_ttl
166
-
167
- private_key = OpenSSL::PKey.read(@private_key_pem)
168
-
169
- payload = {
170
- :iss => "#{@organization.upcase}-#{@account.upcase}.#{@user}.#{@public_key_fingerprint}",
171
- :sub => "#{@organization.upcase}-#{@account.upcase}.#{@user}",
172
- :iat => now,
173
- :exp => @token_expires_at
174
- }
175
-
176
- @token = JWT.encode payload, private_key, "RS256"
161
+ def handle_errors(response)
162
+ if response.code != "200"
163
+ raise BadResponseError.new({}),
164
+ "Bad response! Got code: #{response.code}, w/ message #{response.body}"
177
165
  end
178
166
  end
179
167
 
180
- def jwt_token_expired?
181
- Time.now.to_i > @token_expires_at
182
- end
183
-
184
168
  def request_with_auth_and_headers(connection, request_class, path, body=nil)
185
169
  uri = URI.parse("#{@base_uri}#{path}")
186
170
  request = request_class.new(uri)
187
171
  request["Content-Type"] = "application/json"
188
172
  request["Accept"] = "application/json"
189
- request["Authorization"] = "Bearer #{jwt_token}"
173
+ request["Authorization"] = "Bearer #{@key_pair_jwt_auth_manager.jwt_token}"
190
174
  request["X-Snowflake-Authorization-Token-Type"] = "KEYPAIR_JWT"
191
175
  request.body = body unless body.nil?
192
176
 
@@ -230,7 +214,7 @@ module RubySnowflake
230
214
  end
231
215
 
232
216
  def retreive_result_set(response, streaming)
233
- json_body = Oj.load(response.body, oj_options)
217
+ json_body = Oj.load(response.body, OJ_OPTIONS)
234
218
  statement_handle = json_body["statementHandle"]
235
219
  num_threads = number_of_threads_to_use(json_body["resultSetMetaData"]["partitionInfo"].size)
236
220
  retreive_proc = ->(index) { retreive_partition_data(statement_handle, index) }
@@ -254,8 +238,8 @@ module RubySnowflake
254
238
  )
255
239
  end
256
240
 
257
- partition_json = nil
258
- bm = Benchmark.measure { partition_json = Oj.load(partition_response.body, oj_options) }
241
+ partition_json = {}
242
+ bm = Benchmark.measure { partition_json = Oj.load(partition_response.body, OJ_OPTIONS) }
259
243
  logger.debug { "JSON parsing took: #{bm.real}" }
260
244
  partition_data = partition_json["data"]
261
245
 
@@ -265,17 +249,5 @@ module RubySnowflake
265
249
  def number_of_threads_to_use(partition_count)
266
250
  [[1, (partition_count / @thread_scale_factor.to_f).ceil].max, @max_threads_per_query].min
267
251
  end
268
-
269
- def oj_options
270
- { :bigdecimal_load => :bigdecimal }
271
- end
272
-
273
- def public_key_fingerprint(private_key_pem_string)
274
- public_key_der = OpenSSL::PKey::RSA.new(private_key_pem_string).public_key.to_der
275
- digest = OpenSSL::Digest::SHA256.new.digest(public_key_der)
276
- fingerprint = Base64.strict_encode64(digest)
277
-
278
- "SHA256:#{fingerprint}"
279
- end
280
252
  end
281
253
  end
@@ -31,14 +31,19 @@ module RubySnowflake
31
31
  else
32
32
  BigDecimal(@data[index]).round(@row_types[index][:scale])
33
33
  end
34
+
35
+ # snowflake treats these all as 64 bit IEEE 754 floating point numbers, and will we too
34
36
  when :float, :double, :"double precision", :real
35
- # snowflake treats these all as 64 bit IEEE 754 floating point numbers, and will we too
36
37
  Float(@data[index])
37
- when :time, :datetime, :timestamp, :timestamp_ltz, :timestamp_ntz
38
+
39
+ # Despite snowflake indicating that it sends the offset in minutes, the actual time in UTC
40
+ # is always sent in the first half of the data. If an offset is sent it looks like:
41
+ # "1641008096.123000000 1980"
42
+ # If there isn't one, it's just like this:
43
+ # "1641065696.123000000"
44
+ # in all cases, the actual time, in UTC is the float value, and the offset is ignorable
45
+ when :time, :datetime, :timestamp, :timestamp_ntz, :timestamp_ltz, :timestamp_tz
38
46
  Time.strptime(@data[index], TIME_FORMAT).utc
39
- when :timestamp_tz
40
- timestamp, offset_minutes = @data[index].split(" ")
41
- Time.strptime(@data[index], TIME_FORMAT).utc - (Integer(offset_minutes) * 60)
42
47
  else
43
48
  @data[index]
44
49
  end
@@ -1,3 +1,3 @@
1
1
  module RubySnowflake
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rb_snowflake_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rinsed
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-04 00:00:00.000000000 Z
11
+ date: 2023-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -117,6 +117,7 @@ files:
117
117
  - lib/rb_snowflake_client.rb
118
118
  - lib/ruby_snowflake/client.rb
119
119
  - lib/ruby_snowflake/client/http_connection_wrapper.rb
120
+ - lib/ruby_snowflake/client/key_pair_jwt_auth_manager.rb
120
121
  - lib/ruby_snowflake/client/single_thread_in_memory_strategy.rb
121
122
  - lib/ruby_snowflake/client/streaming_result_strategy.rb
122
123
  - lib/ruby_snowflake/client/threaded_in_memory_strategy.rb