Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1d390db
commit 5dab3f0
Showing
8 changed files
with
1,478 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,68 +1,124 @@ | ||
= about ServerSide | ||
== about Sequel | ||
|
||
ServerSide is an HTTP server framework designed to be as fast as possible, and | ||
as easy as possible to use. ServerSide includes a full-featured HTTP server, a | ||
controller-view system and a bunch of other tools to easily create servers and | ||
clusters of servers. | ||
Sequel is an ORM framework for Ruby. Sequel provides thread safety, connection pooling, and a DSL for constructing queries and table schemas. | ||
|
||
== Sequel vs. ActiveRecord | ||
|
||
Sequel offers the following advantages over ActiveRecord: | ||
|
||
* Better performance with large tables: unlike ActiveRecord, Sequel does not load the entire resultset into memory, but fetches each record separately and implements an Enumerable interface. | ||
* Easy construction of queries using a DSL. | ||
* Using model classes is possible, but not mandatory. | ||
|
||
== Resources | ||
|
||
* {Project page}[http://code.google.com/p/serverside/] | ||
* {Source code}[http://serverside.googlecode.com/svn/] | ||
* {Bug tracking}[http://code.google.com/p/serverside/issues/list] | ||
* {RubyForge page}[http://rubyforge.org/projects/serverside/] | ||
* {Project page}[http://code.google.com/p/ruby-sequel/] | ||
* {Source code}[http://ruby-sequel.googlecode.com/svn/] | ||
* {Bug tracking}[http://code.google.com/p/ruby-sequel/issues/list] | ||
* {RubyForge page}[http://rubyforge.org/projects/sequel/] | ||
|
||
To check out the source code: | ||
|
||
svn co http://serverside.googlecode.com/svn/trunk | ||
svn co http://ruby-sequel.googlecode.com/svn/trunk | ||
|
||
== Installation | ||
|
||
sudo gem install serverside | ||
sudo gem install sequel | ||
|
||
== A Short Tutorial | ||
|
||
=== Connecting to a database | ||
|
||
There are two ways to create a connection to a database. The easier way is to provide a connection URL: | ||
|
||
DB = Sequel.connect("postgres://postgres:postgres@localhost:5432/my_db") | ||
|
||
You can also specify optional parameters, such as the connection pool size: | ||
|
||
DB = Sequel.connect("postgres://postgres:postgres@localhost:5432/my_db", | ||
:max_connections => 10) | ||
|
||
== Usage | ||
The second, more verbose, way is to create an instance of a database class: | ||
|
||
Once you have the ServerSide gem installed, you can use the <tt>serverside</tt> | ||
script to control servers. For example: | ||
DB = Sequel::Postgres::Database.new(:database => 'my_db', :host => 'localhost', | ||
:port => 5432) | ||
|
||
serverside start . | ||
=== Creating Datasets | ||
|
||
will start an HTTP server on port 8000, serving the content of the working | ||
directory. You can stop the server by running <tt>serverside stop .</tt> | ||
Dataset is the primary means through which records are retrieved and manipulated. You can create an blank dataset by using the query method: | ||
|
||
To run the server without forking, use the 'serve' command: | ||
dataset = DB.query | ||
|
||
serverside serve . | ||
Or by using the from methods: | ||
|
||
== Serving ERb Templates | ||
posts = DB.from(:posts) | ||
|
||
ServerSide can render ERb[http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/] | ||
templates in a fashion similar to PHP. You can store templates in .rhtml files, | ||
and ServerSide takes care of all the rest. ServerSide is also smart enough to | ||
allow you to use nice looking URL's with your templates, and automatically adds | ||
the .rhtml extension if the file is there. | ||
|
||
== Serving Dynamic Content | ||
You can also use the equivalent shorthand: | ||
|
||
By default ServerSide serves static files, but you can change the behavior by | ||
creating custom {routing rules}[classes/ServerSide/Connection/Router.html]. | ||
Here's a simple routing rule: | ||
posts = DB[:posts] | ||
|
||
ServerSide::Router.route(:path => '/hello/:name') { | ||
send_response(200, 'text', "Hello #{@parameters[:name]}!") | ||
} | ||
Note: the dataset will only fetch records when you explicitly ask for them, as will be shown below. Datasets can be manipulated to filter through records, change record order and even join tables, as will also be shown below. | ||
|
||
The ServerSide framework also lets you route requests based on any attribute of | ||
incoming requests, such as host name, path, URL parameters etc. | ||
=== Retrieving Records | ||
|
||
To run your custom rules, you can either put them in a file called serverside.rb, | ||
or tell serverside to explicitly load a specific file: | ||
You can retrieve records by using the all method: | ||
|
||
serverside start ~/myapp/myapp.rb | ||
posts.all | ||
|
||
== Running a Cluster of Servers | ||
The all method returns an array of hashes, where each hash corresponds to a record. | ||
|
||
You can also iterate through records one at a time: | ||
|
||
posts.each {|row| p row} | ||
|
||
Or perform more advanced stuff: | ||
|
||
posts.map(:id) | ||
posts.inject({}) {|h, r| h[r[:id]] = r[:name]} | ||
|
||
You can also retrieve the first record in a dataset: | ||
|
||
posts.first | ||
|
||
If the dataset is ordered, you can also ask for the last record: | ||
|
||
posts.order(:stamp).last | ||
|
||
=== Filtering Records | ||
|
||
The simplest way to filter records is to provide a hash of values to match: | ||
|
||
my_posts = posts.filter(:category => 'ruby', :author => 'david') | ||
|
||
You can also specify ranges: | ||
|
||
my_posts = posts.filter(:stamp => 2.weeks.ago..1.week.ago) | ||
|
||
Some adapters will also let you specify Regexps: | ||
|
||
my_posts = posts.filter(:category => /ruby/i) | ||
|
||
You can also use an inverse filter: | ||
|
||
my_posts = posts.exclude(:category => /ruby/i) | ||
|
||
You can then retrieve the records by using any of the retrieval methods: | ||
|
||
my_posts.each {|row| p row} | ||
|
||
You can also specify a custom WHERE clause: | ||
|
||
posts.filter('(stamp < ?) AND (author <> ?)', 3.days.ago, author_name) | ||
|
||
=== Counting Records | ||
|
||
posts.count | ||
|
||
=== Ordering Records | ||
|
||
posts.order(:stamp) | ||
|
||
You can also specify descending order | ||
|
||
ServerSide makes it easy to control a cluster of servers. Just supply a range of | ||
ports instead of a single port: | ||
posts.order(:stamp.DESC) | ||
|
||
serverside -p 8000..8009 start . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
require 'thread' | ||
|
||
module ServerSide | ||
class ConnectionPool | ||
attr_reader :max_size, :mutex, :conn_maker | ||
attr_reader :available_connections, :allocated, :created_count | ||
|
||
def initialize(max_size = 4, &block) | ||
@max_size = max_size | ||
@mutex = Mutex.new | ||
@conn_maker = block | ||
|
||
@available_connections = [] | ||
@allocated = {} | ||
@created_count = 0 | ||
end | ||
|
||
def size | ||
@created_count | ||
end | ||
|
||
def hold | ||
t = Thread.current | ||
if (conn = owned_connection(t)) | ||
return yield(conn) | ||
end | ||
while !(conn = acquire(t)) | ||
sleep 0.001 | ||
end | ||
begin | ||
yield conn | ||
ensure | ||
release(t) | ||
end | ||
end | ||
|
||
def owned_connection(thread) | ||
@mutex.synchronize {@allocated[thread]} | ||
end | ||
|
||
def acquire(thread) | ||
@mutex.synchronize do | ||
@allocated[thread] ||= available | ||
end | ||
end | ||
|
||
def available | ||
@available_connections.pop || make_new | ||
end | ||
|
||
def make_new | ||
if @created_count < @max_size | ||
@created_count += 1 | ||
@conn_maker.call | ||
end | ||
end | ||
|
||
def release(thread) | ||
@mutex.synchronize do | ||
@available_connections << @allocated[thread] | ||
@allocated.delete(thread) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
require 'uri' | ||
|
||
require File.join(File.dirname(__FILE__), 'schema') | ||
|
||
module ServerSide | ||
class Database | ||
def initialize(opts = {}) | ||
@opts = opts | ||
end | ||
|
||
# Some convenience methods | ||
|
||
# Returns a new dataset with the from method invoked. | ||
def from(*args); query.from(*args); end | ||
|
||
# Returns a new dataset with the select method invoked. | ||
def select(*args); query.select(*args); end | ||
|
||
# returns a new dataset with the from parameter set. For example, | ||
# | ||
# db[:posts].each {|p| puts p[:title]} | ||
def [](table) | ||
query.from(table) | ||
end | ||
|
||
# Returns a literal SQL representation of a value. This method is usually | ||
# overriden in descendants. | ||
def literal(v) | ||
case v | ||
when String: "'%s'" % v | ||
else v.to_s | ||
end | ||
end | ||
|
||
# Creates a table. | ||
def create_table(name, columns = nil, indexes = nil, &block) | ||
if block | ||
schema = Schema.new | ||
schema.create_table(name, &block) | ||
schema.create(self) | ||
else | ||
execute Schema.create_table_sql(name, columns, indexes) | ||
end | ||
end | ||
|
||
# Drops a table. | ||
def drop_table(name) | ||
execute Schema.drop_table_sql(name) | ||
end | ||
|
||
# Performs a brute-force check for the existance of a table. This method is | ||
# usually overriden in descendants. | ||
def table_exists?(name) | ||
from(name).count | ||
true | ||
rescue | ||
false | ||
end | ||
|
||
@@adapters = Hash.new | ||
|
||
# Sets the adapter scheme for the database class. | ||
def self.set_adapter_scheme(scheme) | ||
@@adapters[scheme.to_sym] = self | ||
end | ||
|
||
# Converts a uri to an options hash. | ||
def self.uri_to_options(uri) | ||
{ | ||
:user => uri.user, | ||
:password => uri.password, | ||
:host => uri.host, | ||
:port => uri.port, | ||
:database => (uri.path =~ /\/(.*)/) && ($1) | ||
} | ||
end | ||
|
||
def self.connect(conn_string) | ||
uri = URI.parse(conn_string) | ||
c = @@adapters[uri.scheme.to_sym] | ||
raise "Invalid database scheme" unless c | ||
c.new(c.uri_to_options(uri)) | ||
end | ||
end | ||
end | ||
|
||
class Time | ||
SQL_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze | ||
|
||
def to_sql_timestamp | ||
strftime(SQL_FORMAT) | ||
end | ||
end |
Oops, something went wrong.