Skip to content

Instantly share code, notes, and snippets.

@johnpaulashenfelter
Last active July 25, 2018 02:05
Show Gist options
  • Save johnpaulashenfelter/45b9b7eb3657413aa1ddaa7b171ca2ed to your computer and use it in GitHub Desktop.
Save johnpaulashenfelter/45b9b7eb3657413aa1ddaa7b171ca2ed to your computer and use it in GitHub Desktop.
Quick pairing exercise

Database Server Pairing Exercise

This pairing task was chosen from https://www.recurse.com/pairing-tasks

Initial task

Before your interview, write a program that runs a server that is accessible on http://localhost:4000/. When your server receives a request on http://localhost:4000/set?somekey=somevalue it should store the passed key and value in memory. When it receives a request on http://localhost:4000/get?key=somekey it should return the value stored at somekey.

During your interview, you will pair on saving the data to a file. You can start with simply appending each write to the file, and work on making it more efficient if you have time.

Setup

This assume Ruby is installed (tested on MRI 2.5.1, but it should run on most modern Rubies). The server and client only require standard libraries. The spec requires RSpec 3.x to be installed.

Instructions

The server can be started with ./db_runner.rb

The "feature" tests can be run with ./test_client.rb when the server is running

The specs can be run with rspec db_server_spec.rb if RSpec 3.x is installed.

#!/usr/bin/env ruby -w
require_relative 'db_server.rb'
DbServer.new.run
require 'socket'
require 'logger'
require 'pry'
class DbServer
attr_reader :host, :port, :db
STATUS_CODE_MESSAGES = {
'200' => 'OK',
'201' => 'Created',
'404' => 'Not found',
'405' => 'Method not allowed',
'422' => 'Unprocessable entity',
'500' => 'Internal server error',
'501' => 'Not implemented',
'505' => 'HTTP version not supported'
}
def initialize(host: 'localhost', port: 4000, logfile: STDERR)
@host = 'localhost'
@port = 4000
@db = {}
@logger = Logger.new(logfile)
end
def run
@logger.info "Starting up on #{@host}:#{@port}"
server = TCPServer.new(@host, @port)
loop do
socket = server.accept
request = socket.gets
response = ""
@logger.info request
method, uri, scheme = request.split
if scheme != "HTTP/1.1"
response = "Do not understand scheme: #{scheme}. Try HTTP/1.1"
send_response(socket: socket, status_code: '505', response: response)
@logger.warn response
socket.close
next
end
if method != "GET"
response = "Do not understand method: #{method}. Try GET"
send_response(socket: socket, status_code: '405', response: response)
@logger.warn response
socket.close
next
end
action = URI.parse(uri).path
query = URI.parse(uri).query
query_key, query_value = query.split("=")
case action
when "/get"
if query_key != "key"
code = "422"
response = "I don't understand #{query_key}. Try ?key=value"
else
value = get(key: query_value)
if value == ""
code = "404"
response = "I couldn't find a value for key #{query_value}"
else
code = "200"
response = value
end
end
when "/set"
code = "201"
set(key: query_key, value: query_value)
response = "Done"
else
code = "501"
response = "I don't understand action: #{action} for #{uri}"
end
send_response(socket: socket, status_code: code, response: response)
end
end
def get(key:)
value = @db[key]
value.nil? ? '' : value
end
def set(key:, value:)
@db[key] = value
end
private
def set_status_message(status_code:)
if STATUS_CODE_MESSAGES.keys.include?(status_code)
STATUS_CODE_MESSAGES[status_code]
else
STATUS_CODE_MESSAGES['500']
end
end
def send_response(socket:, status_code: '500', response: '')
socket.print "HTTP/1.1 #{status_code} #{set_status_message(status_code: status_code)}\r\n" +
"Content-Type: text/plain\r\n" +
"Content-Length: #{response.bytesize}\r\n" +
"Connection: close\r\n"
socket.print "\r\n"
socket.print response
socket.close
end
end
require 'rspec'
require 'net/http'
require 'pry'
require_relative 'db_server.rb'
RSpec.describe 'DbServer' do
describe 'default' do
it 'host is localhost' do
db = DbServer.new
expect(db.host).to eql('localhost')
end
it 'port is 4000' do
db = DbServer.new
expect(db.port).to eql(4000)
end
end
describe '#set' do
it "sets the value of the key" do
server = DbServer.new
server.set(key: 'foo', value: 'penguin')
expect(server.db['foo']).to eql("penguin")
end
end
describe '#get' do
it "returns the value for an existing key" do
server = DbServer.new
server.set(key: 'foo', value: 'penguin')
expect(server.get(key: 'foo')).to eql("penguin")
end
it "returns a blank string for a missing key" do
server = DbServer.new
expect(server.get(key: 'bar')).to eql('')
end
end
end
# https://www.tutorialspoint.com/ruby/ruby_socket_programming.htm
# https://stackoverflow.com/questions/27368345/how-to-test-a-multi-threaded-tcpserver-with-rspec
# Database server
# Before your interview, write a program that runs a server that is accessible on http://localhost:4000/. When your server receives a request on http://localhost:4000/set?somekey=somevalue it should store the passed key and value in memory. When it receives a request on http://localhost:4000/get?key=somekey it should return the value stored at somekey.
# During your interview, you will pair on saving the data to a file. You can start with simply appending each write to the file, and work on making it more efficient if you have time.
# https://practicingruby.com/articles/implementing-an-http-file-server
#!/usr/bin/env ruby -w
require "net/http"
BASE_URL = "http://localhost:4000"
puts "Expecting 404 for /get?key=missing"
uri = uri = URI("#{BASE_URL}/get?key=missing")
response = Net::HTTP.get_response(uri)
puts response.code
puts response.body
puts
puts "Expecting 201 for setting /set?foo=rhino"
uri = uri = URI("#{BASE_URL}/set?foo=rhino")
response = Net::HTTP.get_response(uri)
puts response.code
puts response.body
puts
puts "Expecting 200, rhino for /get?key=foo"
uri = uri = URI("#{BASE_URL}/get?key=foo")
response = Net::HTTP.get_response(uri)
puts response.code
puts response.body
puts
puts "Bad requests"
puts
puts
puts "Expecting 501 for /foo?bar=baz"
uri = uri = URI("#{BASE_URL}/foo?bar=baz")
response = Net::HTTP.get_response(uri)
puts response.code
puts response.body
puts
puts "Expecting 422 for /get?bar=baz"
uri = uri = URI("#{BASE_URL}/get?bar=baz")
response = Net::HTTP.get_response(uri)
puts response.code
puts response.body
puts
puts "Expecting 405 for POST /get?key=foo"
uri = URI("#{BASE_URL}/get")
response = Net::HTTP.post_form(uri, 'key' => 'foo')
puts response.code
puts response.body
puts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment