Class: GitHub::KV
- Inherits:
-
Object
- Object
- GitHub::KV
- Defined in:
- lib/github/kv.rb,
lib/github/kv/config.rb
Defined Under Namespace
Classes: Config, MissingConnectionError
Constant Summary collapse
- MAX_KEY_LENGTH =
255- MAX_VALUE_LENGTH =
65535- KeyLengthError =
Class.new(StandardError)
- ValueLengthError =
Class.new(StandardError)
Class.new(StandardError)
- InvalidValueError =
Class.new(StandardError)
Instance Attribute Summary collapse
-
#config ⇒ Object
writeonly
Sets the attribute config.
-
#use_local_time ⇒ Object
Returns the value of attribute use_local_time.
Class Method Summary collapse
Instance Method Summary collapse
- #connection ⇒ Object
-
#del(key) ⇒ Object
- del
-
String -> nil.
-
#exists(key) ⇒ Object
- exists
-
String -> Result<Boolean>.
-
#get(key) ⇒ Object
- get
-
String -> Result<String | nil>.
-
#increment(key, amount: 1, expires: nil, touch_on_insert: false) ⇒ Object
- increment
-
String, Integer, expires: Time? -> Integer.
-
#initialize(config: GitHub::KV.config, &conn_block) ⇒ KV
constructor
- initialize
-
[Exception], Boolean, Proc -> nil.
-
#mdel(keys) ⇒ Object
- mdel
-
String -> nil.
-
#mexists(keys) ⇒ Object
- mexists
- String
-
-> Result<>.
-
#mget(keys) ⇒ Object
- mget
- String
-
-> Result<[String | nil]>.
-
#mset(kvs, expires: nil) ⇒ Object
- mset
-
{ String => String }, expires: Time? -> nil.
-
#mttl(keys) ⇒ Object
- mttl
- String
-
-> Result<[Time | nil]>.
-
#set(key, value, expires: nil) ⇒ Object
- set
-
String, String, expires: Time? -> nil.
-
#setnx(key, value, expires: nil) ⇒ Object
- setnx
-
String, String, expires: Time? -> Boolean.
-
#ttl(key) ⇒ Object
- ttl
-
String -> Result<[Time | nil]>.
Constructor Details
#initialize(config: GitHub::KV.config, &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.
84 85 86 87 88 89 |
# File 'lib/github/kv.rb', line 84 def initialize(config: GitHub::KV.config, &conn_block) @encapsulated_errors = config.encapsulated_errors @use_local_time = config.use_local_time @table_name = config.table_name @conn_block = conn_block end |
Instance Attribute Details
#config=(value) ⇒ Object (writeonly)
Sets the attribute config
57 58 59 |
# File 'lib/github/kv.rb', line 57 def config=(value) @config = value end |
#use_local_time ⇒ Object
Returns the value of attribute use_local_time.
56 57 58 |
# File 'lib/github/kv.rb', line 56 def use_local_time @use_local_time end |
Class Method Details
.config ⇒ Object
59 60 61 |
# File 'lib/github/kv.rb', line 59 def self.config @config ||= Config.new end |
.configure {|config| ... } ⇒ Object
67 68 69 |
# File 'lib/github/kv.rb', line 67 def self.configure yield(config) end |
.reset ⇒ Object
63 64 65 |
# File 'lib/github/kv.rb', line 63 def self.reset @config = Config.new end |
Instance Method Details
#connection ⇒ Object
91 92 93 |
# File 'lib/github/kv.rb', line 91 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
377 378 379 380 381 |
# File 'lib/github/kv.rb', line 377 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>
202 203 204 205 206 |
# File 'lib/github/kv.rb', line 202 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>
107 108 109 110 111 |
# File 'lib/github/kv.rb', line 107 def get(key) validate_key(key) mget([key]).map { |values| values[0] } end |
#increment(key, amount: 1, expires: nil, touch_on_insert: false) ⇒ Object
- increment
-
String, Integer, expires: Time? -> Integer
Increment the key’s value by an amount.
key - The key to increment. amount - The amount to increment the key’s value by.
The user can increment by both positive and
negative values
expires - When the key should expire. touch_on_insert - Only when expires is specified. When true
the expires value is only touched upon
inserts. Otherwise the record is always
touched.
Returns the key’s value after incrementing.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'lib/github/kv.rb', line 287 def increment(key, amount: 1, expires: nil, touch_on_insert: false) validate_key(key) validate_amount(amount) if amount validate_expires(expires) if expires validate_touch(touch_on_insert, expires) expires ||= GitHub::SQL::NULL # This query uses a few MySQL "hacks" to ensure that the incrementing # is done atomically and the value is returned. The first trick is done # using the `LAST_INSERT_ID` function. This allows us to manually set # the LAST_INSERT_ID returned by the query. Here we are able to set it # to the new value when an increment takes place, essentially allowing us # to do: `UPDATE...;SELECT value from key_value where key=:key` in a # single step. # # However the `LAST_INSERT_ID` trick is only used when the value is # updated. Upon a fresh insert we know the amount is going to be set # to the amount specified. # # Lastly we only do these tricks when the value at the key is an integer. # If the value is not an integer the update ensures the values remain the # same and we raise an error. encapsulate_error { sql = GitHub::SQL.run(" INSERT INTO \#{@table_name} (`key`, `value`, `created_at`, `updated_at`, `expires_at`)\n VALUES(:key, :amount, :now, :now, :expires)\n ON DUPLICATE KEY UPDATE\n `value`=IF(\n concat('',`value`*1) = `value`,\n LAST_INSERT_ID(IF(\n `expires_at` IS NULL OR `expires_at`>=:now,\n `value`+:amount,\n :amount\n )),\n `value`\n ),\n `updated_at`=IF(\n concat('',`value`*1) = `value`,\n :now,\n `updated_at`\n ),\n `expires_at`=IF(\n concat('',`value`*1) = `value`,\n IF(\n :touch OR (`expires_at` IS NULL OR `expires_at`<:now),\n :expires,\n `expires_at`\n ),\n `expires_at`\n )\n SQL\n\n # The ordering of these statements is extremely important if we are to\n # support incrementing a negative amount. The checks occur in this order:\n # 1. Check if an update with new values occurred? If so return the result\n # This could potentially result in `sql.last_insert_id` with a value\n # of 0, thus it must be before the second check.\n # 2. Check if an update took place but nothing changed (I.E. no new value\n # was set)\n # 3. Check if an insert took place.\n #\n # See https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html for\n # more information (NOTE: CLIENT_FOUND_ROWS is set)\n if sql.affected_rows == 2\n # An update took place in which data changed. We use a hack to set\n # the last insert ID to be the new value.\n sql.last_insert_id\n elsif sql.affected_rows == 0 || (sql.affected_rows == 1 && sql.last_insert_id == 0)\n # No insert took place nor did any update occur. This means that\n # the value was not an integer thus not incremented.\n raise InvalidValueError\n elsif sql.affected_rows == 1\n # If the number of affected_rows is 1 then a new value was inserted\n # thus we can just return the amount given to us since that is the\n # value at the key\n amount\n end\n }\nend\n", key: key, amount: amount, now: now, expires: expires, touch: !touch_on_insert, connection: connection) |
#mdel(keys) ⇒ Object
- mdel
-
String -> nil
Deletes the specified keys. Returns nil. Raises on error.
Example:
kv.mdel(["foo", "octocat"])
# => nil
392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/github/kv.rb', line 392 def mdel(keys) validate_key_array(keys) encapsulate_error do GitHub::SQL.run(" DELETE FROM \#{@table_name} 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]>
218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/github/kv.rb', line 218 def mexists(keys) validate_key_array(keys) Result.new { existing_keys = GitHub::SQL.values(" SELECT `key` FROM \#{@table_name} 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]
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/github/kv.rb', line 124 def mget(keys) validate_key_array(keys) Result.new { kvs = GitHub::SQL.results(" SELECT `key`, value FROM \#{@table_name} WHERE `key` IN :keys AND (`expires_at` IS NULL OR `expires_at` > :now)\n SQL\n\n kvs.keys.each { |key| kvs[key.downcase] = kvs[key] }\n keys.map { |key| kvs[key.downcase] }\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
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/github/kv.rb', line 167 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 \#{@table_name} (`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) |
#mttl(keys) ⇒ Object
- mttl
- String
-
-> Result<[Time | nil]>
Returns the expires_at time for the specified key or nil.
Example:
kv.mttl(["foo", "octocat"])
# => #<Result value: [2018-04-23 11:34:54 +0200, nil]>
436 437 438 439 440 441 442 443 444 445 446 447 |
# File 'lib/github/kv.rb', line 436 def mttl(keys) validate_key_array(keys) Result.new { kvs = GitHub::SQL.results(" SELECT `key`, expires_at FROM \#{@table_name}\n 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 |
#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
147 148 149 150 151 152 |
# File 'lib/github/kv.rb', line 147 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
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/github/kv.rb', line 248 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 \#{@table_name} 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 \#{@table_name} (`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>
416 417 418 419 420 421 422 423 424 425 |
# File 'lib/github/kv.rb', line 416 def ttl(key) validate_key(key) Result.new { GitHub::SQL.value(" SELECT expires_at FROM \#{@table_name}\n WHERE `key` = :key AND (expires_at IS NULL OR expires_at > :now)\n SQL\n }\nend\n", :key => key, :now => now, :connection => connection) |