Class: GitHub::KV
- Inherits:
-
Object
- Object
- GitHub::KV
- 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)
Class.new(StandardError)
- InvalidValueError =
Class.new(StandardError)
Instance Attribute Summary collapse
-
#use_local_time ⇒ Object
Returns the value of attribute use_local_time.
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(encapsulated_errors = [SystemCallError], use_local_time: false, &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(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.
70 71 72 73 74 |
# File 'lib/github/kv.rb', line 70 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_time ⇒ Object
Returns the value of attribute use_local_time.
55 56 57 |
# File 'lib/github/kv.rb', line 55 def use_local_time @use_local_time end |
Instance Method Details
#connection ⇒ Object
76 77 78 |
# File 'lib/github/kv.rb', line 76 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
361 362 363 364 365 |
# File 'lib/github/kv.rb', line 361 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>
186 187 188 189 190 |
# File 'lib/github/kv.rb', line 186 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>
92 93 94 95 96 |
# File 'lib/github/kv.rb', line 92 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.
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 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 |
# File 'lib/github/kv.rb', line 271 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 key_values (`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,\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
376 377 378 379 380 381 382 383 384 385 386 |
# File 'lib/github/kv.rb', line 376 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]>
202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/github/kv.rb', line 202 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]
109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/github/kv.rb', line 109 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
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/github/kv.rb', line 151 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) |
#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]>
420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/github/kv.rb', line 420 def mttl(keys) validate_key_array(keys) Result.new { kvs = GitHub::SQL.results(" SELECT `key`, expires_at FROM key_values\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
131 132 133 134 135 136 |
# File 'lib/github/kv.rb', line 131 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
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/github/kv.rb', line 232 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>
400 401 402 403 404 405 406 407 408 409 |
# File 'lib/github/kv.rb', line 400 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) |