Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.bundle
vendor
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
12 changes: 12 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
source "https://rubygems.org"

gem "json"
gem "securerandom"
gem "sinatra"
gem "pqueue"
gem "puma"
gem "redis"
group :development, :test do
gem 'rspec'
gem 'rack-test'
end
60 changes: 60 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
GEM
remote: https://rubygems.org/
specs:
base64 (0.3.0)
connection_pool (2.5.4)
diff-lcs (1.6.2)
json (2.7.6)
mustermann (3.0.4)
ruby2_keywords (~> 0.0.1)
nio4r (2.7.4)
pqueue (2.1.0)
puma (7.1.0)
nio4r (~> 2.0)
rack (2.2.19)
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-test (2.2.0)
rack (>= 1.3)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.26.1)
connection_pool
rspec (3.13.1)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.5)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.6)
ruby2_keywords (0.0.5)
securerandom (0.3.2)
sinatra (3.2.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.2.0)
tilt (~> 2.0)
tilt (2.6.1)

PLATFORMS
ruby

DEPENDENCIES
json
pqueue
puma
rack-test
redis
rspec
securerandom
sinatra

BUNDLED WITH
2.7.2
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# keyserver-onboarding
Write a server which can generate random api keys, assign them for usage and release them after sometime. Following endpoints should be available on the server to interact with it.
1. There should be one endpoint to generate keys. Once a key is generated it has an expiry of 5 minutes, after which the key is no longer available.
2. There should be an endpoint to get an available key. On hitting this endpoint server should serve a random key which is not already being used. This key should be blocked and should not be served again by E2 while it is in this state. If no eligible key is available then it should serve 404.
3. There should be an endpoint to unblock a key. Unblocked keys can be served via E2 again. Once a key becomes unblocked we can take it as having a new expiry 5 mins from when it was unblocked.
4. There should be an endpoint to delete a key. Deleted keys should be purged.
5. All keys are to be kept alive by clients calling this endpoint every 5 minutes. If a particular key has not received a keep alive in last five minutes then it should be deleted and never used again.

Apart from these endpoints, following rules should be enforced:
1. All blocked keys should get released automatically within 60 secs if E3 is not called.
2. No endpoint call should result in an iteration of whole set of keys i.e. no endpoint request should be O(n). They should either be O(lg n) or O(1).

RSpecs (Unit tests) expected for the code.

You should build this without full fledged Rails framework.

#### Technical Requirements:
Ruby verison: 2.6.6
rspec version: 3.10.x

To run:
bundle install
bundle exec rackup

To test:
bundle exec rspec
54 changes: 54 additions & 0 deletions app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require './key_server.rb'
require 'sinatra'
require 'json'
require 'redis'

redis = Redis.new
redis.flushall
keyserver = KeyServer.new
# Thread.new do
# loop do
# sleep 1
# keyserver.free_up
# end
# end

get '/' do
'Hi, Welcome to my app.'
end
#E1
post '/generateKeys' do
content_type :json
keys=keyserver.generate(5)
{status: "success", keys_gen: keys}.to_json
end
#E2
get '/key' do
content_type :json
available_key = keyserver.get_key
if available_key == nil
halt 404, "No keys available."
end
{key: available_key}.to_json
end
#E3
put '/releasekey/:id' do
content_type :json
key = params["id"]
halt 400 if keyserver.unblock_key(key)==false
{status: "Successfully unblocked the key."}.to_json
end
#E4
delete '/deletekey/:id' do
content_type :json
key = params["id"]
halt 400 if keyserver.delete_key(key)==false
{status: "successfully deleted"}.to_json
end
#E5
put '/keepalive/:id' do
content_type :json
key = params["id"]
halt 400 if keyserver.keep_alive(key)==false
{status: "Key Refreshed."}.to_json
end
6 changes: 6 additions & 0 deletions config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'rubygems'
require 'bundler'
Bundler.require
require './app.rb'

run Sinatra::Application
7 changes: 7 additions & 0 deletions constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module KeyServerConstants
T_USED = 60
T_FREE = 300
UNBLOCKED_SET = 'UNBLOCKED'
BLOCKED_ZSET = 'BLOCKED' # score = expire_at (float)
FREE_ZSET = 'FREE' # score = expire_at (float)
end
141 changes: 141 additions & 0 deletions key_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
require 'redis'
require 'securerandom'
require 'json'

class KeyServer
AVAILABLE_SET = "available_keys"
KEEPALIVE_TTL = 300
BLOCK_TTL = 60

def initialize
@redis = Redis.new
end

def generate(n = 1)
created = []

n.times do
id = SecureRandom.hex
lua_script = <<-LUA
local key_id = KEYS[1]
local ttl = ARGV[1]
local available_set = ARGV[2]
local expire_at = tonumber(redis.call('TIME')[1]) + tonumber(ttl)

redis.call('SETEX', 'key:' .. key_id, ttl, key_id)
redis.call('ZADD', available_set, expire_at, key_id)

return 1
LUA

@redis.eval(lua_script, keys: [id], argv: [KEEPALIVE_TTL, AVAILABLE_SET])
created << id
end

{ created: created, count: created.size }
end

def get_key
lua_script = <<-LUA
local now = tonumber(ARGV[1])
local block_ttl = ARGV[2]
local available_set = ARGV[3]

redis.call('ZREMRANGEBYSCORE', available_set, 0, now)

local result = redis.call('ZPOPMIN', available_set, 1)

if result and result[1] then
local id = result[1]

redis.call('SETEX', 'blocked:' .. id, block_ttl, "1")

return {id, now}
else
return nil
end
LUA

result = @redis.eval(lua_script, keys: [], argv: [Time.now.to_i, BLOCK_TTL, AVAILABLE_SET])

return nil unless result && result[0]

{ id: result[0], assigned_at: result[1].to_i }
end

def unblock_key(id)
lua_script = <<-LUA
local id = KEYS[1]
local ttl = ARGV[1]
local available_set = ARGV[2]

if redis.call('EXISTS', 'blocked:' .. id) == 0 then
return 0
end

local expire_at = tonumber(redis.call('TIME')[1]) + tonumber(ttl)

redis.call('DEL', 'blocked:' .. id)
redis.call('ZADD', available_set, expire_at, id)
redis.call('EXPIRE', 'key:' .. id, ttl)

return 1
LUA

result = @redis.eval(lua_script, keys: [id], argv: [KEEPALIVE_TTL, AVAILABLE_SET])

return false if result == 0
{ message: "Key #{id} unblocked" }
end

def delete_key(id)
lua_script = <<-LUA
local id = KEYS[1]
local available_set = ARGV[1]

if redis.call('EXISTS', 'key:' .. id) == 0 then
return 0
end

redis.call('DEL', 'key:' .. id)
redis.call('DEL', 'blocked:' .. id)
redis.call('ZREM', available_set, id)

return 1
LUA

result = @redis.eval(lua_script, keys: [id], argv: [AVAILABLE_SET])

return false if result == 0
{ message: "Key #{id} deleted" }
end

def keep_alive(id)
lua_script = <<-LUA
local id = KEYS[1]
local ttl = ARGV[1]
local available_set = ARGV[2]

local current_ttl = redis.call('TTL', 'key:' .. id)
if current_ttl < 0 then
return 0
end

local expire_at = tonumber(redis.call('TIME')[1]) + tonumber(ttl)

redis.call('ZADD', available_set, expire_at, id)
redis.call('EXPIRE', 'key:' .. id, current_ttl + tonumber(ttl))

return 1
LUA

result = @redis.eval(lua_script, keys: [id], argv: [KEEPALIVE_TTL, AVAILABLE_SET])

return false if result == 0
{ message: "Keepalive refreshed for #{id}" }
end

def ttl(id)
@redis.ttl("key:#{id}")
end
end
Loading