Skip to content

Commit

Permalink
implement sticky sessions in dispatcher device
Browse files Browse the repository at this point in the history
  • Loading branch information
rfk committed Mar 4, 2011
1 parent 7d7f293 commit f11cda3
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 26 deletions.
2 changes: 2 additions & 0 deletions m2wsgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@
mongrel2 config database. Perhaps a Connection.from_config method
with keywords to select the connection by handler id, host, route etc.
* support for except-100-continue; this may have to live in mongrel2
"""
# Copyright (c) 2011, Ryan Kelly.
# All rights reserved; available under the terms of the MIT License.
Expand Down
103 changes: 78 additions & 25 deletions m2wsgi/device/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,32 @@
type to m2wsgi::
m2wsgi --conn-type=Dispatcher dotted.app.name tcp://127.0.0.1:8888
To make it worthwhile, you'll probably want to run several handler processes
connecting to the dispatcher.
One important use-case for the dispatcher is to implement "sticky sessions".
By passing the --sticky option, you ensure that all requests from a specific
connction will be routed to the same handler.
By default, handler stickiness is associated with mongrel2's internal client
id. You can associated it with e.g. a session cookie by providing a regex
to extract the necessary information from the request. The information from
any capturing groups in the regex forms the sticky session key.
Here's how you might implement stickiness based on a session cookie::
python -m m2wsgi.device.dispatcher \
--sticky-regex="SESSIONID=([A-Za-z0-9]+)"
tcp://127.0.0.1:9999
tcp://127.0.0.1:8888
Note that the current implementation of sticky sessions is based on consistent
hashing. This has the advantage that multiple dispatcher devices can keep
their handler selection consistent without any explicit coordination; the
downside is that adding handlers will cause some sessions to be moved to the
new handler.
OK, but why?
Expand All @@ -49,9 +74,9 @@
respond with an "X" request. At this point the handler knows that no more
requests will be sent its way, and it can safely terminate.
The basic version of this device just routes reqeusts round-robin, but you
can easily implement more complex logic e.g. consistent hashing based on
a session token.
The basic version of this device just routes reqeusts round-robin, and there's
a subclass that can implement basic sticky sessions. More complex logic can
easily be built in a custom subclass.
Any Downsides?
Expand All @@ -73,6 +98,7 @@
"""

import os
import re
import errno
import threading
from textwrap import dedent
Expand Down Expand Up @@ -114,6 +140,8 @@ def __init__(self,send_sock,recv_sock,disp_sock=None,ping_sock=None,
self.ping_interval = ping_interval
self.pending_requests = deque()
self.pending_responses = deque()
# The set of all active handlers is an opaque data type.
# Subclasses can use anything they want.
self.active_handlers = self.init_active_handlers()
# Handlers that have been sent a ping and haven't yet sent a
# reply are "dubious". Handlers that have sent a disconnect
Expand Down Expand Up @@ -190,12 +218,6 @@ def _trigger_ping_alarm(self):
except EnvironmentError:
pass

def _whatis(self,sock):
for (k,v) in self.__dict__.iteritems():
if sock == v:
return k
return "???"

def run(self):
"""Run the socket handling loop."""
self.running = True
Expand Down Expand Up @@ -257,7 +279,7 @@ def run(self):
self.read_handler_responses()
# If we have some active handlers, we can dispatch a request.
# Note that we only send a single request then re-enter
# this loop, to give handlers a chance to wake up.
# this loop, to give other handlers a chance to wake up.
if self.has_active_handlers():
req = self.get_pending_request()
if req is not None:
Expand Down Expand Up @@ -504,20 +526,35 @@ def send_request_to_handler(self,req,handler):
return False


class ConsistentHashDispatcher(Dispatcher):
"""Dispatcher routing requests via consistent hashing.
class StickyDispatcher(Dispatcher):
"""Dispatcher implementing sticky sessions using consistent hashing.
This is Dispatcher subclass tries to route the same connection to the
same handler across multiple requests, by selecting the handler based
on a consistent hashing algorithm.
This is an example Dispatcher subclass that routes requests to handlers
based on a consistent hash of the request path. Obviously in real
life you would hash based on e.g. a session cookie, but I don't know
what you session cookie is called.
By default the handler is selected based on the connection id. You
can override this by providing a regular expression which will be run
against each request; the contents of all capturing groups will become
the handler selection key.
"""

def __init__(self,send_sock,recv_sock,disp_sock=None,ping_sock=None,
ping_interval=1,sticky_regex=None):
if sticky_regex is None:
# Capture connid from "svrid conid path headers body"
sticky_regex = r"^[^\s]+ ([^\s]+ )"
if isinstance(sticky_regex,basestring):
sticky_regex = re.compile(sticky_regex)
self.sticky_regex = sticky_regex
super(StickyDispatcher,self).__init__(send_sock,recv_sock,disp_sock,
ping_sock,ping_interval)

def init_active_handlers(self):
return ConsistentHash()

def has_active_handlers(self):
return bool(self.active_handlers.target_ring)
return bool(self.active_handlers)

def add_active_handler(self,handler):
self.active_handlers.add_target(handler)
Expand All @@ -526,14 +563,19 @@ def rem_active_handler(self,handler):
self.active_handlers.rem_target(handler)

def is_active_handler(self,handler):
for (h,t) in self.active_handlers.target_ring:
if t == handler:
return True
return False
return self.active_handlers.has_target(handler)

def dispatch_request(self,req):
(sid,cid,path,rest) = req.split(' ',3)
handler = self.active_handlers[path]
# Extract sticky key using regex
m = self.sticky_regex.search(req)
if m is None:
key = req
elif m.groups():
key = "".join(m.groups())
else:
key = m.group(0)
# Select handler based on sticky key
handler = self.active_handlers[key]
return self.send_request_to_handler(req,handler)


Expand All @@ -544,10 +586,21 @@ def dispatch_request(self,req):
"""))
op.add_option("","--ping-interval",type="int",default=1,
help="interval between handler pings")
op.add_option("","--sticky",action="store_true",
help="use sticky client <-a >handler pairing")
op.add_option("","--sticky-regex",
help="regex for extracting sticky connection key")
(opts,args) = op.parse_args()
if opts.sticky_regex:
opts.sticky = True
if len(args) == 2:
args = [args[0],None,args[1]]
d = Dispatcher(*args)
kwds = opts.__dict__
if kwds.pop("sticky",False):
d = StickyDispatcher(*args,**kwds)
else:
kwds.pop("sticky_regex",None)
d = Dispatcher(*args,**kwds)
try:
d.run()
finally:
Expand Down
16 changes: 15 additions & 1 deletion m2wsgi/util/conhash.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import scipy.stats
import numpy
except ImportError:
raise
scipy = numpy = None


Expand Down Expand Up @@ -89,6 +88,12 @@ def add_target(self,target):
def rem_target(self,target):
self.target_ring = [(h,t) for (h,t) in self.target_ring if t != target]

def has_target(self,target):
for (h,t) in self.target_ring:
if t == target:
return True
return False

def __getitem__(self,key):
h = self.hash(key)
i = bisect.bisect_left(self.target_ring,(h,key))
Expand All @@ -100,6 +105,9 @@ def __getitem__(self,key):
else:
return self.target_ring[i][1]

def __len__(self):
return len(self.target_ring) / self.num_duplicates


class SConsistentHash(object):
"""A consistent hash object based on sorted hashes with each key.
Expand Down Expand Up @@ -128,6 +136,9 @@ def add_target(self,target):
def rem_target(self,target):
self.targets.remove(target)

def has_target(self,target):
return (target in self.targets)

def __getitem__(self,key):
targets = iter(self.targets)
try:
Expand All @@ -142,6 +153,9 @@ def __getitem__(self,key):
best_h = h
return best_t

def __len__(self):
return len(self.targets)


# For now, we use the SConsistentHash as standard.
# It's slower but not really *slow*, and it consistently produces
Expand Down

0 comments on commit f11cda3

Please sign in to comment.