Class: GitHub::KV

Inherits:
Object
  • Object
show all
Defined in:
lib/github/kv.rb

Defined Under Namespace

Classes: MissingConnectionError

Constant Summary collapse

MAX_KEY_LENGTH =
255
MAX_VALUE_LENGTH =
65535
KeyLengthError =
Class.new(StandardError)
ValueLengthError =
Class.new(StandardError)
UnavailableError =
Class.new(StandardError)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(encapsulated_errors = [SystemCallError], use_local_time: false, &conn_block) ⇒ KV

initialize

[Exception], Boolean, Proc -> nil

Initialize a new KV instance.

encapsulated_errors - An Array of Exception subclasses that, when raised,

will be replaced with UnavailableError.

use_local_time: - Whether to use Ruby’s Time.now instaed of MySQL’s

`NOW()` function. This is mostly useful in testing
where time needs to be modified (eg. Timecop).
Default false.

&conn_block - A block to call to open a new database connection.

Returns nothing.



69
70
71
72
73
# File 'lib/github/kv.rb', line 69

def initialize(encapsulated_errors = [SystemCallError], use_local_time: false, &conn_block)
  @encapsulated_errors = encapsulated_errors
  @use_local_time = use_local_time
  @conn_block = conn_block
end

Instance Attribute Details

#use_local_timeObject

Returns the value of attribute use_local_time.



54
55
56
# File 'lib/github/kv.rb', line 54

def use_local_time
  @use_local_time
end

Instance Method Details

#connectionObject



75
76
77
# File 'lib/github/kv.rb', line 75

def connection
  @conn_block.try(:call) || (raise MissingConnectionError, "KV must be initialized with a block that returns a connection")
end

#del(key) ⇒ Object

del

String -> nil

Deletes the specified key. Returns nil. Raises on error.

Example:

kv.del("foo")
  # => nil


264
265
266
267
268
# File 'lib/github/kv.rb', line 264

def del(key)
  validate_key(key)

  mdel([key])
end

#exists(key) ⇒ Object

exists

String -> Result<Boolean>

Checks for existence of the specified key.

Example:

kv.exists("foo")
  # => #<Result value: true>

kv.exists("octocat")
  # => #<Result value: false>


185
186
187
188
189
# File 'lib/github/kv.rb', line 185

def exists(key)
  validate_key(key)

  mexists([key]).map { |values| values[0] }
end

#get(key) ⇒ Object

get

String -> Result<String | nil>

Gets the value of the specified key.

Example:

kv.get("foo")
  # => #<Result value: "bar">

kv.get("octocat")
  # => #<Result value: nil>


91
92
93
94
95
# File 'lib/github/kv.rb', line 91

def get(key)
  validate_key(key)

  mget([key]).map { |values| values[0] }
end

#mdel(keys) ⇒ Object

mdel

String -> nil

Deletes the specified keys. Returns nil. Raises on error.

Example:

kv.mdel(["foo", "octocat"])
  # => nil


279
280
281
282
283
284
285
286
287
288
289
# File 'lib/github/kv.rb', line 279

def mdel(keys)
  validate_key_array(keys)

  encapsulate_error do
    GitHub::SQL.run("      DELETE FROM key_values WHERE `key` IN :keys\n    SQL\n  end\n\n  nil\nend\n", :keys => keys, :connection => connection)

#mexists(keys) ⇒ Object

mexists
String

-> Result<>

Checks for existence of all specified keys. Booleans will be returned in the same order as keys are specified.

Example:

kv.mexists(["foo", "octocat"])
  # => #<Result value: [true, false]>


201
202
203
204
205
206
207
208
209
210
211
# File 'lib/github/kv.rb', line 201

def mexists(keys)
  validate_key_array(keys)

  Result.new {
    existing_keys = GitHub::SQL.values("      SELECT `key` FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > :now)\n    SQL\n\n    keys.map { |key| existing_keys.include?(key) }\n  }\nend\n", :keys => keys, :now => now, :connection => connection).to_set

#mget(keys) ⇒ Object

mget
String

-> Result<[String | nil]>

Gets the values of all specified keys. Values will be returned in the same order as keys are specified. nil will be returned in place of a String for keys which do not exist.

Example:

kv.mget(["foo", "octocat"])
  # => #<Result value: ["bar", nil]


108
109
110
111
112
113
114
115
116
117
118
# File 'lib/github/kv.rb', line 108

def mget(keys)
  validate_key_array(keys)

  Result.new {
    kvs = GitHub::SQL.results("      SELECT `key`, value FROM key_values WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > :now)\n    SQL\n\n    keys.map { |key| kvs[key] }\n  }\nend\n", :keys => keys, :now => now, :connection => connection).to_h

#mset(kvs, expires: nil) ⇒ Object

mset

{ String => String }, expires: Time? -> nil

Sets the specified hash keys to their associated values, setting them to expire at the specified time. Returns nil. Raises on error.

Example:

kv.mset({ "foo" => "bar", "baz" => "quux" })
  # => nil

kv.mset({ "expires" => "soon" }, expires: 1.hour.from_now)
  # => nil


150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/github/kv.rb', line 150

def mset(kvs, expires: nil)
  validate_key_value_hash(kvs)
  validate_expires(expires) if expires

  rows = kvs.map { |key, value|
    value = value.is_a?(GitHub::SQL::Literal) ? value : GitHub::SQL::BINARY(value)
    [key, value, now, now, expires || GitHub::SQL::NULL]
  }

  encapsulate_error do
    GitHub::SQL.run("      INSERT INTO key_values (`key`, value, created_at, updated_at, expires_at)\n      VALUES :rows\n      ON DUPLICATE KEY UPDATE\n        value = VALUES(value),\n        updated_at = VALUES(updated_at),\n        expires_at = VALUES(expires_at)\n    SQL\n  end\n\n  nil\nend\n", :rows => GitHub::SQL::ROWS(rows), :connection => connection)

#set(key, value, expires: nil) ⇒ Object

set

String, String, expires: Time? -> nil

Sets the specified key to the specified value. Returns nil. Raises on error.

Example:

kv.set("foo", "bar")
  # => nil


130
131
132
133
134
135
# File 'lib/github/kv.rb', line 130

def set(key, value, expires: nil)
  validate_key(key)
  validate_value(value)

  mset({ key => value }, expires: expires)
end

#setnx(key, value, expires: nil) ⇒ Object

setnx

String, String, expires: Time? -> Boolean

Sets the specified key to the specified value only if it does not already exist.

Returns true if the key was set, false otherwise. Raises on error.

Example:

kv.setnx("foo", "bar")
  # => false

kv.setnx("octocat", "monalisa")
  # => true

kv.setnx("expires", "soon", expires: 1.hour.from_now)
  # => true


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/github/kv.rb', line 231

def setnx(key, value, expires: nil)
  validate_key(key)
  validate_value(value)
  validate_expires(expires) if expires

  encapsulate_error {
    # if the key already exists but has expired, prune it first. We could
    # achieve the same thing with the right INSERT ... ON DUPLICATE KEY UPDATE
    # query, but then we would not be able to rely on affected_rows

    GitHub::SQL.run("      DELETE FROM key_values WHERE `key` = :key AND expires_at <= :now\n    SQL\n\n    value = value.is_a?(GitHub::SQL::Literal) ? value : GitHub::SQL::BINARY(value)\n    sql = GitHub::SQL.run(<<-SQL, :key => key, :value => value, :now => now, :expires => expires || GitHub::SQL::NULL, :connection => connection)\n      INSERT IGNORE INTO key_values (`key`, value, created_at, updated_at, expires_at)\n      VALUES (:key, :value, :now, :now, :expires)\n    SQL\n\n    sql.affected_rows > 0\n  }\nend\n", :key => key, :now => now, :connection => connection)

#ttl(key) ⇒ Object

ttl

String -> Result<[Time | nil]>

Returns the expires_at time for the specified key or nil.

Example:

kv.ttl("foo")
  # => #<Result value: 2018-04-23 11:34:54 +0200>

kv.ttl("foo")
  # => #<Result value: nil>


303
304
305
306
307
308
309
310
311
312
# File 'lib/github/kv.rb', line 303

def ttl(key)
  validate_key(key)

  Result.new {
    GitHub::SQL.value("      SELECT expires_at FROM key_values\n      WHERE `key` = :key AND (expires_at IS NULL OR expires_at > :now)\n    SQL\n  }\nend\n", :key => key, :now => now, :connection => connection)