Shahzad Bhatti Welcome to my ramblings and rants!

June 1, 2011

Deploying Rails 3.0 App on Amazon EC2

Filed under: EC2 — admin @ 12:56 pm

It’s been a few years since I wrote a short HOW-TO on working with EC2 , but recently I tried to migrate the backend of Trading Floor – Facebook and iOS game I have been developing to EC2. So I am documenting the steps for setting up the EC2 for Rails 3.0.

Pre-requisites

I assume you already signed up for EC2, otherwise go to http://aws.amazon.com/ec2/ to signup. Also, you will need Java 5.0 or above, which you can download it from Oracle.

Download EC2 API Tools

First, download EC2 from http://developer.amazonwebservices.com/connect/entry.jspa?externalID=351 and uncompress it in your root directory.

Create a X.509 Certificate

Next, create a X.509 certificate from the AWS Account section. You can then download your certificate and key safely, e.g. I saved them in .ec2 directory under my home directory. Note that you will not be able to download the key again, so don’t lose it.

Environment Variables

Next, I changed my shell as:

 export EC2_HOME=~/ec2-api-tools-1.4.2.4
 export PATH=$PATH:$EC2_HOME/bin
 export EC2_KEY_DIR=~/.ec2
 export EC2_PRIVATE_KEY=$EC2_KEY_DIR/pk-HFG55OCFPZARA6YHW5JGIE6JFD7EQE72.pem
 export EC2_CERT=$EC2_KEY_DIR/cert-HFG55OCFPZARA6YHW5JGIE6JFD7EQE72.pem
 

Where EC2_PRIVATE_KEY and EC2_CERT points to the X.509 key and certificate I downloaded from the Amazon.

Create a Key-Pair

Then I created a pair of keys as:

 ec2-add-keypair plexobject
 

Create a Security Group

I then created a security group for the server

 ec2-add-group web -d 'Web Server'
 ec2-authorize web -P tcp -p 22 -s 0.0.0.0/0
 ec2-authorize web -P tcp -p 80 -s 0.0.0.0/0
 ec2-authorize web -P tcp -p 443 -s 0.0.0.0/0
 

Finding a basic Ubuntu based AMI

Previously I used S3 based AMI, but Amazon now supports EBS based AMIs that has advantage that any changes to the root survive instances of EC2. I launched EC2 instance with basic Ubuntu 11.0 Natty from http://alestic.com/ as:

 ec2-run-instances ami-06ad526f --instance-count 1 --instance-type m1.small \
 --key plexobject --group web -z us-east-1d -m
 

Where -z describes the availability zone and -m turns on monitoring.

Installing Ubuntu Packages

I then proceeded to install basic packages such as Java, Curl, Git, Build, Ruby (1.8.7) and Rails (3.0.3) based on Rails/Ubuntu docs such as:

 sudo apt-get install openjdk-6-jdk
 sudo apt-get install mercurial
 sudo apt-get install curl git-core build-essential zlib1g-dev libssl-dev libreadline5-dev
 sudo  apt-get install libcurl4-openssl-dev 
 sudo apt-get install ruby
 sudo apt-get install rubygems1.8
 sudo gem install rubygems-update
 sudo update_rubygems
 sudo gem install rails
 

I then edited /etc/profile /etc/bash.bashrc and added environment variables

 export PATH=$PATH:/var/lib/gems/1.8/bin/
 export JAVA_HOME=/usr/lib/jvm/java-1.6.0-openjdk/
 

Next, I installed Sqlite and Mysql:

 sudo apt-get install sqlite3 libsqlite3-dev
 sudo gem install sqlite3-ruby
 sudo apt-get install mysql-server mysql-client
 sudo apt-get install libmysql-ruby libmysqlclient-dev
 

Next, I installed Apache and Passenger:

 sudo apt-get install apache2 apache2-mpm-prefork apache2-prefork-dev
 sudo apt-get install apache2-dev libapr1-dev libaprutil1-dev
 sudo gem install passenger
 sudo /var/lib/gems/1.8/gems/passenger-3.0.7/bin/passenger-install-apache2-module*
 

I then edited /etc/apache2/apache2.conf and added:

 LoadModule passenger_module /var/lib/gems/1.8/gems/passenger-3.0.7/ext/apache2/mod_passenger.so
 PassengerRoot /var/lib/gems/1.8/gems/passenger-3.0.7
 PassengerRuby /usr/bin/ruby1.8
 

and then restarted apache

 /etc/init.d/apache2 restart
 

Creating EBS Volume for Data

Next, I created an EBS volume to store all data such as database tables and Rails application from the AWS Console and then attached it to the instance as:

 ec2-stop-instances i-73ab181d 
 ec2-attach-volume  vol-612eaa0a -i i-73ab181d -d /dev/sdf
 ec2-start-instances i-73ab181d 
 

Note that you have to create the EBS volumen in the same availability zone as your instance. I then logged into my machine using

 ssh -i plexobject.pem ubuntu@ec2-50-19-134-251.compute-1.amazonaws.com
 

and then formatted the newly built EBS volume as:

 sudo fdisk -l
 sudo mkfs -t ext4 /dev/xvdf
 

I then edited /etc/fstab and added

 /dev/xvdf       /data   auto    defaults,nobootwait,noatime     0       0
 

and then rebooted machine

 sudo reboot
 

Moving Mysql Data Directory

Mysql installs data directory on the root volume in /var/lib/mysql directory, which I wanted to move to newly created volume. So I created a directory /data/mysql so I stopped mysql:

 sudo /etc/init.d/mysql stop
 

I then copied mysql data directory such as:

 sudo cp -R -p /var/lib/mysql/mysql /data/mysql
 sudo chown -R mysql:mysql /data/mysql/
 

I didn’t copy entire mysql directory, only mysql subdirectory. Next I edited /etc/mysql/my.cnf and changed datadir to /data/mysql directory and then edited /etc/apparmor.d/usr.sbin.mysqld and changed all /var/lib/mysql to /data/mysql. Finally I restarted AppArmor profiles as:

 sudo /etc/init.d/apparmor reload
 

Then restarted mysql:

 sudo /etc/init.d/mysql restart
 

I changed my root password and created a local mysql user as

 mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
 mysql> GRANT ALL PRIVILEGES ON *.* TO 'tfuser'@'localhost' IDENTIFIED BY 'mypass' WITH GRANT OPTION;
 mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON dbname.* TO 'tfuser'@'localhost' IDENTIFIED BY 'mypass';
 

I copied my app to /data/trading_floor and changed permissions of all files to www-data

 sudo chown -R www-data:www-data /data/trading_floor
 

Then created /etc/apache2/sites-available/tf with

 
         ServerAdmin shahbhat@gmail.com
         ServerName tf.plexobject.com
         DocumentRoot /data/trading_floor/public/
         
                  AllowOverride all
                 Options -MultiViews
                 RailsEnv production
          
 
 

Finally, I restarted apache

 /etc/init.d/apache2 restart
 

Creating Elastic IP address

I wanted a permanent IP address for the server so I created EIP using AWS Console. Then associated my instance with the new IP address:

 ec2-associate-address 50.19.248.7 -i i-73ab181d
 

It rebooted your machine with the new IP address. I then changed DNS zone and pointed tf.plexobject.com to 50.19.248.7 (this may take hours to propagate). Next, I changed my Facebook app’s configuration and iOS app’s configuration to point to tf.plexobject.com.

Creating my EBS Image

Once I was happy with the server configuration, I created EBS image for future use. First, I detached data volume and then created image as follows:

 ec2-stop-instances i-f97ca197
 ec2-detach-volume vol-3f65d954
 ec2-create-image i-f97ca197 -n tf-20110601 -d 'Trading Floor Application Server'
 

I terminated my previous instance as

 ec2-terminate-instances -i  i-f97ca197 
 

and created instance with the new image

 ec2-run-instances ami-80837ae9 --instance-count 1 --instance-type m1.small --key tf --group web -z us-east-1d -m
 

After the launch, you would have to reattach the data volume

 ec2-stop-instances i-73ab181d 
 ec2-attach-volume  vol-612eaa0a -i i-73ab181d -d /dev/sdf
 ec2-start-instances i-73ab181d 
 

Summary

Voila, I had my game application running on the cloud. In order to cut per/hour cost I reserved instances for entire year. I am not quite done with my server and am now working on application specific configuration and adding better monitoring/backup. As, we have learned from recent Amazon Cloud outage that deploying your app on the cloud is only half the work, making it performant, scalable and fault tolerant is other half which is still manual work. Finally, I plan to release the Facebook app for Trading Floor and submit iOS app in a couple of weeks, be sure to try it and send me your suggestions.

September 22, 2010

An implementation of Virtual Node Router with Consistent Hash algorithm

Filed under: Java — admin @ 1:02 pm

Since the Dynamo paper, published a few years ago, DHTs and consistent hash have become mainstream. Here is my implementation of a virtual node router that uses consistent hash algorithm for splitting requests to the virtual nodes:

 import java.nio.ByteBuffer;
 import java.security.MessageDigest;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 
 public class VirtualNodeRouter {
     interface HashCalculator {
         long calculateHash(String key);
     }
 
     private static class VirtualNode {
         final String nodeName;
         final int replicaNumber;
 
         VirtualNode(final String nodeName, final int replicaNumber) {
             this.nodeName = nodeName.toLowerCase();
             this.replicaNumber = replicaNumber;
         }
 
         boolean matches(String host) {
             return nodeName.equalsIgnoreCase(host);
         }
 
         @Override
         public String toString() {
             return nodeName + ":" + replicaNumber;
         }
     }
 
     private final HashCalculator hashFunction;
     private final SortedMap<Long, VirtualNode> virtualNodePoolByHash = new TreeMap<Long, VirtualNode>(
             new Comparator<Long>() {
                 public int compare(Long i, Long j) {
                     if (i > j) {
                         return 1;
                     } else if (i < j) {
                         return -1;
                     } else {
                         return 0;
                     }
                 }
             });
 
     public VirtualNodeRouter() {
         this(new HashCalculator() {
             public long calculateHash(String key) {
                 try {
                     MessageDigest sha1 = MessageDigest.getInstance("SHA1");
                     sha1.update(key.getBytes());
                     byte[] digest = sha1.digest();
                     return bytesToLong(digest);
                 } catch (Exception e) {
                     throw new RuntimeException(e);
                 }
             }
         });
     }
 
     public VirtualNodeRouter(final HashCalculator f) {
         this.hashFunction = f;
     }
 
     /**
      * Adds a node with one replica
      * 
      * @param node
      *            - node name
      */
     public void add(String node) {
         add(node, 1);
     }
 
     /**
      * Adds a node to the available pool
      * 
      * @param node
      *            - node name
      * @param replicas
      *            - # of replicas - increase # of replicas based on the
      *            computing power of the machine
      */
     public void add(String node, int replicas) {
         // Note: You can call this method incrementally by adding more replicas,
         // so that you don't cause DOS on
         // your own services
         int existingReplicas = getReplicas(node);
 
         for (int i = 0; i < replicas; i++) {
             VirtualNode virtualNode = new VirtualNode(node, i
                     + existingReplicas);
             virtualNodePoolByHash.put(hashFunction.calculateHash(virtualNode
                     .toString()), virtualNode);
         }
     }
 
     /**
      * remove the node from available pool
      * 
      * @param node
      */
     public void remove(String node) {
         Iterator<Long> it = virtualNodePoolByHash.keySet().iterator();
         while (it.hasNext()) {
             Long key = it.next();
             VirtualNode virtualNode = virtualNodePoolByHash.get(key);
             if (virtualNode.matches(node)) {
                 it.remove();
             }
         }
     }
 
     public String getNode(String key) {
         if (virtualNodePoolByHash.isEmpty()) {
             return null;
         }
         long hash = hashFunction.calculateHash(key);
         for (Map.Entry<Long, VirtualNode> e : virtualNodePoolByHash.entrySet()) {
             if (hash < e.getKey()) {
                 return e.getValue().nodeName;
             }
         }
         SortedMap<Long, VirtualNode> tailMap = virtualNodePoolByHash
                 .tailMap(hash);
         hash = tailMap.isEmpty() ? virtualNodePoolByHash.firstKey() : tailMap
                 .firstKey();
         return virtualNodePoolByHash.get(hash).nodeName;
     }
 
     public void dump() {
         for (Map.Entry<Long, VirtualNode> e : virtualNodePoolByHash.entrySet()) {
             System.out.println("  " + e.getKey() + " => " + e.getValue());
         }
     }
 
     public int getReplicas(String nodeName) {
         int replicas = 0;
         for (VirtualNode node : virtualNodePoolByHash.values()) {
             if (node.matches(nodeName)) {
                 replicas++;
             }
         }
         return replicas;
     }
 
     private static long bytesToLong(byte[] b) {
         ByteBuffer bb = ByteBuffer.wrap(b);
         return bb.getLong();
     }
 }
 

The virtual nodes are added by specifying the node name and the number of replicas. You can add more virtual nodes for the more powerful machines than the low-end machines. You can call remove method when the node goes down or is unavailable. The getNode is called when a request needs to be routed. For example, if you are caching results of a service, you can use the key of cache to find the virtual node and then store the cache value at that node. You can read following resources to learn more about the consistent hash algorithm:



September 16, 2010

My impression of Diaspora codebase

Filed under: Computing — admin @ 5:07 pm

I briefly reviewed the Diaspora source code, which is all written in Ruby on Rails 3.0. Here are my initial thoughts on the codebase:

What I liked:

  • It’s all open source — YES!
  • Diaspora uses latest Rails 3.0 APIs such as global respond_to in controllers, thin controllers, and fat models with new query APIs. The code uses RSpec for tests and Factory-Girl for creating test objects instead of fixtures, which are much easily managed. There are a few Selenium tests, but most of the tests are in RSpec.
  • Diaspora is built using latest technologies and standards such as PKI (OpenSSL), HTML5, VCard, Websockets, Microformat, XRDS, PubSubHubbub along with popular libraries such as EventMachine, HAML, jQuery, Fancybox, Sprinkle, Bundler, Blueprint, etc.
  • Deployment scripts and documentation – Though, there isn’t any documentation on overall architecture or comments in the code, but I found installation documentation very helpful. Also, the deployment rake tasks and configurations are all included in the source code.

What I disliked:

Though, I found the code to be fairly easy to read and consistent in style I found following problems related to the performance, scalability and modularity.

  • Service API – Diaspora uses Rails controller for serving HTML as well as JSON, XML and XRDS requests but I would have preferred separate services for the API with much more defined contract.
  • Pagination – I found sporadic use of pagination in the code but a number of classes use Rails’ builtin relationships without any pagination. It’s like when you ask for banana you get the gorilla holding the banana. In my experience, this has been problem with all O/R mapping tools, which give you nice syntax for fetching related objects until your server runs out of memory or your database dies on you.
  • Before/after filters – I found a number of such filters in the code, which I have found to be another common issue with the scalability when before/after filters require a lot of overhead.
  • Asynchronous Messaging – I didn’t see any use of asynchronous messaging as a lot of requests such as adding/removing a friend can be done asynchronous.
  • Modularity – Diaspora code uses modules and models for the business logic, but I found a couple of models such as user to be too big, which can be divided into suitable modules.
  • MongoDB – I have nothing against MongoDB but I found a lot of code depends on MongoDB. I would have preferred using data access service instead, which completely encapsulates underlying storage technology and allows you to replace it without modifying all the code.

Conclusion

Despite the Knuth’s advice: “Forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil,” you need the architecture for scalability and performance when you are building the platform to replace Facebook. As social networking tools depend much more on Network effects or Metcalfe’s law instead of best technology, I hope early release of the software allows it to capture more users who use it. I was somewhat disappointed that identi.ca has not caught much attraction as open source alternative to Twitter. And I hope that Diaspora succeeds in becoming a good alternative to Facebook.

July 25, 2010

Tutorial days from OSCON 2010

Filed under: Computing — admin @ 10:21 pm

I had fun at OSCON last year, so I decided to go back this year. It’s incredible experience being part of hundreds of developers (more than 2500), who are excited about open source and upcoming technogolies. Though, there were a number of tracks, but I was mainly interested in Mobile Computing, Cloud Computing, No-SQL, and Scala. There were also interesting sessions on Emerging Languages and Hardware Hacking with Arduino, but I didn’t get chance to attend them.

Tutorials Day One

I arrived in Portalnd on Sunday night and the first session I attended on Monday morning was on Android. I saw fair number of Android phones at the conference and people were excited about Android. There was a lot of bashing of Apple, but I won’t get into it. Here are some of the highlights from the Android session:

Android for Java Developers

Android is a specially designed O/S based on Linux that uses a number of open source libraries and custom framework for building mobile applications. It even comes with a command line tool adb that opens shell directly on the device or emulator. Though, Android applications are written in Java but the byte-codes are converted into Dalvik instruction sets (.dex files). Dalvik is a register based Java VM as opposed to Sun’s stack based and uses JavaSE minus Swing/AWT APIs. You use Dalvik generated executable and resources (images, audio, configurations, etc.) to build the application file (APK), which is then signed by self-generated certificate. The best part of Android development is their deployment, which is an order of magnitude easier compare to Apple iOS.

Android SDK

Before developing, you would need to download the Eclipse IDE, Android SDK and then install Eclipse plugin from https://dl-ssl.google.com/android/eclipse/.

Creating Android Device

One of the downside of Android is variations of the capabilities of all Android devices, however you don’t need to own all variations. All you need is to create Android device by giving name, target version, SD card, skin and other hardware limitations. Android comes with emulator as opposed to simulator with iPhone SDK and follows hardware much more closely. On the downside, it takes a long time to start the Android emulator, but on the upside you can emulate call, SMS, and launch multiple emulators, which can other emulators. Note, the android device file definition goes into ~/.android/avd directory.

Hello World

You can create a new project and specify the build target, application name, package name, and main activity class. An activity class represents a controller class, which you define for each screen. Android uses UI to generate layout file (R file). If your application need resources such as images and property files, you store them in res directory. You can also create resource files for storing strings to make your application easily localized for various languages.

Activities

Activities represent screens and are managed by the activity managers. The activity can be in one of five states: starting, running, paused, stopped, and destroyed. The Activity class defines callback methods such as onCreate, onStart, onRestoreInstanceState, and onResume when the state changes.

Intent

Intent represents events or actions, which can be explicit or implicit.

Services

One of the key advantage of Android has been its support of multi-tasking and you can have a background processing using services. Services also have lifecycle, but simpler than activities.

Content Providers

Content providers allow sharing data between applications such as contacts, mediastore, settings, etc.

Broadcast Receivers

These allow pub-sub based mechanism for system events such as SMS messages.

Architecture of Android applications

Here are some of the tips that Marko Gargenta gave when designing application:

  • Isolate I/O operations such as network or disk operations into separate tasks or background services, which either use notification or database to communicate with interactive application. For example, in his sample twitter application, he used background service to poll tweets and stored them to the database. The activities then polled tweets from the database and also subscribed to the notification when new tweet arrives.
  • Use layout for screen design as it is more declarative and separate text values from the layout and use string resources.
  • Due to the variations of the Android devices, use layout weight and density intensity pixel (dp or sp) instead of fixed pixels (px) for components.
  • Android SDK provides GUI tool for designing layout and you will need to bind the UI components back to the activity classes, so use consistent naming convention for both layout file and the activity file, e.g.
       public class Twitter extends Activity {
         EditText editTweet;
         Button buttonUpdate;
         public void onCreate(...) {
           editTweet = (EditText) findViewById(R.id.editTweet);
           editButton = (Button) findViewById(R.id.editButton);
           buttonUpdate.setOnClickListener(this)
             String tweet = editTweet.getText().toString();
           }
         }
       }
     

Adapters

Android allows easily access large datasets as arrays that can be displayed on the screen.

Logging

Android uses custom libc and Java for Logging and you can add logging as:

   Log.debug("ClassName", "button clicked");
 

You can use “adb logcat” command to view logs, e.g. “adb logcat Twitter:* *:S”

Security Permissions

Any permissions that user need must be defined in the manifest file.

Advanced components

Android comes with a number of advanced components such as Map, Menus, Graphics, Animations, Multimedia, Preferences, Sqlite databases, etc.

Cloud to Device Push

This is a new service similar to iPhone push notification.

Debugging

Marko suggested use of Eclipse debugger, logcat, hierarchy viewer and traceview for debugging.

For more information on Android session, Download the slides.

The Seductions of Scala

For the second half of the day, I attended Dean Wampler’s session on Scala. I have been long interested in Scala and have done a little development on my own. As OSCON offered a lot of sessions on Scala, I took the opportunity to learn more on Scala. Dean highlighted concurrency, concise code, and correctness, better object model as major benefits of Scala.

Introduction

Scala comes with a number of features for concise code such as implicit type conversion, properties with uniform access principle and optional semicolons and paranthesis when function arguments are one or empty. Scala allows objects to act as function using apply method, e.g.

   class Logger(val level: Level) {
         def apply(message: String) = {
                 log(level, message)
         }
   }
 
 val error = new Logger(ERROR) ...
 error("Network error.")
 

Also, Scala treats primitive types as objects, but are comiled down as primitivies. Scala also treats functions as objects, e.g. you can create list or map without new

 val list = List(1, 2, 3, 4, 5)
 val map = Map("name" -> "Dean", "age" -> 39)
 

Above list syntax is same as 1::2::3::4::5::Nil. Scala also allows any symbol for function name so you can define functions that look like operator overloading. Scala uses infix operator notation, for example, following two expressions are equivalent:

 "hello" + "world" 
 "hello".+("world")
 

Scala gives you higher level operations such as map, filter, fold/reduce, e.g.

 val list = "a" :: "b" :: Nil
 list map {
         s => s.toUpperCase
 }
 

Generics

Martin Ordesky added limited support of Generics in Java, but he added fully functional generics support in Scala, e.g.

 class List[A] { ...
 def map[B](f: A => B): List[B]
 ... 
 }
 

Traits

One of nice feature of Scala is its support of Traits, which are interfaces with implementation and are similar to Ruby mixins. Here is an example:

 trait Logger { def log(level: Level,
 message: String) = { Log.log(level, message)
 } }
 val dean = new Person(...) extends Logger
 dean.log(ERROR, "Bozo alert!!")
 

Scala also defines traits for functions to convert them into objects, e.g.

 trait Function1[A,R] extends AnyRef {
 def apply(a:A): R
 ... }
 

User-defined factory methods

You can define functions as factory-methods to instantiate objects, e.g.

  val persons = Map("dean" -> deanPerson, "alex", -> alexPerson)
 

DSL

Scala offers powerful semantics to define internal DSLs, e.g. you can create your own controls, e.g.

 import java.io._ object Loop {
 {...}
 }
 def loop(file: File, f: (Int,String) => Unit) =
 ...
 loop (new File("...")) { (n, line) => ...
 }
 

Options as alternative to Null

Scala avoid NullPointerExceptions by wrapping nulls into options, e.g.

 abstract class Option[T] {...} case class Some[T](t: T)
 extends Option[T] {...} case object None
 extends Option[Nothing] {...}
 

Case Classes

Case classes provide succint syntax for creatng Javabeans.

For comprehensions

Scala provides for comprehensions, which are similar to Python generators, e.g.

 val l = List( Some("a"), None, Some("b"), None, Some("c"))
 for (Some(s) <- l) yield s
 

Actors

Scala provides Actor based concurrency similar to Erlang, though there are multiple implementations and Akka seems to provide better implementation than what comes with Scala. Here is an example:

 case class Point(
 x: Double, y: Double)
 abstract class Shape { def draw()
 }
 ....
 package shapes import scala.actors._, Actor._ object ShapeDrawingActor
 extends Actor { def act() {
 loop { receive {
   case s:Shape =>
   s.draw()
 ... }
 } }
 }
 

Tutorials Day Two

On day two, I attended all-day Scala summit, which covered various topics for practical Scala.

Why Scala?

The summit started with session on "Why Scala?" by Alex Payne and Dean Wampler. Dean repeated some of same concepts from Monday's session on Scala's conciseness, concurrency, correctness, infix operator, type inference, case classes, etc. Dean then gave some examples of actors using Akka, where he calls multiple services using actors and then gather the results, e.g.

 val futures = for { s   <- services
 server <- allServersFor(s) }
 yield (server !!! HeartBeat)
 Futures.awaitAll(futures) val results = for {
 future <- futures
 result <- future.result } yield result val all = results reduceLeft(
 (r1, r2) => r1 merge r2 ) compact(render(all))
 }}
 

Akka: Simpler Scalability, Fault-Tolerance, Concurrency & Remoting through Actors

Jonas Bonér then gave brief overview of Akka, which provides a number ofabstractions for concurrency such as actors, STM, and agents. Jonas gave introduction to actors, which provide concurrency based on message-passing, shared-nothing, and mailbox. In Akka, actors can be thread based or event based, where event based actors are very light-weight and you can create millions of them (each actor takes about 600 bytes as opposed to 300 bytes in Erlang).

factory-methods

Akka uses factory-methods to hide type of actors, e.g.

 val counter = actorOf[Counter]  // counter is ActorRef
 actor.start
 actor.stop
 
 

Jonas suggested use of actorOf as opposed to "new Counter" syntax as it avoids calling methods on the objects directly. Akka uses !, !! and !!! notations to send messages, where ! is just fire and forget, !! collects results using Futures and !!! returns Future, e..g

 counter ! Tick // send message -- fire-forget
 val result  = (actor !! Message).as[String] // uses Future under the hood with timeout
 val resultOption = actor !! Message
 val result = resultOption.getOrElse(defaultResult)
 val result = resultOption.getOrElse(throw new Exception("time out"))
 val future = actor !!! Message
 future.await
 val result = future.get
 
 Futures.awaitOne(List(fut1, ..))
 Futures.awaitAll(List(fut1,))
 
 

You use self.reply to reply back to the sender and access sender using self.sender or self.senderFuture.

Immutable Messages

In order to keep actors free of side effects, messages must be immutable using case classes, tuples or lists, e.g.

 - case class Register(user: User)
 - actor ! Register(user)
 - actor ! (username, password)
 - actor ! List("xxx", "yy")
 

Dispatchers

Akka comes with a number of dispatches such as event based, thread based, reactor, etc. See Dispatchers class for more information.

Queues

Akka also comes with various queue types such as unbounded LinkedBlockingQueue, bounded LinkedBlockingQueue, etc.

ActorRegistry

ActorRegistry provides lookup methods for actors such as ActorRegistry.actorsFor.

Fault tolerance

Akka borrows concepts of hierarchy of supervisors for managing actors or processes from Erlang. Erlang's philosophy for fault tolerance is let it crash and the supervisor automatically starts failed process or group of processes. You use link(actor), unlink(actor), startLink(actor) to connect actors with supervisors and trap events using trapExit = List(classOf[ServiceException], classOf[PersistentException]), e.g.

 class Supervisor extends Actor {
   import self._
   trapExit = List(classOf[Throwable])
 }
 
 class FaultTolerantService extends Actor
   override def preRestart
   override def postRestart
 

Remote Actors

You can start a node, which is remotely accessible using:

 RemoteNode.start("localhost", 9999)
 spawnLinkRemote[MyActor]("darkstar", 9999)
 
 RemoteNode.register("service:id", )
 

STM, Transactors, Modules, Camel, Storage

We ran out of time for the rest of contents, but you can read more from the Slides.

Simple Build Tool

Next, Mark Harrah presented SBT, which everyone raved at the summit. SBT uses Scala based DSL for writing build scripts and internally uses Ivy for managing dependencies. You can create a new project by creating a new directory and typing sbt. You can set various properties in sbt shell such as target version, e.g.

 set build.scala.versions 2.80
 reload
 

You can easily create custom tasks in sbt by extending DefaultProject, e.g.

 import sbt._
 class MyProject(info: ProjectInfo) extends DefaultProject(info) {
   lazy val hi = task { println("Hi"); None}
   lazy val goodbye = task { println("Bye"); None} dependsOn(hi)
 }
 

You can also target tasks for just test using

 import sbt._
 class MyProject(info: ProjectInfo) extends DefaultProject(info) {
   val sc = "org.scala-tools.testing" %% "scalacheck" % "1.7" % "test"
 }
 OR
 import sbt._
 class MyProject(info: ProjectInfo) extends DefaultProject(info) {
   val sc = "org.scala-tools.sbt" %% "launcher-interface" % "0.74"
   val tt = "org.scala-tools.sbt" %% "launcher-interface" % "0.74" % "test"
 }
 

You can define main application as follows:

 import xsbti._
 class HW extends AppMain {
   def run(config: AppConfiguration): MainResult = {config.arguments foreach println; new Exit {def code = 0}}
 }
 

You generate executable jar by typing publish-local in sbt shell. You can define plugins as follows:

 class Plugins(inf: ProjectInfo) extends PluginDefinition(info) {
 val android = "org.scala-tools.sbt" % "sbt-android-plugin" % "0.5.0"
 }
 

Finally, sbt allows you to create processors, which behave like scaffolding in Rails, e.g.

 import xsbti._
 import processor._
 class HW extends BasicProcessor {
   def apply(project: Project, args: String) {
     import project._
         val contents = "This is " + name + "" + version + "\n" + args + "\n"
         FileUtilities.write(info.projectPath / "README" asFile, contents, log)
   }
 }
 

When you type publish, it will create README file for the project. That was pretty much the introduction to the sbt.

Specs & Scala, Tips and Tricks for a Friendly DSL Syntax

Eric Torreborre talked about Spec, which a BDD based testing tool for Scala. Spec provides support for BDD, Structures, Matchers, ScalaCheck, Mocks, Runners and databases. You use matchers to compare strings or XML contents, e.g.

 class Reverse2Spec extends  Specficiation {
   reverse("") must_== ""
 ...
 
 

You can restrict scope by defining tag method, e.g.

 class Example(des : String) {
   def in(arg: Any) = expectations
   def tag(t : String) = this
 }
 

Scala DSL

Spec uses a number of tricks for simplifying the syntax such as implicit parameters, operators, lazy evaluation.

 "With a 3 tests ok" in {
   prop 
 }
 //Some paraemeters can be implicit
 implicit val defaultparams = new Params
 
 "this should not explode" in {
   error("boom")
 }
 def in(e: => Any) =                                     // parameters are evaluated lazily when you use it
 

It also uses principles such as add principle by adding new functionality, e.g.

 result + 1
 result.pp + 1
 

Spec also supports table similar to Fit and Fitness for writing concise tests. Overall, I was impressed with wide set of tools for writing tests.

Lift: Quick and Fun

Lift is a Scala based web framework for writing secure, typesafe, concise, and interactive (like desktop) applications. It abstracts much of plumbing of HTTP, which I personally don't like as I have found web frameworks that does that results in leaky abstractions. Lift also uses stateful web applications, which require sticky sessions, which is another area that I have found to be problematic for scalability and upgrade. Here is an example of Lift chat server:

 package code.comet
 import net.liftweb._
 import http._
 import actor._
 import scala.xml.
 object ChatServer extends LiftActor withListenerManager {
         private var msgs = List("Welcome")
         def createUpdate = msgs
         override def lowPriority = {
                 case s: String => msgs ::= s; updateListeners()
         }
 }
 
 class Chat extends CometActor withCometListener {
   private var msgs: List[String] = Nil
   def regiserWith = ChatServer
   override def lowPriority = {
      case l: List[String] = msgs = l; reRender(false)  // don't use reRender
   }
   def line(in: NodeSeq) : NodeSeq = msgs.reverse.flatMap(m => bind("chat", in, "item" -> m))
   def render = bind("chat", "line" -> line _)
 }
 

In Lift, every component has GUID and version that was used to render and then sets up long poll and then receive deltas (every 100ms). You can use sbt to deploy jetty and prepare war file, e.g.

 sbt
 >jetty-run
 >prepareWeb                                                     // uses JRebel to reload classes
 

Rewiring Android with Scala

This was another interesting talk by Nathan Hamblen for using Scala for writing Android applications. The key selling point of Scala has been conciseness of the language, and you can write simple code such as:

 dialog.setOnShowListener { di: DialogInterface => 
   runSomeCode() 
 }
 

instead of

 dialog.setOnShowListener(
   new DialogInterface.OnShowListener() {
     public void onShow(DialogInterface interface) {
       runSomeCode();
     }
   }
 );
 

or

 future { runSomeCode(myObject) }
 

instead of

 new AsyncTask () {
   protected Integer doInBackground(MyObject... objs) {
     runSomeCode(objs[0]);
   }
 }.execute(myObject);
 
 

Nathan showed how you can define Scala traits to add lazy handlers for android code, e.g.

 trait ScalaActivity extends Activity {
 ...
 lazy val handler = new Handler
 def post(block: => Unit) { 
   handler.post(new Runnable{
     def run { block }
   })
 }
 

Or you can extend APIs, e.g.

 implicit def f2cancel(block: DialogInterface => Unit) = 
   new DialogInterface.OnCancelListener {
     def onCancel(dialog: DialogInterface) { 
       block(dialog) 
     }
   }
 ...
 new AlertDialog.Builder(this)
   .setOnCancelListener { 
     di: DialogInterface => finish() 
   }
 

Nathan also showed a plugin (sbt-android-plugin) to create type-safe layout instead of using R.java file generated by Android, which you can get it from git clone git://github.com/meetup/meetabout.git. On the downside, Scala based android applications require Scala jar files and the size of application becomes considerable large. Though, you can use tools to extract the classes that you need, but it would still be larger than Java code.

Scala in Practice

Alex Payne and Coda Hale had a section on Scala in practice, but it was only Q/A session. I was a bit disappointed that they didn't come prepare with actual usage or war stories from their work environment.

High Wizardry in the Land of Scala

The last section of the day was a bit on type and category theory, which was interesting but mostly theortical. Daniel Spiewak explained difference between kind and type system. The only tip from the session I got was that Values are to types as types are to kinds. Finally, Daniel explained that newly released 2.8.0 version of Scala supports continuation but it's all broken and useless.

Summary

Overall, I found sessions on both Android and Scala were well worth the time and it peaked my interest in both. I think the ecosystem of Scala has matured and there is better tools support with the new version (2.8). I am going to try to influence co-workers into using it for new development. I am also going to start Android project pretty soon but I am a bit hesitant on writing in Scala due to increased application size.


May 10, 2010

Building a stock quote server in Erlang using Ejabberd, XMPP, Bosh, Exmpp, Strophe and Yaws

Filed under: Erlang — admin @ 1:40 pm

Recently, I have been building a stock quote server at work that publishes financial data using using Ejabberd, XMPP, PubSub, Exmpp and Bosh on the server side and Strophe library on the web application front. I will describe a simplified implementation of the quote server using Yahoo Quotes.

Installation

Download Ejabberd and go through the installation wizad. You will be asked your host name, admin account/password and whether ejabberd would be running in a clustered environment. For this tutorial, we will be running ejabberd on a single. Once installed, you can start the ejabbered server using

 /Applications/ejabberd-2.1.3/bin/ejabberdctl start
 

As, I am using Mac, the actual path on your machine may be different. The ejabbered comes with a web baesd admin tool, that you can access using

 http://<your-host-name>:5280/admin
 

and you would be able to see available nodes, users, etc.


Registering Users

We will be creating two users: producer and consumer, where the former would be used for publishing stock quotes and latter would be used for subscribing quotes on the web side, i.e.,

 sudo /Applications/ejabberd-2.1.3/bin/ejabberdctl register producer  producer
 sudo /Applications/ejabberd-2.1.3/bin/ejabberdctl register consumer  consumer
 

Debuging with Psi

You can debug XMPP communications using a jabber client such as Psi, which you can download. After you download, you can install and specify your local hostname as a server, e.g.



You can then login using consumer@<your-host-name> with password consumer. As, we will be using PubSub protocol, you can discover available nodes or topics using General->Service Discovery from the menu, e.g.


Downloading Sample Code

I have stored all code needed for this example on http://github.com/bhatti/FQPubSub, that you can checkout using:

 git clone git@github.com:bhatti/FQPubSub.git
 

The sample code depends on exmpp, lhttpc, jsonerl, and yaws modules so after downloading the code, checkout dependent modules using

 git submodule init
 git submodule update
 

Above commands will checkout dependent modules in deps directory.

Building Sample Code

Before building, ensure you have make and autoconf tools installed, then replace <paraclete.local> with your <your-host-name> in docroot/index.html and src/quote_utils.hrl. Then type following command

 make
 

to build all sample code and dependent libraries

Starting Web Server

Though, the web code including Srophe library and Javascript can be run directly in the browser, but you can start Yaws to serve the application as follows:

 erl -pa ebin deps/exmpp/ebin/ deps/lhttpc/ebin/ deps/yaws/ebin -boot start_sasl -run web_server start 
 

Note, that the web server will be continuously running, so you can open a separate shell before typing above command.

Publishing Quotes

Create two separate shells and type following command in first shell:

   erl -pa ebin deps/exmpp/ebin/ deps/lhttpc/ebin/ deps/yaws/ebin -boot start_sasl -run quote_publisher start AAPL
 

and following command in second shell

   erl -pa ebin deps/exmpp/ebin/ deps/lhttpc/ebin/ deps/yaws/ebin -boot start_sasl -run quote_publisher start IBM
 

Above commands will start Erlang processes, that will poll Yahoo Quotes every second and publish the quotes on the node AAPL and IBM respectively.

Next point your browser to http://<your-host-name>:8000/, and add “IBM” and “AAPL” symbols, you would then see quotes for both symbols, e.g.

Code under the hood

Now that you are able to run the example, let’s take a look at the code how it works:

Client library for Yahoo Finance

Though, at work we use our own real time stock quote feed, but for this sample I implemented stock quote feed using Yahoo Finance. The src/yquote_client.hrl and src/yquote_client.erl define client API for accessing Yahoo finance service. Here is the Erlang code for requesting the quote using HTTP request and parsing it:

  1 %%%-------------------------------------------------------------------
 
  2 %%% File : yquote_client.erl
  3 %%% Author : Shahzad Bhatti
  4 %%% Purpose : Wrapper Library for Yahoo Stock Quotes
 
  5 %%% Created : May 8, 2010
  6 %%%-------------------------------------------------------------------
  7 
  8 -module(yquote_client).
 
  9 
 10 -author('bhatti@plexobject.com').
 11 
 12 -export([
 13          quote/1
 14         ]).
 
 15 
 16 -record(quote, {
 17         symbol,
 18         price,
 19         change,
 20         volume,
 
 21         avg_daily_volume,
 22         stock_exchange,
 23         market_cap,
 24         book_value,
 25         ebitda,
 26         dividend_per_share,
 
 27         dividend_yield,
 28         earnings_per_share,
 29         week_52_high,
 30         week_52_low,
 31         day_50_moving_avg,
 32         day_200_moving_avg,
 
 33         price_earnings_ratio,
 34         price_earnings_growth_ratio,
 35         price_sales_ratio,
 36         price_book_ratio,
 37         short_ratio}).
 38 
 
 39 
 40 
 41 quote(Symbol) ->
 42     inets:start(),
 43     {ok,{_Status, _Headers, Response}} = http:request(get, {url(Symbol), []},
 
 44         [{timeout, 5000}], [{sync, true}]),
 45 
 46     Values = re:split(Response, "[,\r\n]"),
 47     #quote{
 
 48         symbol = list_to_binary(Symbol),
 49         price = to_float(lists:nth(1, Values)),
 50         change = to_float(lists:nth(2, Values)),
 51         volume = to_integer(lists:nth(3, Values)),
 
 52         avg_daily_volume = to_integer(lists:nth(4, Values)),
 53         stock_exchange = lists:nth(5, Values), % to_string
 54         market_cap = to_float(lists:nth(6, Values)), % B
 
 55         book_value = to_float(lists:nth(7, Values)),
 56         ebitda = to_float(lists:nth(8, Values)), % B
 57         dividend_per_share = to_float(lists:nth(9, Values)),
 
 58         dividend_yield = to_float(lists:nth(10, Values)),
 59         earnings_per_share = to_float(lists:nth(11, Values)),
 60         week_52_high = to_float(lists:nth(12, Values)),
 61         week_52_low = to_float(lists:nth(13, Values)),
 
 62         day_50_moving_avg = to_float(lists:nth(14, Values)),
 63         day_200_moving_avg = to_float(lists:nth(15, Values)),
 64         price_earnings_ratio = to_float(lists:nth(16, Values)),
 65         price_earnings_growth_ratio = to_float(lists:nth(17, Values)),
 
 66         price_sales_ratio = to_float(lists:nth(18, Values)),
 67         price_book_ratio = to_float(lists:nth(19, Values)),
 68         short_ratio = to_float(lists:nth(20, Values))}.
 69 
 
 70 url(Symbol) ->
 71     "http://finance.yahoo.com/d/quotes.csv?s=" ++ Symbol ++ "&f=l1c1va2xj1b4j4dyekjm3m4rr5p5p6s7".
 72 
 
 73 to_float(<<"N/A">>) ->
 74     -1;
 75 to_float(Bin) ->
 76     {Multiplier, Bin1} = case bin_ends_with(Bin, <<$B>>) of
 
 77         true ->
 78             {1000000000, bin_replace(Bin, <<$B>>, <<>>)};
 79         false ->
 80             case bin_ends_with(Bin, <<$M>>) of
 
 81                 true ->
 82                     {1000000, bin_replace(Bin, <<$M>>, <<>>)};
 83                 false ->
 84                     {1,Bin}
 
 85             end
 86     end,
 87     L = binary_to_list(Bin1),
 88     list_to_float(L) * Multiplier.
 
 89 
 90 
 91 
 

Note that I am omitting some code in above listing, as I just wanted to highlight HTTP request and parsing code.

Publishing the Stock Quote

I used exmpp library to communicate with the XMPP server in Erlang. Here is the code for publishing the quotes using Bosh/XMPP protocol:

  1 %%%-------------------------------------------------------------------
 
  2 %%% File : quote_publisher.erl
  3 %%% Author : Shahzad Bhatti
  4 %%% Purpose : OTP server for publishing quotes
 
  5 %%% Created : May 8, 2010
  6 %%%-------------------------------------------------------------------
  7 -module(quote_publisher).
 
  8 
  9 -export([
 10     start/1,
 11     start/5,
 12     stop/1]).
 13 
 
 14 -export([init/5]).
 15 
 16 -include_lib("quote_utils.hrl").
 17 
 18 -record(state, {session, jid, service=?TEST_XMPP_PUBSUB, symbol}).
 
 19 
 20 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 21 %% APIs
 22 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
 23 start(Symbol) ->
 24     start(?TEST_XMPP_SERVER, ?TEST_XMPP_PORT, ?PRODUCER_USERNAME,
 25         ?PRODUCER_PASSWORD, Symbol).
 
 26 
 27 start(Host, Port, User, Password, Symbol) ->
 28     spawn(?MODULE, init, [Host, Port, User, Password, Symbol]).
 
 29 
 30 stop(Pid) ->
 31     Pid ! stop.
 32   
 33 init(Host, Port, User, Password, Symbol) ->
 
 34     {ok, {MySession, MyJID}} = quote_utils:connect(Host, Port, User, Password),
 35     State = #state{session=MySession, jid=MyJID, symbol = Symbol},
 
 36     create_symbol_node(State),
 37     loop(State).
 38 
 39 loop(#state{session=MySession, jid=_MyJID, service = _Service,
 
 40         symbol = _Symbol}=State) ->
 41     receive
 42         stop ->
 43             quote_utils:disconnect(MySession);
 
 44         Record = #received_packet{packet_type=message, raw_packet=_Packet} ->
 45             loop(State);
 46         Record ->
 
 47             loop(State)
 48     after 2000 ->
 49         publish_quote(State),
 50         loop(State)
 
 51     end.
 52 
 53 create_symbol_node(#state{session=MySession, jid=MyJID, service = Service,
 
 54         symbol = Symbol}) ->
 55     IQ = exmpp_client_pubsub:create_node(Service, Symbol),
 56     PacketId = exmpp_session:send_packet(MySession, exmpp_stanza:set_sender(IQ, MyJID)),
 
 57     PacketId2 = erlang:binary_to_list(PacketId),
 58     receive #received_packet{id=PacketId2, raw_packet=Raw} ->
 
 59       case exmpp_iq:is_error(Raw) of
 60         true -> {error, Raw};
 61         _ -> ok
 
 62       end
 63     end.
 64   
 65 publish_quote(#state{session=MySession, jid=MyJID, service = Service, symbol = Symbol}) ->
 
 66     Quote = yquote_client:quote(Symbol),
 67     JsonQuote = ?record_to_json(quote, Quote),
 68     M = exmpp_xml:element(?QUOTE_DATA),
 
 69     IQ = exmpp_client_pubsub:publish(Service, Symbol, exmpp_xml:append_cdata(M,
 70             JsonQuote)),
 71     Xml = exmpp_stanza:set_id(exmpp_stanza:set_sender(IQ, MyJID), Symbol),
 
 72     PacketId = exmpp_session:send_packet(MySession, exmpp_stanza:set_sender(IQ, MyJID)),
 73     PacketId2 = erlang:binary_to_list(PacketId),
 
 74     receive #received_packet{id=PacketId2, raw_packet=Raw} ->
 75       case exmpp_iq:is_error(Raw) of
 
 76         true -> error;
 77         _ -> ok
 78       end
 79     end.
 
 80 
 81 
 82 
 

In above code, a process is created for each symbol, which periodically polls stock quote and publishes it to the XMPP node using pubsub/bosh protocol. Note that a unique node is created for each symbol and node must be created before anyone can publish or subscribe. Also, note that publish/subscribe APIs use request/ack protocol, so after sending the request, the process retrieves the acknowledgement of the request.

Here are some utility functions used by the publisher:

  1 -module(quote_utils).
 
  2   
  3 -include_lib("quote_utils.hrl").
  4 
  5 -export([
  6     init_session/2,
 
  7     connect/4,
  8     disconnect/1]).
  9 
 10 bosh_url(Host, Port) ->
 
 11     "http://" ++ Host ++ ":" ++ integer_to_list(Port) ++ "/http-bind".
 12 
 
 13 
 14 connect(Host, _Port, User, Password) ->
 15     safe_start_apps(),
 
 16     MySession = exmpp_session:start({1,0}),
 17     exmpp_xml:start_parser(), %% Create XMPP ID (Session Key):
 18     MyJID = exmpp_jid:make(User, Host, random),
 
 19     %% Create a new session with basic (digest) authentication:
 20     exmpp_session:auth_basic_digest(MySession, MyJID, Password),
 21     
 
 22     
 23     {ok, _StreamId, _Features} = exmpp_session:connect_BOSH(MySession, bosh_url(Host, 5280), Host, []),
 
 24     try quote_utils:init_session(MySession, Password)
 25     catch
 26         _:Error -> io:format("got error: ~p~n", [Error]), {error, Error}
 
 27     end,
 28     {ok, {MySession, MyJID}}.
 29 
 30 init_session(MySession, Password) ->
 
 31     %% Login with defined JID / Authentication:
 32     try exmpp_session:login(MySession, "PLAIN")
 33     catch
 
 34         throw:{auth_error, 'not-authorized'} ->
 35         %% Try creating a new user:
 36         io:format("Register~n",[]),
 37         %% In a real life client, we should trap error case here
 
 38         %% and print the correct message.
 39         exmpp_session:register_account(MySession, Password),
 40         %% After registration, retry to login:
 
 41         exmpp_session:login(MySession)
 42     end,
 43     %% We explicitely send presence:
 44     exmpp_session:send_packet(MySession, exmpp_presence:set_status(exmpp_presence:available(), "Ready to publish!!!")),
 
 45     ok.
 46 
 47 disconnect(MySession) ->
 48     exmpp_session:stop(MySession).
 49 
 
 50 safe_start_apps() ->
 51     try start_apps()
 52     catch
 53         _:Error -> io:format("apps already started : ~p~n", [Error]), {error, Error}
 
 54     end.
 55 
 56 start_apps() ->
 57     ok = application:start(exmpp),
 58     ok = application:start(crypto),
 59     ok = application:start(ssl),
 
 60     ok = application:start(lhttpc).
 61 
 

Note that above code auto-registers users, which is not recommended for production use.

Javascript code using Strophe library

The web application depends on jQuery, Strophe and Strophe Pubsub. These libraries are included in docroot directory that are imported by index.html. The Strophe library and ejabbered 2.1.3 version supports cross domain scripting so that bosh service here doesn’t need to be on the same domain/port, but it must have a /crossdomain.xml policy file that allows access from wherever index.html lives. The Javascript initializes the connection parameter as follows (you would have to change Host):

   1 <script type="text/javascript">
 
   2     // The BOSH_SERVICE here doesn't need to be on the same domain/port, but
 
   3     // it must have a /crossdomain.xml policy file that allows access from
 
   4     // wherever crossdomain.html lives.
   5     // TODO: REPLACE <paraclete.local> with your <host-name>
 
   6     var HOST = 'paraclete.local';
   7     var JID = 'consumer@' + HOST;
 
   8     var PASSWORD = 'consumer';
   9     var BOSH_SERVICE = 'http://' + HOST + ':5280/http-bind'; //'/xmpp-httpbind'
 
  10     var PUBSUB = 'pubsub.' + HOST;
  11     var connection = null;
 
  12     var autoReconnect = true;
  13     var hasQuotes = [];
  14     var subscriptions = [];
 
  15   
  16     function log(msg) {
  17         $('#log').append('<div></div>').append(document.createTextNode(msg));
 
  18     }
  19   
  20     function rawInput(data) {
  21         //log('RECV: ' + data);
 
  22     }
  23     
  24     function rawOutput(data) {
  25         //log('SENT: ' + data);
 
  26     }
  27     function onQuote(stanza) {
  28         //log('onQuote###### ' + stanza);
 
  29         try {
  30             $(stanza).find('event items item data').each(function(idx, elem) {
  31                 quote = jQuery.parseJSON($(elem).text());
 
  32                 //{"price":235.86,"change":-10.39,"volume":59857756,"avg_daily_volume":20775600,"stock_exchange":[78,97,115,100,97,113,78,77],"market_cap":2.146e+11,
 
  33                 //"book_value":43.257,"ebitda":1.5805e+10,"dividend_per_share":0.0,"dividend_yield":-1,"earnings_per_share":11.796,"week_52_high":272.46,"week_52_low":119.38,
 
  34                 //"day_50_moving_avg":245.206,"day_200_moving_avg":214.119,"price_earnings_ratio":20.88,"price_earnings_growth_ratio":1.05,"price_sales_ratio":4.38,
 
  35                 //"price_book_ratio":5.69,"short_ratio":0.7}
  36                 if (hasQuotes[quote.symbol] != undefined) {
 
  37                     $('price_' + quote.symbol).innerHTML = quote.price;
  38                     $('change_' + quote.symbol).innerHTML = quote.change;
  39                     $('volume_' + quote.symbol).innerHTML = quote.volume;
 
  40                 } else {
  41                     hasQuotes[quote.symbol] = true;
  42                     $('#quotesTable > tbody:last').append('<tr id="quote_' +
 
  43                         quote.symbol + '"><td>' + quote.symbol +
  44                         '</td><td id="price_' + quote.symbol + '">' + quote.price +
 
  45                         '</td><td id="change_' + quote.symbol + '" class="class_change_' + quote.symbol + '">' +
  46                         quote.change + '</td><td id="volume_' +
 
  47                         quote.symbol + '">' +
  48                         quote.volume + '</td></tr>');
  49                 }
 
  50 
  51                 if(quote.change < 0) {
  52                     $('.class_change_' + quote.symbol).css('color', 'red');
 
  53                 } else {
  54                     $('.class_change_' + quote.symbol).css('color', 'green');
 
  55                 }
  56             });
  57         } catch (e) {
  58             log(e)
 
  59         }
  60         return true;
  61     }
  62 
 
  63     function handleSubscriptionChange (stanza) {
  64         //log("***handleSubscriptionChange Received: " + stanza);
 
  65     }
  66         
  67     function onConnect(status) {
  68         if (status == Strophe.Status.CONNECTING) {
 
  69             log('Strophe is connecting.');
  70         } else if (status == Strophe.Status.CONNFAIL) {
  71             log('Strophe failed to connect.');
 
  72             $('#connect').get(0).value = 'connect';
  73         } else if (status == Strophe.Status.DISCONNECTING) {
 
  74             log('Strophe is disconnecting.');
  75         } else if (status == Strophe.Status.DISCONNECTED) {
  76             if (autoReconnect) {
 
  77                 log( "Streaming disconnected. Trying to reconnect...", METHODNAME );
  78                 connection.connect($('#jid').get(0).value, $('#pass').get(0).value, onConnect);
  79                 log( "Streaming reconnected.", METHODNAME );
 
  80             } else {
  81                 log('Strophe is disconnected.');
  82                 $('#connect').get(0).value = 'connect';
 
  83                 //publishEvent( "streamingDisconnected" );
  84             }
  85         } else if (status == Strophe.Status.CONNECTED) {
 
  86             log('Strophe is connected.');
  87             //log('QUOTE_BOT: Send a message to ' + connection.jid + ' to talk to me.');
 
  88             connection.addHandler(onMessage, null, 'message', null, null, null);
  89             connection.send($pres().tree());
 
  90             publishEvent( "streamingConnected" );
  91         }
  92     }
  93 
 
  94     function subscribe(symbol) {
  95         if (subscriptions[symbol]) return;
  96         try {
 
  97             connection.pubsub.subscribe(JID, PUBSUB, symbol, [], onQuote, handleSubscriptionChange);
  98             subscriptions[symbol] = true;
  99             log("Subscribed to " + symbol);
 
 100         } catch (e) {
 101             alert(e)
 102         }
 103     }
 104     function unsubscribe(symbol) {
 
 105         if (!subscriptions[symbol]) return;
 106         try {
 107             connection.pubsub.unsubscribe(JID, PUBSUB, symbol, handleSubscriptionChange);
 108             subscriptions[symbol] = false;
 
 109             log("Unsubscribed from " + symbol);
 110         } catch (e) {
 111             alert(e)
 112         }
 
 113     }
 114   
 115     function onMessage(msg) {
 116         var to = msg.getAttribute('to');
 
 117         var from = msg.getAttribute('from');
 118         var type = msg.getAttribute('type');
 119         var elems = msg.getElementsByTagName('body');
 
 120   
 121         if (type == "chat" && elems.length > 0) {
 122             var body = elems[0];
 
 123             log('QUOTE_BOT: I got a message from ' + from + ': ' + Strophe.getText(body));
 124             var reply = $msg({to: from, from: to, type: 'chat'}).cnode(Strophe.copyElement(body));
 125             connection.send(reply.tree());
 
 126             log('QUOTE_BOT: I sent ' + from + ': ' + Strophe.getText(body));
 127         }
 128         // we must return true to keep the handler alive.
 
 129         // returning false would remove it after it finishes.
 
 130         return true;
 131     }
 132  
 133     $(document).ready(function () {
 
 134         connection = new Strophe.Connection(BOSH_SERVICE);
 135         connection.rawInput = rawInput;
 136         connection.rawOutput = rawOutput;
 137         connection.connect(JID, PASSWORD, onConnect);
 138         //connection.disconnect();
 
 139         $('#add_symbol').bind('click', function () {
 140             var symbol = $('#symbol').get(0).value;
 
 141             subscribe(symbol);
 142         });
 143     });
 144 
 145 </script>
 146 
 
 

When the document is loaded, the connection to the ejabberd server is established. Here is the form and table that is used to add subscription and display current quote information for the symbols:

  1 <form name='symbols'>
 
  2     <label for='symbol'>Symbol:</label>
  3     <input type='text' id='symbol'/>
 
  4     <input type='button' id='add_symbol' value='add' />
 
  5 </form>
  6 <hr />
  7 <div id='log'></div>
 
  8 <table id="quotesTable" width="600" border="2" bordercolor="#333333">
 
  9     <thead>
 10         <tr>
 11             <th>Symbol</th>
 
 12             <th>Price</th>
 13             <th>Change</th>
 14             <th>Volume</th>
 
 15         </tr>
 16     </thead>
 17     <tbody>
 18     </tbody>
 
 19 </table>
 20 
 

When the form is submitted, it calls subscribe method, which in turn sends request to the ejabbered server for subscription. When a new quote is received, it calls onQuote function, which inserts a row in the table when a new symbol is added or updates the quote information if it already exists.

Conclusion

The ejabberd, XMPP, exmpp, Bosh and Strophe provides a robust and mature solution for messaging and are especially suitable for web applications that want to build highly scalable and interactive applications. Though, above code is fairly simple, but same design principles can be used to support large number of stock quotes updates. As, we need to send stock quotes from tens of thousands symbols for every tick within a fraction of a second, the Erlang provides very scalable solution, where each symbol is simply served by an Erlang process. Finally, I am still learning more about Ejabberd’s clustering, security, and other features so that it can truly survive the production load, so I would love to hear any feedback you might have with similar systems.

References


March 17, 2010

Smarter Email appender for Log4j with support of duplicate-removal, summary-report and JMX

Filed under: Computing — admin @ 5:06 pm

I have been using SMTPAppender for a while to notify developers when something breaks on the production site and for most part it works well. However, due to some misconfiguration or service crash it can result in large number of emails. I was struck by similar problem at work when my email box suddently got tons of emails from the production site. So I decided to write a bit intelligent email appender. My goals for the appender were:

  • Throttle emails based on some configured time
  • Remove duplicate emails
  • Support JMX for dynamic configuration
  • Provide summary report with count of errors and their timings

I created FilteredSMTPAppender class that extends SMTPAppender. The FilteredSMTPAppender defines a nested class Stats for keeping track of errors. For each unique exception, it creates an instance of Stats, that stores the first and last occurrence of this exception as well as count. The Stats class uses hash of stack trace to identify unique exceptions, however it ignores first line, which often stores some dynamic information. FilteredSMTPAppender registers iteslf as MBean so that it can be configured at runtime. It overrides append method to capture the event and overrides checkEntryConditions to add filtering. It also changes the layout so that the summary count of error messages are added to the footer of email message.

The FilteredSMTPAppender uses a number of helper classes such as ServiceJMXBeanImpl for MBean definition, LRUSortedList to keep fixed cache of exceptions. Here is listing of LRUSortedList and ServiceJMXBeanImpl.

Listing of FilteredSMTPAppender.java

   1 package com.plexobject.log;
 
   2 
   3 import java.beans.PropertyChangeEvent;
   4 import java.beans.PropertyChangeListener;
   5 import java.util.Comparator;
 
   6 import java.util.Date;
   7 
   8 import javax.mail.MessagingException;
   9 
 
  10 import org.apache.commons.lang.builder.EqualsBuilder;
  11 import org.apache.commons.lang.time.FastDateFormat;
  12 
  13 import org.apache.log4j.Layout;
 
  14 import org.apache.log4j.net.SMTPAppender;
  15 import org.apache.log4j.spi.LoggingEvent;
  16 
  17 import com.plexobject.jmx.JMXRegistrar;
 
  18 import com.plexobject.jmx.impl.ServiceJMXBeanImpl;
  19 import com.plexobject.metrics.Metric;
  20 import com.plexobject.metrics.Timer;
 
  21 import com.plexobject.util.Configuration;
  22 import com.plexobject.util.LRUSortedList;
  23 
  24 public class FilteredSMTPAppender extends SMTPAppender {
 
  25 
  26     private static final String SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS = "smtp.filter.min.duplicate.interval.secs";
  27     private static final int MAX_STATS = Configuration.getInstance().getInteger("smtp.filter.max", 100);
 
  28     private static int MIN_DUPLICATE_EMAILS_INTERVAL = Configuration.getInstance().getInteger(SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS,
  29             60); // 1 minute
  30     private static final Date STARTED = new Date();
 
  31     private static final FastDateFormat DATE_FMT = FastDateFormat.getInstance("MM/dd/yy HH:mm");
  32 
  33     final static class Stats implements Comparable<Stats> {
 
  34 
  35         final int checksum;
  36         final long firstSeen;
 
  37         long lastSeen;
  38         long lastSent;
  39         int numSeen;
 
  40         int numEmails;
  41 
  42         Stats(LoggingEvent event) {
  43             StringBuilder sb = new StringBuilder();
 
  44             String[] trace = event.getThrowableStrRep();
  45             for (int i = 1; i < trace.length && i < 20; i++) { // top 20 lines
 
  46                 // of trace
  47                 sb.append(trace[i].trim());
  48             }
  49             this.checksum = sb.toString().hashCode();
 
  50             firstSeen = lastSeen = System.currentTimeMillis();
  51             numSeen = 1;
  52         }
  53 
  54         boolean check() {
 
  55             long current = System.currentTimeMillis();
  56             long elapsed = current - lastSent;
  57 
  58             numSeen++;
 
  59             lastSeen = current;
  60 
  61             if (elapsed > MIN_DUPLICATE_EMAILS_INTERVAL * 1000) {
  62                 lastSent = current;
 
  63                 numEmails++;
  64                 return true;
  65             } else {
 
  66                 return false;
  67             }
  68         }
  69 
 
  70         @Override
  71         public boolean equals(Object object) {
  72             if (!(object instanceof Stats)) {
 
  73                 return false;
  74             }
  75             Stats rhs = (Stats) object;
  76             return new EqualsBuilder().append(this.checksum, rhs.checksum).isEquals();
 
  77 
  78         }
  79 
  80         @Override
  81         public int hashCode() {
 
  82             return checksum;
  83         }
  84 
  85         @Override
 
  86         public String toString() {
  87             return " (" + checksum + ") occurred " + numSeen + " times, " + numEmails + " # of emails, first @" + DATE_FMT.format(new Date(firstSeen)) + ", last @" + DATE_FMT.format(new Date(lastSeen)) + " since server started @" + DATE_FMT.format(STARTED);
 
  88         }
  89 
  90         @Override
  91         public int compareTo(Stats other) {
 
  92             return checksum - other.checksum;
  93         }
  94     }
  95 
 
  96     final static class StatsCmp implements Comparator<Stats> {
  97 
 
  98         @Override
  99         public int compare(Stats first, Stats second) {
 100             return first.checksum - second.checksum;
 
 101         }
 102     }
 103     private static final LRUSortedList<Stats> STATS_LIST = new LRUSortedList<Stats>(
 
 104             MAX_STATS, new StatsCmp());
 105     private LoggingEvent event;
 106     private ServiceJMXBeanImpl mbean;
 107     private Layout layout;
 
 108 
 109     public FilteredSMTPAppender() {
 110         mbean = JMXRegistrar.getInstance().register(getClass());
 111         mbean.addPropertyChangeListener(new PropertyChangeListener() {
 112 
 
 113             @Override
 114             public void propertyChange(PropertyChangeEvent event) {
 115                 try {
 116                     if (event != null && SMTP_FILTER_MIN_DUPLICATE_INTERVAL_SECS.equalsIgnoreCase(event.getPropertyName())) {
 
 117                         MIN_DUPLICATE_EMAILS_INTERVAL = Integer.parseInt((String) event.getNewValue());
 118                     }
 119                 } catch (Exception e) {
 120                     e.printStackTrace();
 121                 }
 
 122             }
 123         });
 124 
 125     }
 126 
 127     public void append(LoggingEvent event) {
 
 128         this.event = event;
 129         if (layout == null) {
 130             layout = getLayout();
 131         }
 
 132         super.append(event);
 133     }
 134 
 135     protected boolean checkEntryConditions() {
 136         final Timer timer = Metric.newTimer(getClass().getSimpleName() + ".checkEntryConditions");
 
 137         try {
 138             boolean check = true;
 139             if (event != null) {
 
 140                 Stats newStats = new Stats(event);
 141                 Stats stats = STATS_LIST.get(newStats);
 142                 if (stats == null) {
 143                     stats = newStats;
 
 144                     STATS_LIST.add(stats);
 145                 } else {
 146                     check = stats.check();
 147                 }
 148                 if (check) {
 
 149                     setMessageFooter(stats);
 150                 }
 151             }
 152             return check && super.checkEntryConditions();
 
 153         } finally {
 154             timer.stop();
 155         }
 156     }
 157 
 
 158     private void setMessageFooter(Stats stats) {
 159         String message = event.getMessage().toString();
 160 
 161         final String footer = "\n\n-------------------------\n" + message + " - " + stats;
 
 162 
 163         if (layout != null) {
 164             setLayout(new Layout() {
 165 
 
 166                 @Override
 167                 public void activateOptions() {
 168                     layout.activateOptions();
 169 
 170                 }
 
 171 
 172                 @Override
 173                 public String format(LoggingEvent evt) {
 174                     return layout.format(evt);
 175                 }
 
 176 
 177                 @Override
 178                 public String getFooter() {
 179                     return footer;
 180                 }
 
 181 
 182                 @Override
 183                 public boolean ignoresThrowable() {
 184                     return layout.ignoresThrowable();
 
 185                 }
 186             });
 187         }
 188     }
 189 }
 190 
 
 191 
 

Listing of ServiceJMXBeanImpl.java

   1 package com.plexobject.util;
 
   2 
   3 import java.util.ArrayList;
   4 import java.util.Collection;
   5 import java.util.Collections;
 
   6 import java.util.Comparator;
   7 import java.util.Iterator;
   8 import java.util.List;
 
   9 import java.util.ListIterator;
  10 
  11 import org.apache.log4j.Logger;
  12 
 
  13 
  14 public class LRUSortedList<T> implements List<T> {
 
  15     private static final Logger LOGGER = Logger.getLogger(LRUSortedList.class);
  16     private final int max;
 
  17     private final Comparator<T> comparator;
  18 
  19     private final List<Pair<Long, T>> list = new ArrayList<Pair<Long, T>>();
 
  20     private final List<Pair<Long, Integer>> timestamps = new ArrayList<Pair<Long, Integer>>();
 
  21 
  22     // comparator to sort by timestamp
  23     private static final Comparator<Pair<Long, Integer>> CMP = new Comparator<Pair<Long, Integer>>() {
 
  24         @Override
  25         public int compare(Pair<Long, Integer> first, Pair<Long, Integer> second) {
 
  26             if (first.getFirst() < second.getFirst()) {
  27                 return -1;
  28             } else if (first.getFirst() > second.getFirst()) {
 
  29                 return 1;
  30             } else {
  31                 return 0;
 
  32             }
  33         }
  34     };
  35 
  36     public LRUSortedList(int max, Comparator<T> comparator) {
 
  37         this.max = max;
  38         this.comparator = comparator;
  39     }
  40 
 
  41     @Override
  42     public boolean add(T e) {
  43         if (list.size() > max) {
 
  44             removeOldest();
  45         }
  46         // add object
  47         long timestamp = System.nanoTime();
 
  48         int insertionIdx = Collections.binarySearch(this, e, comparator);
  49         if (insertionIdx < 0) {// not found
 
  50             insertionIdx = (-insertionIdx) - 1;
  51             list.add(insertionIdx, new Pair<Long, T>(timestamp, e));
  52         } else {
 
  53             // found
  54             list.set(insertionIdx, new Pair<Long, T>(timestamp, e));
  55         }
 
  56 
  57         // as timestamps are sorted, we just remove the oldest (first)
  58         if (timestamps.size() > max) {
 
  59             timestamps.remove(0);
  60         }
  61         // update timestamp
  62         Pair<Long, Integer> t = new Pair<Long, Integer>(timestamp, insertionIdx);
 
  63         timestamps.add(t);
  64         return true;
  65     }
  66 
 
  67     @Override
  68     public void add(int index, T element) {
  69         throw new UnsupportedOperationException(
 
  70                 "can't add element at arbitrary index, must use add to keep sorted order");
  71     }
  72 
  73     @Override
 
  74     public boolean addAll(Collection<? extends T> c) {
  75         for (T e : c) {
 
  76             add(e);
  77         }
  78         return c.size() > 0;
  79     }
 
  80 
  81     @Override
  82     public boolean addAll(int index, Collection<? extends T> c) {
 
  83         throw new UnsupportedOperationException(
  84                 "can't add element at arbitrary index, must use addAll to keep sorted order");
  85     }
 
  86 
  87     @Override
  88     public void clear() {
  89         list.clear();
 
  90     }
  91 
  92     @SuppressWarnings("unchecked")
  93     @Override
 
  94     public boolean contains(Object e) {
  95         if (e == null) {
  96             return false;
 
  97         }
  98         try {
  99             return Collections.binarySearch(this, (T) e, comparator) >= 0;
 
 100         } catch (ClassCastException ex) {
 101             LOGGER.error("Unexpected type for contains "
 102                     + e.getClass().getName() + ": " + e);
 
 103             return false;
 104         }
 105     }
 106 
 107     @Override
 
 108     public boolean containsAll(Collection<?> c) {
 109         for (Object e : c) {
 110             if (!contains(e)) {
 
 111                 return false;
 112             }
 113         }
 114         return true;
 
 115     }
 116 
 117     @Override
 118     public T get(int index) {
 119         Pair<Long, T> e = list.get(index);
 
 120         return e != null ? e.getSecond() : null;
 121     }
 122 
 123     public T get(Object e) {
 
 124         int ndx = indexOf(e);
 125         if (ndx >= 0) {
 126             return get(ndx);
 127         }
 
 128         return null;
 129     }
 130 
 131     @SuppressWarnings("unchecked")
 132     @Override
 
 133     public int indexOf(Object e) {
 134         try {
 135             return Collections.binarySearch(this, (T) e, comparator);
 
 136         } catch (ClassCastException ex) {
 137             LOGGER.error("Unexpected type for get " + e.getClass().getName()
 138                     + ": " + e);
 
 139             return -1;
 140         }
 141     }
 142 
 143     @Override
 144     public boolean isEmpty() {
 
 145         return list.isEmpty();
 146     }
 147 
 148     @Override
 149     public Iterator<T> iterator() {
 
 150         final Iterator<Pair<Long, T>> it = list.iterator();
 151         return new Iterator<T>() {
 
 152 
 153             @Override
 154             public boolean hasNext() {
 155                 return it.hasNext();
 
 156             }
 157 
 158             @Override
 159             public T next() {
 160                 Pair<Long, T> e = it.next();
 
 161                 return e.getSecond();
 162             }
 163 
 164             @Override
 165             public void remove() {
 
 166                 it.remove();
 167             }
 168         };
 169     }
 170 
 171     @Override
 
 172     public int lastIndexOf(Object o) {
 173         for (int i = list.size() - 1; i >= 0; i--) {
 174             T e = get(i);
 
 175             if (e.equals(o)) {
 176                 return i;
 177             }
 178         }
 179         return -1;
 
 180     }
 181 
 182     @Override
 183     public ListIterator<T> listIterator() {
 184         final ListIterator<Pair<Long, T>> it = list.listIterator();
 
 185         return buildListIterator(it);
 186     }
 187 
 188     @Override
 189     public ListIterator<T> listIterator(int index) {
 
 190         final ListIterator<Pair<Long, T>> it = list.listIterator(index);
 191         return buildListIterator(it);
 192     }
 
 193 
 194     @SuppressWarnings("unchecked")
 195     @Override
 196     public boolean remove(Object e) {
 
 197         try {
 198             int ndx = Collections.binarySearch(this, (T) e, comparator);
 199             if (ndx >= 0) {
 
 200                 remove(ndx);
 201                 return true;
 202             } else {
 203                 return false;
 
 204             }
 205 
 206         } catch (ClassCastException ex) {
 207             LOGGER.error("Unexpected type for remove " + e.getClass().getName()
 
 208                     + ": " + e);
 209             return false;
 210         }
 211     }
 
 212 
 213     @Override
 214     public T remove(int index) {
 215         Pair<Long, T> e = list.remove(index);
 
 216         Pair<Long, Integer> t = new Pair<Long, Integer>(e.getFirst(), 0);
 217 
 218         int insertionIdx = Collections.binarySearch(timestamps, t, CMP);
 
 219         if (insertionIdx >= 0) {
 220             timestamps.remove(insertionIdx);
 221         }
 222         return e != null ? e.getSecond() : null;
 
 223     }
 224 
 225     @Override
 226     public boolean removeAll(Collection<?> c) {
 
 227         boolean all = true;
 228         for (Object e : c) {
 229             all = all && remove(e);
 
 230         }
 231         return all;
 232     }
 233 
 234     @Override
 235     public boolean retainAll(Collection<?> c) {
 
 236         boolean changed = false;
 237         Iterator<?> it = c.iterator();
 238         while (it.hasNext()) {
 
 239             Object e = it.next();
 240             if (!contains(e)) {
 241                 it.remove();
 242                 changed = true;
 243             }
 
 244         }
 245         return changed;
 246     }
 247 
 248     @Override
 
 249     public T set(int index, T element) {
 250         throw new UnsupportedOperationException();
 251     }
 
 252 
 253     @Override
 254     public int size() {
 255         return list.size();
 
 256     }
 257 
 258     @Override
 259     public List<T> subList(int fromIndex, int toIndex) {
 
 260         List<T> tlist = new ArrayList<T>();
 261         List<Pair<Long, T>> plist = list.subList(fromIndex, toIndex);
 
 262         for (Pair<Long, T> e : plist) {
 263             tlist.add(e.getSecond());
 264         }
 265         return tlist;
 
 266     }
 267 
 268     @Override
 269     public Object[] toArray() {
 270         return subList(0, list.size()).toArray();
 
 271     }
 272 
 273     @SuppressWarnings("hiding")
 274     @Override
 275     public <T> T[] toArray(T[] a) {
 
 276         return subList(0, list.size()).toArray(a);
 277     }
 278 
 279     @Override
 280     public String toString() {
 
 281         StringBuilder sb = new StringBuilder();
 282         Iterator<T> it = iterator();
 283         while (it.hasNext()) {
 
 284             sb.append(it.next() + ", ");
 285         }
 286         return sb.toString();
 287     }
 288 
 
 289     private void removeOldest() {
 290         timestamps.remove(timestamps.size() - 1);
 291     }
 292 
 293     private ListIterator<T> buildListIterator(
 
 294             final ListIterator<Pair<Long, T>> it) {
 295         return new ListIterator<T>() {
 
 296 
 297             @Override
 298             public void add(T e) {
 299                 it.add(new Pair<Long, T>(System.nanoTime(), e));
 
 300             }
 301 
 302             @Override
 303             public boolean hasNext() {
 304                 return it.hasNext();
 
 305 
 306             }
 307 
 308             @Override
 309             public boolean hasPrevious() {
 
 310                 return it.hasPrevious();
 311 
 312             }
 313 
 314             @Override
 315             public T next() {
 
 316                 Pair<Long, T> e = it.next();
 317                 return e.getSecond();
 318             }
 319 
 320             @Override
 
 321             public int nextIndex() {
 322                 return it.nextIndex();
 323 
 324             }
 
 325 
 326             @Override
 327             public T previous() {
 328                 Pair<Long, T> e = it.previous();
 
 329                 return e.getSecond();
 330             }
 331 
 332             @Override
 333             public int previousIndex() {
 
 334                 return it.previousIndex();
 335 
 336             }
 337 
 338             @Override
 339             public void remove() {
 
 340                 it.remove();
 341 
 342             }
 343 
 344             @Override
 345             public void set(T e) {
 
 346                 it.set(new Pair<Long, T>(System.nanoTime(), e));
 347 
 348             }
 349         };
 350     }
 
 351 
 352 }
 353 
 354 
 

Listing of LRUSortedList.java

   1 package com.plexobject.jmx.impl;
 
   2 
   3 import java.beans.PropertyChangeListener;
   4 import java.beans.PropertyChangeSupport;
   5 import java.util.Map;
 
   6 import java.util.concurrent.ConcurrentHashMap;
   7 import java.util.concurrent.atomic.AtomicLong;
   8 
   9 import javax.management.AttributeChangeNotification;
 
  10 import javax.management.MBeanNotificationInfo;
  11 import javax.management.Notification;
  12 import javax.management.NotificationBroadcasterSupport;
 
  13 import javax.management.NotificationListener;
  14 
  15 import org.apache.commons.lang.builder.EqualsBuilder;
  16 import org.apache.commons.lang.builder.HashCodeBuilder;
 
  17 import org.apache.commons.lang.builder.ToStringBuilder;
  18 import org.apache.log4j.Logger;
  19 
  20 import com.plexobject.jmx.ServiceJMXBean;
 
  21 import com.plexobject.metrics.Metric;
  22 import com.plexobject.util.TimeUtils;
  23 
  24 public class ServiceJMXBeanImpl extends NotificationBroadcasterSupport
 
  25         implements ServiceJMXBean, NotificationListener {
  26     private static final Logger LOGGER = Logger
  27             .getLogger(ServiceJMXBeanImpl.class);
 
  28     private Map<String, String> properties = new ConcurrentHashMap<String, String>();
  29     private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
 
  30 
  31     private final String serviceName;
  32     private AtomicLong totalErrors;
 
  33     private AtomicLong totalRequests;
  34 
  35     private AtomicLong sequenceNumber;
  36     private String state;
 
  37 
  38     public ServiceJMXBeanImpl(final String serviceName) {
  39         this.serviceName = serviceName;
 
  40         this.totalErrors = new AtomicLong();
  41         this.totalRequests = new AtomicLong();
  42         this.sequenceNumber = new AtomicLong();
 
  43     }
  44 
  45     @Override
  46     public double getAverageElapsedTimeInNanoSecs() {
 
  47         return Metric.getMetric(getServiceName())
  48                 .getAverageDurationInNanoSecs();
  49     }
  50 
 
  51     public String getProperty(final String name) {
  52         return properties.get(name);
  53     }
 
  54 
  55     public void setProperty(final String name, final String value) {
 
  56         final String oldValue = properties.put(name, value);
  57         final Notification notification = new AttributeChangeNotification(this,
 
  58                 sequenceNumber.incrementAndGet(), TimeUtils
  59                         .getCurrentTimeMillis(), name + " changed", name,
  60                 "String", oldValue, value);
  61         sendNotification(notification);
 
  62         handleNotification(notification, null);
  63     }
  64 
  65     @Override
 
  66     public String getServiceName() {
  67         return serviceName;
  68     }
  69 
 
  70     @Override
  71     public long getTotalDurationInNanoSecs() {
  72         return Metric.getMetric(getServiceName()).getTotalDurationInNanoSecs();
 
  73     }
  74 
  75     @Override
  76     public long getTotalErrors() {
 
  77         return totalErrors.get();
  78     }
  79 
  80     public void incrementError() {
 
  81         final long oldErrors = totalErrors.getAndIncrement();
  82         final Notification notification = new AttributeChangeNotification(this,
 
  83                 sequenceNumber.incrementAndGet(), TimeUtils
  84                         .getCurrentTimeMillis(), "Errors changed", "Errors",
  85                 "long", oldErrors, oldErrors + 1);
 
  86         sendNotification(notification);
  87     }
  88 
  89     @Override
  90     public long getTotalRequests() {
 
  91         return totalRequests.get();
  92     }
  93 
  94     public void incrementRequests() {
 
  95         final long oldRequests = totalRequests.getAndIncrement();
  96         final Notification notification = new AttributeChangeNotification(this,
 
  97                 sequenceNumber.incrementAndGet(), TimeUtils
  98                         .getCurrentTimeMillis(), "Requests changed",
  99                 "Requests", "long", oldRequests, oldRequests + 1);
 
 100         sendNotification(notification);
 101     }
 102 
 103     @Override
 104     public MBeanNotificationInfo[] getNotificationInfo() {
 105         String[] types = new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE };
 
 106         String name = AttributeChangeNotification.class.getName();
 107         String description = "An attribute of this MBean has changed";
 108         MBeanNotificationInfo info = new MBeanNotificationInfo(types, name,
 
 109                 description);
 110 
 111         return new MBeanNotificationInfo[] { info };
 112     }
 113 
 
 114     @Override
 115     public String getState() {
 116         return state;
 117     }
 118 
 
 119     /**
 120      * @param state
 121      *            the state to set
 
 122      */
 123     public void setState(String state) {
 124         this.state = state;
 125     }
 
 126 
 127     /**
 128      * @see java.lang.Object#equals(Object)
 
 129      */
 130     @Override
 131     public boolean equals(Object object) {
 132         if (!(object instanceof ServiceJMXBeanImpl)) {
 
 133             return false;
 134         }
 135         ServiceJMXBeanImpl rhs = (ServiceJMXBeanImpl) object;
 136         return new EqualsBuilder().append(this.serviceName, rhs.serviceName)
 
 137                 .isEquals();
 138     }
 139 
 140     /**
 141      * @see java.lang.Object#hashCode()
 
 142      */
 143     @Override
 144     public int hashCode() {
 145         return new HashCodeBuilder(786529047, 1924536713).append(
 
 146                 this.serviceName).toHashCode();
 147     }
 148 
 149     /**
 150      * @see java.lang.Object#toString()
 
 151      */
 152     @Override
 153     public String toString() {
 154         return new ToStringBuilder(this)
 
 155                 .append("serviceName", this.serviceName).append("totalErrors",
 156                         this.totalErrors).append("totalRequests",
 157                         this.totalRequests).append("totalRequests",
 
 158                         this.totalRequests).append("state", this.state).append(
 159                         "properties", this.properties).toString();
 160     }
 
 161 
 162     public void addPropertyChangeListener(PropertyChangeListener pcl) {
 163         pcs.addPropertyChangeListener(pcl);
 164     }
 165 
 
 166     public void removePropertyChangeListener(PropertyChangeListener pcl) {
 167         pcs.removePropertyChangeListener(pcl);
 168 
 169     }
 170 
 
 171     @Override
 172     public void handleNotification(Notification notification, Object handback) {
 173         LOGGER.info("Received notification: ClassName: "
 174                 + notification.getClass().getName() + ", Source: "
 
 175                 + notification.getSource() + ", Type: "
 176                 + notification.getType() + ", tMessage: "
 177                 + notification.getMessage());
 178         if (notification instanceof AttributeChangeNotification) {
 
 179             AttributeChangeNotification acn = (AttributeChangeNotification) notification;
 180             pcs.firePropertyChange(acn.getAttributeName(), acn.getOldValue(),
 181                     acn.getNewValue());
 182 
 183         }
 184     }
 
 185 }
 186 
 187 
 

Testing

Finally, here is how you can test this filter:

  1 package com.plexobject;
 
  2 
  3 import java.net.InetAddress;
  4 import java.util.Date;
  5 
 
  6 import org.apache.log4j.Logger;
  7 import org.apache.log4j.PatternLayout;
  8 import org.apache.log4j.net.SMTPAppender;
 
  9 
 10 import com.plexobject.log.FilteredSMTPAppender;
 11 
 12 public class Main {
 
 13     private static final Logger LOGGER = Logger.getLogger(Main.class);
 14     public static void main(String[] args) {
 
 15         SMTPAppender appender = new FilteredSMTPAppender();
 16         try {
 17             appender.setTo("bhatti@xxx.com");
 18             appender.setFrom("bhatti@xxx.com");
 
 19             appender.setSMTPHost("smtp.xxx.net");
 20             appender.setLocationInfo(true);
 21             appender.setSubject("Error from " + InetAddress.getLocalHost());
 22 
 
 23             appender.setLayout(new PatternLayout());
 24             appender.activateOptions();
 25             LOGGER.addAppender(appender);
 26         } catch (Exception e) {
 
 27             LOGGER.error("Failed to register smtp appender", e);
 28         }
 29         while (true) {
 30             try {
 
 31                 throw new Exception("throwing exception at " + new Date());
 32             } catch (Exception e) {
 
 33                 LOGGER.error("Logging error at " + new Date(), e);
 34             }
 35             try {
 
 36                 Thread.sleep(1000);
 37             } catch (InterruptedException e) {
 38                 Thread.interrupted();
 39             }
 40         }
 
 41     }
 42 }
 43 
 44 
 

Above code simulates error generation every second, but it sends email based on the throttling level defined in the configuration. Obviously you can use log4j properties file to define all this configuration, e.g.

<!– Send email when error happens –>
<appender name=”APP-EMAIL” class=”com.plexobject.log.FilteredSMTPAppender”>
<param name=”BufferSize” value=”256″ />
<param name=”SMTPHost” value=”smtp.xxx.net” />
<param name=”From” value=”bhatti@xxx.com” />
<param name=”To” value=”bhatti@xxx.com” />
<param name=”Subject” value=”Production Error” />
<layout class=”org.apache.log4j.PatternLayout”>
<param name=”ConversionPattern”
value=”[%d{ISO8601}]%n%n%-5p%n%n%c%n%n%m%n%n” />
</layout>

<filter class=”org.apache.log4j.varia.StringMatchFilter”>
<param name=”StringToMatch” value=”My Error”/>
<param name=”AcceptOnMatch” value=”false” />
</filter>
</appender>

Summary

I am skipping other classes, but you can download entire code from FilteredSMTPAppender.zip. This solution seems to be working from me but feel free to share your experience with similar problems.

February 3, 2010

A few recipes for reprocessing messages in Dead-Letter-Queue using ActiveMQ

Filed under: Computing — admin @ 2:42 pm

Messaging based asynchronous processing is a key component of any complexed software especially in transactional environment. There are a number of solutions that provide high performance and reliable messaging in Java space such as ActiveMQ, FUSE broker, JBossMQ, SonicMQ, Weblogic, Websphere, Fiorano, etc. These providers support JMS specification, which provides abstraction for queues, message providers and message consumers. In this blog, I will go over some recipes for recovering messages from dead letter queue when using ActiveMQ.

What is Dead Letter Queue

Generally, when a consumer fails to process a message within a transaction or does not send acknowledgement back to the broker, the message is put back to the queue. The message is then delivered upto certain number of times based on configuration and finally the message is put to dead letter queue when that limit is exceeded. The ActiveMQ documentation recommends following settings for defining dead letter queues:

<broker...>
	<destinationPolicy>
		<policyMap>
			<policyEntries>
				<!-- Set the following policy on all queues using the '>' wildcard -->
				<policyEntry queue=">">
					<deadLetterStrategy>
						<individualDeadLetterStrategy queuePrefix="DLQ." useQueueForQueueMessages="true" />
					</deadLetterStrategy>
				</policyEntry>
			</policyEntries>
		</policyMap>
	</destinationPolicy> ... 
</broker>

and you can control redelivery policy as follows:

RedeliveryPolicy policy = connection.getRedeliveryPolicy();
policy.setInitialRedeliveryDelay(500);
policy.setBackOffMultiplier(2);
policy.setUseExponentialBackOff(true);

policy.setMaximumRedeliveries(2);
It is important that you create dlq per queue, otherwise ActiveMQ puts them into a single dead letter queue.

Handle QueueViewMBean

ActiveMQ provides QueueViewMBean to invoke administration APIs on the queues. The easiest way to get this handle is to use BrokerFacadeSupport class, which is extended by RemoteJMXBrokerFacade and LocalBrokerFacade. You can use RemoteJMXBrokerFacade if you are connecting to remote ActiveMQ server, e.g. here is Spring configuration for setting it up:

<bean id="brokerQuery" class="org.apache.activemq.web.RemoteJMXBrokerFacade" autowire="constructor" destroy-method="shutdown">
	<property name="configuration">
		<bean class="org.apache.activemq.web.config.SystemPropertiesConfiguration"/>
	</property>
	<property name="brokerName">
		<null/>
	</property>
</bean>

Alternatively, you can use LocalBrokerFacade if you are running embedded ActiveMQ server, e.g. below is Spring configuration for it:

<bean id="brokerQuery" class="org.apache.activemq.web.LocalBrokerFacade" autowire="constructor" scope="prototype"/>

Getting number of messages from the queue

Once you got handle to QueueViewMBean, you can use following API to find the number of messages in the queue:

public long getQueueSize(final String dest) {
  try {
    return brokerQuery.getQueue(dest).getQueueSize();
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

Copying Messages using JMS APIs

The JMS specification provides APIs to browse queue in read mode and then you can send the messages to another queue, e.g.

import org.apache.activemq.web.BrokerFacadeSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

import javax.jms.*;
import java.util.Enumeration;

public class DlqReprocessor {
    @Autowired
    private JmsTemplate jmsTemplate;
    @Autowired
    BrokerFacadeSupport brokerQuery;
    @Autowired
    ConnectionFactory connectionFactory;

    @SuppressWarnings("unchecked")
    void redeliverDLQUsingJms(
            final String brokerName,
            final String from,
            final String to) {
        Connection connection = null;
        Session session = null;
        try {
            connection = connectionFactory.createConnection();
            connection.start();
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            Queue dlq = session.createQueue(from);
            QueueBrowser browser = session.createBrowser(dlq);
            Enumeration<Message> e = browser.getEnumeration();
            while (e.hasMoreElements()) {
                Message message = e.nextElement();
                final String messageBody = ((TextMessage) message).getText();
                jmsTemplate.send(to, new MessageCreator() {
                    @Override
                    public Message createMessage(final Session session) throws JMSException {
                        return session.createTextMessage(messageBody);
                    }
                })
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        } finally {
            try {
                session.close();
            } catch (Exception ex) {
            }
            try {
                connection.close();
            } catch (Exception ex) {
            }
        }
    }
}
The downside of above approach is that it leaves the original messages in the dead letter queue.

Copying Messages using Spring’s JmsTemplate APIs

You can effectively do the same thing with JmsTemplate provided by Spring with a bit less code, e.g.

    void redeliverDLQUsingJmsTemplateBrowse(
            final String from,
            final String to) {
        try {
            jmsTemplate.browse(from, new BrowserCallback() {
                @SuppressWarnings("unchecked")
                @Override
                public Object doInJms(Session session, QueueBrowser browser) throws JMSException {
                    Enumeration<Message> e = browser.getEnumeration();
                    while (e.hasMoreElements()) {
                        Message message = e.nextElement();
                        final String messageBody = ((TextMessage) message).getText();
                        jmsTemplate.send(to, new MessageCreator() {
                            @Override
                            public Message createMessage(final Session session) throws JMSException {
                                return session.createTextMessage(messageBody);
                            }
                        });
                    }
                    return null;
                }
            });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

Moving Messages using receive/send APIs

As I mentioned, the above approaches leave messages in the DLQ, which may not be what you want. Thus, another simple approach would be to consume messages from the dead letter queue and send it to another,e.g.

    public void redeliverDLQUsingJmsTemplateReceive(
            final String from,
            final String to) {
        try {
            jmsTemplate.setReceiveTimeout(100);
            Message message = null;
            while ((message = jmsTemplate.receive(from)) != null) {
                final String messageBody = ((TextMessage) message).getText();
                jmsTemplate.send(to, new MessageCreator() {
                    @Override
                    public Message createMessage(final Session session)
                            throws JMSException {
                        return session.createTextMessage(messageBody);
                    }
                });
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
  

Moving Messages using ActiveMQ’s API

Finally, the best approach I found waas to use ActiveMQ’s APIs to move messags, e.g.

    public void redeliverDLQUsingJMX(
            final String brokerName, final String from,
            final String to) {
        try {
            final QueueViewMBean queue = brokerQuery.getQueue(from);
            for (int i = 0; i < 10 && queue.getQueueSize() > 0; i++) {
                CompositeData[] compdatalist = queue.browse();
                for (CompositeData cdata : compdatalist) {
                    String messageID = (String) cdata.get("JMSMessageID");
                    queue.moveMessageTo(messageID, to);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

I have been using this approach and have found to be reliable for reprocessing dead letter queue, though these techniques an also be used for general queues. I am sure there are tons of alternatives including using full-fledged enterprise service bus route. Let me know if you have interesting solutions to this problem.

January 20, 2010

PlexRBAC: an open source project for providing powerful role based security (II)

Filed under: Computing — admin @ 1:50 pm

This is continuation of my previous blog on my open source project PlexRBAC for managing role based access control. Last time, I covered REST APIs and in this blog I will cover internal domain model, RBAC APIs in Java and examples of instance or dynamic based security.

Layers

PlexRBAC consists of following layers

Business Domain Layer

This layer defines core classes that are part of the RBAC based security domain such as:

  • Domain – As described previously, the domain allows you to support multiple applications or realms.
  • Subject – The subject represents users who are defined in an application.
  • Role – A role represents job title or function.
  • Permission – A permission is composed of operation, target and an expression that is used for dynamic or instance based security.
  • SecurityError – Upon a permission failure, you can choose to store them in the database using SecurityError.

Repository Layer

This layer is responsible for accessing or storing above objects in the database. PlexRBAC uses Berkley DB for persistence and each domain is stored as a separate database, which allows you to segregate permissions and roles for distinct domains. Following are list of repositories supported by PlexRBAC:

  • DomainRepository – provides database access for Domains.
  • PermissionRepository – provides database access for Permissions.
  • SubjectRepository – provides database access for Subjects.
  • SecurityErrorRepository – provides database access for SecurityErrors.
  • RoleRepository – provides database access for Roles.
  • SecurityMappingRepository – provides APIs to map permissions with roles and to map subject with roles.
  • RepositoryFactory – provides factory methods to create above repositories.

Security Layer

This class defines PermissionManager for authorizing permissions.

Evaluation Layer

This layer proivdes evaluation engine for instance based security.

Service Layer

This layer defines REST services such as:

  • DomainService – this service provides REST APIs for accessing Domains.
  • PermissionService – this service provides REST APIs for accessing Permissions.
  • SubjectService – this service provides REST APIs for accessing Subjects.
  • RoleService – this service provides REST APIs for accessing Roles.
  • AuthenticationService – this service provides REST APIs for authenticating users.
  • AuthorizationService – this service provides REST APIs for authorizing permissions.
  • RolePermissionService – this service provides REST APIs for mapping permissions with roles.
  • SubjectRolesService – this service provides REST APIs for mapping subjects with roles.

JMX Layer

This layer defines JMX helper classes for managing services and configuration remotely.

Caching Layer

This layer provides caching security permissions to improve performance.

Metrics Layer

This layer provides performance measurement classes such as Timing class to measure method invocation benchmarks.

Utility Layer

This layer provides helper classes.

Web Layer

This layer provides filters for enforcing authentication and authorization when accessing REST APIs.

Example

Let’s use the same example that we described last time but with addition of instance based security. Let’s assume there are five roles: Teller, Customer-Service-Representative (CSR), Account, AccountingManager and LoanOfficer, where

  • A teller can modify customer deposit accounts — but only if customer and teller live in same region
  • A customer service representative can create or delete customer deposit accounts — but only if customer and teller live in same region
  • An accountant can create general ledger reports — but only if year is == current year
  • An accounting manager can modify ledger-posting rules — but only if year is == current year
  • A loan officer can create and modify loan accounts – but only if account balance is < 10000

In addition, following classes will be used to add domain specific security:

  1 
  2 class User {
 
  3 
  4     private String id;
  5     private String region;
  6 
 
  7     User() {
  8     }
  9 
 10     public User(String id, String region) {
 11         this.id = id;
 
 12         this.region = region;
 13     }
 14 
 15     public void setRegion(String region) {
 16         this.region = region;
 
 17     }
 18 
 19     public String getRegion() {
 20         return region;
 21     }
 
 22 
 23     public void setId(String id) {
 24         this.id = id;
 25     }
 26 
 
 27     public String getId() {
 28         return id;
 29     }
 30 }
 31 
 
 32 class Customer extends User {
 33 
 34     public Customer(String id, String region) {
 35         super(id, region);
 
 36     }
 37 }
 38 
 39 class Employee extends User {
 40 
 
 41     public Employee(String id, String region) {
 42         super(id, region);
 43     }
 44 }
 45 
 
 46 class Account {
 47 
 48     private String id;
 49     private double balance;
 
 50 
 51     Account() {
 52     }
 53 
 54     public Account(String id, double balance) {
 
 55         this.id = id;
 56         this.balance = balance;
 57     }
 58 
 59     /**
 
 60      * @return the id
 61      */
 62     public String getId() {
 
 63         return id;
 64     }
 65 
 66     /**
 67      * @param id
 
 68      *            the id to set
 69      */
 70     public void setId(String id) {
 
 71         this.id = id;
 72     }
 73 
 74     public void setBalance(double balance) {
 
 75         this.balance = balance;
 76     }
 77 
 78     public double getBalance() {
 79         return balance;
 
 80     }
 81 }
 82 
 83 
 

Bootstrapping

Let’s create handle to repository-factory as:

 1 
 2     private static final String TEST_DB_DIR = "test_db_dir_perms";
 
 3     RepositoryFactory repositoryFactory = new RepositoryFactoryImpl(TEST_DB_DIR);
 

And instance of permission manager as:

 1 PermissionManager permissionManager = new PermissionManagerImpl(repositoryFactory,
 
 2             new JavascriptEvaluator());
 

Creating a domain

Now, let’s create a domain for banking:

 1     private static final String BANKING = "banking";
 
 2     repositoryFactory.getDomainRepository().save(new Domain(BANKING, ""));
 

Creating Users

Next step is to create users for the domain or application so let’s define accounts for tom, cassy, ali, mike and larry, i.e.,

 1         final SubjectRepository subjectRepo = repositoryFactory
 
 2                 .getSubjectRepository(BANKING);
 3         Subject tom = subjectRepo.save(new Subject("tom", "pass"));
 4         Subject cassy = subjectRepo.save(new Subject("cassy", "pass"));
 
 5         Subject ali = subjectRepo.save(new Subject("ali", "pass"));
 6         Subject mike = subjectRepo.save(new Subject("mike", "pass"));
 
 7         Subject larry = subjectRepo.save(new Subject("larry", "pass"));
 8 
 

Creating Roles

Now, we will create roles for Teller, CSR, Accountant, AccountManager and LoanManager:

  1         final RoleRepository roleRepo = repositoryFactory
 
  2                 .getRoleRepository(BANKING);
  3         Role employee = roleRepo.save(new Role("Employee"));
  4         Role teller = roleRepo.save(new Role("Teller", employee));
 
  5         Role csr = roleRepo.save(new Role("CSR", teller));
  6         Role accountant = roleRepo.save(new Role("Accountant", employee));
 
  7         Role accountantMgr = roleRepo.save(new Role("AccountingManager",
  8                 accountant));
  9         Role loanOfficer = roleRepo
 
 10                 .save(new Role("LoanOfficer", accountantMgr));
 11 
 

Creating Permissions

We can then create new permissions and save them in the database as follows:

  1         final PermissionRepository permRepo = repositoryFactory
 
  2                 .getPermissionRepository(BANKING);
  3         Permission cdDeposit = permRepo.save(new Permission("(create|delete)",
  4                 "DepositAccount",
 
  5                 "employee.getRegion().equals(customer.getRegion())")); // 1
  6         Permission ruDeposit = permRepo.save(new Permission("(read|modify)",
  7                 "DepositAccount",
 
  8                 "employee.getRegion().equals(customer.getRegion())")); // 2
  9         Permission cdLoan = permRepo.save(new Permission("(create|delete)",
 10                 "LoanAccount", "account.getBalance() < 10000")); // 3
 
 11         Permission ruLoan = permRepo.save(new Permission("(read|modify)",
 12                 "LoanAccount", "account.getBalance() < 10000")); // 4
 
 13 
 14         Permission rdLedger = permRepo.save(new Permission("(read|create)",
 15                 "GeneralLedger", "year == new Date().getFullYear()")); // 5
 
 16 
 17         Permission rGlpr = permRepo
 18                 .save(new Permission("read", "GeneralLedgerPostingRules",
 19                         "year == new Date().getFullYear()")); // 6
 
 20 
 21         Permission cmdGlpr = permRepo.save(new Permission(
 22                 "(create|modify|delete)", "GeneralLedgerPostingRules",
 23                 "year == new Date().getFullYear()")); // 7
 
 24 
 

Mapping Subjects/Permissions to Roles

Now we will map subjects to roles as follows:

 1         final SecurityMappingRepository smr = repositoryFactory
 
 2                 .getSecurityMappingRepository(BANKING);
 3 
 4         // Mapping Users to Roles
 5         smr.addRolesToSubject(tom, teller);
 6         smr.addRolesToSubject(cassy, csr);
 7         smr.addRolesToSubject(ali, accountant);
 
 8         smr.addRolesToSubject(mike, accountantMgr);
 9         smr.addRolesToSubject(larry, loanOfficer);
 0 
 

Then we will map permissions to roles as follows:

 1         smr.addPermissionsToRole(teller, ruDeposit);
 2         smr.addPermissionsToRole(csr, cdDeposit);
 
 3         smr.addPermissionsToRole(accountant, rdLedger);
 4         smr.addPermissionsToRole(accountant, ruLoan);
 5         smr.addPermissionsToRole(accountantMgr, cdLoan);
 6         smr.addPermissionsToRole(accountantMgr, rGlpr);
 7         smr.addPermissionsToRole(loanOfficer, cmdGlpr);
 8 
 
 

Authorization

Now the fun part of authorization, let’s check if user “tom” can view deposit-accounts, e.g.

  1    public static Map<String, Object> toMap(final Object... keyValues) {
 
  2         Map<String, Object> map = new HashMap<String, Object>();
  3         for (int i = 0; i < keyValues.length - 1; i += 2) {
 
  4             map.put(keyValues[i].toString(), keyValues[i + 1]);
  5         }
  6         return map;
  7     }
 
  8     @Test
  9     public void testReadDepositByTeller() {
 10         initDatabase();
 11         permissionManager.check(new PermissionRequest(BANKING, "tom", "read",
 
 12                 "DepositAccount", toMap("employee", new Employee("tom",
 13                         "west"), "customer", new Customer("zak", "west"))));
 
 14     }
 15 
 16 
 

Note that above test method builds a PermissionRequest that encapsulates domain, subject, operation, target and context and then calls check method of SecurityManager, which throws SecurityException if permission fails.

Then we check if tom, the teller can delete deposit-account, e.g.

 1     @Test(expected = SecurityException.class)
 
 2     public void testDeleteByTeller() {
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "tom", "delete",
 
 5                 "DepositAccount", toMap("employee", new Employee("tom",
 6                         "west"), "customer", new Customer("zak", "west"))));
 
 7     }
 8 
 

Which would throw security exception.

Now let’s check if cassy, the CSR can delete deposit-account, e.g.

 1     @Test
 2     public void testDeleteByCsr() {
 
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "cassy",
 5                 "delete", "DepositAccount", toMap("employee",
 
 6                         new Employee("cassy", "west"), "customer",
 7                         new Customer("zak", "west"))));
 
 0 
 

Which works as CSR have permissions for deleting deposit-account. Now, let’s check if ali, the accountant can view general-ledger, e.g.

 1    @Test
 2     public void testReadLedgerByAccountant() {
 
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "ali", "read",
 5                 "GeneralLedger", toMap("year", 2010, "account",
 
 6                         new Account("zak", 500))));
 7     }
 8 
 9 
 

Which works as expected. Next we check if ali can delete general-ledger:

 1     @Test(expected = SecurityException.class)
 
 2     public void testDeleteLedgerByAccountant() {
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "ali", "delete",
 
 5                 "GeneralLedger", toMap("year", 2010, "account",
 6                         new Account("zak", 500))));
 7     }
 
 8 
 9 
 

Which would fail as only account-manager can delete. Next we check if mike, the account-manager can create general-ledger, e.g.

 1     @Test
 2     public void testCreateLedgerByAccountantManager() {
 
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "mike",
 5                 "create", "GeneralLedger", toMap("year", 2010,
 
 6                         "account", new Account("zak", 500))));
 7     }
 8 
 

Which works as expected. Now we check if mike can create posting-rules of general-ledger, e.g.

 1     @Test(expected = SecurityException.class)
 
 2     public void testPostLedgingRulesByAccountantManager() {
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "mike",
 
 5                 "create", "GeneralLedgerPostingRules", toMap("year",
 6                         2010, "account", new Account("zak", 500))));
 
 7     }
 8 
 

Which fails authorization. Then we check if larry, the loan officer can create posting-rules of general-ledger, e.g.

 1     @Test
 2     public void testPostLedgingRulesByLoanManager() {
 
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "larry",
 5                 "create", "GeneralLedgerPostingRules", toMap("year",
 
 6                         2010, "account", new Account("zak", 500))));
 7     }
 8 
 

Which works as expected. Now, let’s check the same permission but with different year, e.g.

 1     @Test(expected = SecurityException.class)
 
 2     public void testPostLedgingRulesByLoanManagerWithExceededAmount() {
 3         initDatabase();
 4         permissionManager.check(new PermissionRequest(BANKING, "larry",
 
 5                 "create", "GeneralLedgerPostingRules", IDUtils.toMap("year",
 6                         2011)));
 7     }
 8 
 

Which fails as year doesn’t match.

Summary

Above examples demonstrate how PlexRBAC API can be used along with instance or dynamic based security. In next post, I will describe caching and how PlexRBAC can be integrated with J2EE and Spring security.

January 10, 2010

PlexRBAC: an open source project for providing powerful role based security (I)

Filed under: Computing — admin @ 7:45 pm

Overview

In my last blog I described core pieces of a security system and mentioned a new open source project PlexRBAC I recently started to provide Role Based Security both as a REST service and Java library. In this post, I will go over the some of the features that are now available. This project is based on my experience with a number of home built solutions for RBAC and standard J2EE solutions. However, a key differentiator is that it adds instance based security or context based security that adds dynamic access control. The role based security consists of following components:

Domain

Though, domain is strictly not part of role based security but RBAC provides segregation of security policies by domains, where a domain can represent a security realm or an application.

Subject

The subject represents users who are defined in an application.

Role

A role represents job title or function. A subject or user belongs to one or more roles. One of key feature of PlexRBAC is that roles support inheritance where a role can have one or more roles. This helps define security policies that follow “don’t repeat yourself” or DRY.

Permission

A permission consists of two sub parts: operation and target, where operation is a “verb” that describes action and target represents “object” that is acted upon. All permissions are assigned to roles. In PlexRBAC, permissions also contains an expression which is evaluated to check dynamic security. PlexRBAC allows Javascript based expressions and provides access to runtime request parameters. Finally, PlexRBAC offers regular expressions for both operations and target, so you can define operations like “(read|write|create|delete)” or “read*”, etc.

Following diagram shows the relationship between these components:

Getting Started

PlexRBAC depends on Java 1.6+ and Maven 2.0+. You can download the project using git:

 git clone git@github.com:bhatti/PlexRBAC.git
 

Then you can start the REST based web service within Jetty by typing:

 mvn jetty:run-war
 

The service will listen on port 8080 and you can test it with curl.

Authentication

Though, PlexRBAC is not designed for authentication but it provides Basic authentication and all administration APIs are protected with the authentication. By default, it uses an account “super_admin” with password “changeme”, which you can modify with configurations. Also, as PlexRBAC supports domains to segregates security policies, subjects are also restricted to the domains where they are defined.

REST APIs

Following are APIs defined in PlexRBAC:

Domains

  • GET /api/security/domains – returns list of all domains in JSON format.
  • GET /api/security/domains/{domain-id} – returns details of given domain in JSON format.
  • PUT /api/security/domains/{domain-id} with body of domain details in JSON format.
  • DELETE /api/security/domains – deletes all domains.
  • DELETE /api/security/domains/{domain-id} – deletes domain identified by domain-id.

Subjects

  • GET /api/security/subjects/{domain-id} – returns list of all subjects in domain identified by domain-id in JSON format.
  • GET /api/security/subjects/{domain-id}/{id} – returns details of given subject identified by id in given domain.
  • PUT /api/security/subjects/{domain-id}/{id} with body of subject details in JSON format.
  • DELETE /api/security/subjects/{domain-id} – deletes all subjects in given domain.
  • DELETE /api/security/subjects/{domain-id}/{id} – deletes subject identified by id.

Roles

  • GET /api/security/roles/{domain-id} – returns list of all roles in domain identified by domain-id in JSON format.
  • GET /api/security/roles/{domain-id}/{id} – returns details of given role identified by id in given domain.
  • PUT /api/security/roles/{domain-id}/{id} with body of role details in JSON format.
  • DELETE /api/security/roles/{domain-id} – deletes all roles in given domain.
  • DELETE /api/security/roles/{domain-id}/{id} – deletes role identified by id.

Permissions

  • GET /api/security/permissions/{domain-id} – returns list of all permissions in domain identified by domain-id in JSON format.
  • GET /api/security/permissions/{domain-id}/{id} – returns details of given permission identified by id in given domain.
  • POST /api/security/permissions/{domain-id} with body of permission details in JSON format. Note that this API uses POST instead of PUT as the id will be assigned by the server.
  • DELETE /api/security/permissions/{domain-id} – deletes all permissions in given domain.
  • DELETE /api/security/permissions/{domain-id}/{id} – deletes permission identified by id.

Mapping of Roles and Permissions

  • PUT /api/security/role_perms/{domain-id}/{role-id} – adds permissions identified by permissionIds that stores list of permission-ids in JSON format. Note that permissionIds is passed as a form parameter.
  • DELETE /api/security/role_perms/{domain-id}/{role-id} – removes permissions identified by permissionIds that stores list of permission-ids in JSON format. Note that permissionIds is passed as a form parameter.

Mapping of Subjects and Roles

  • PUT /api/security/subject_roles/{domain-id}/{subject-id} – adds roles identified by rolenames that stores list of role-ids in JSON format. Note that rolenames is passed as a form parameter.
  • DELETE /api/security/subject_roles/{domain-id}/{subject-id} – removes roles identified by rolenames that stores list of role-ids in JSON format. Note that rolenames is passed as a form parameter.

Authorization

  • GET /api/security/authorize/{domain-id} – with query parameter of operation and target.

Example

Let’s start with a banking example where a bank-object can be account, general-ledger-report or ledger-posting-rules and account is further grouped into customer account or loan account, e.g.

Let’s assume there are five roles: Teller, Customer-Service-Representative (CSR), Account, AccountingManager and LoanOfficer, where

  • A teller can modify customer deposit accounts.
  • A customer service representative can create or delete customer deposit accounts.
  • An accountant can create general ledger reports.
  • An accounting manager can modify ledger-posting rules.
  • A loan officer can create and modify loan accounts.

Creating a domain

The first thing is to create a security domain for your application. As we are dealing with banking domain, let’s call our domain “banking”.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/domains/banking" -d '{"id":"banking"}'
 

It will return response:

 {"id":"banking","ownerSubjectNames":"super_admin"}
 

The first thing to note that we are passing user and password using Basic authentication as all accesses to administration APIs require login. Now, you can find out available domains via

 curl -v --user "super_admin:changeme" "http://localhost:8080/api/security/domains"
 

which would return something like:

 [{"id":"banking","ownerSubjectNames":"super_admin"},{"description":"default","id":"default","ownerSubjectNames":"super_admin"}]
 

Creating Users

Next step is to create users for the domain or application so let’s define accounts for tom, cassy, ali, mike and larry, i.e.,

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subjects/banking" -d '{"id":"tom","credentials":"pass"}'
 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subjects/banking" -d '{"id":"cassy","credentials":"pass"}'
 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subjects/banking" -d '{"id":"ali","credentials":"pass"}'
 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subjects/banking" -d '{"id":"mike","credentials":"pass"}'
 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subjects/banking" -d '{"id":"larry","credentials":"pass"}'
 

Note that each user is identified by an id or username and credentials and in above examples usernames or subject-ids are prefixed with domain-ids, e.g. “ddefault:super_admin”.

Creating Roles

As I mentioned, a role represents job title or responsibilities and each role can have one or more parents. By default, PlexRBAC defines an “anonymous” role, which is used for users who are not logged in and all user-defined roles extend “anonymous” role.

First, we create a role for bank employee called “Employee”:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"Employee"}'
 

which returns

 {"id":"Employee","parentIds":["anonymous"]}
 

As you can see the “Employee” role is created with parent of “anonymous”. Next, we create “Teller” role:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"Teller","parentIds":["Employee"]}'
 

which returns:

 {"id":"Teller","parentIds":["Employee"]}
 

Then we create a role for customer-service-representative called “CSR” that is extended by Teller e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"CSR","parentIds":["Teller"]}' 
 

which returns:

 {"id":"CSR","parentIds":["Teller"]}
 

Then we create a role for “Accountant”:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"Accountant","parentIds":["Employee"]}' 
 

which returns:

 {"id":"Accountant","parentIds":["Employee"]}
 

Then we create a role for “AccountingManager”, which is extended by “Accountant”, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"AccountingManager","parentIds":["Accountant"]}' 
 

which returns:

 {"id":"AccountingManager","parentIds":["Accountant"]}
 

Finally, we create a role for “LoanOfficer”, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/roles/banking" -d '{"id":"LoanOfficer","parentIds":["Employee"]}' 
 

which returns:

 {"id":"LoanOfficer","parentIds":["Employee"]}
 

Creating Permissions

As described above, a permission is composed of operation, target and expression, where an operation and target can be any regular expression and expression can be any Javascript expression. However following permissions don’t define any expressions for simplicity. First, we create a permission to create or delete deposit-account, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(create|delete)","target":"DepositAccount","expression":""}' 
 

which returns:

 {"expression":"","id":"1","operation":"(create|delete)","target":"DepositAccount"}
 

Each permission is automatically assigned a unique numeric id. Next, we create a permission to read or modify deposit-account, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(read|modify)","target":"DepositAccount","expression":""}' 
 

which returns:

 {"expression":"","id":"2","operation":"(read|modify)","target":"DepositAccount"}
 

Then, we create a permission to create or delete loan-account

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(create|delete)","target":"LoanAccount","expression":""}' 
 

which returns:

 {"expression":"","id":"3","operation":"(create|delete)","target":"LoanAccount"}
 

Then we create a permission to read or modify loan-account, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(read|modify)","target":"LoanAccount","expression":""}' 
 

which returns:

 {"expression":"","id":"4","operation":"(read|modify)","target":"LoanAccount"}
 

Then we create a role to view and create general-ledger, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(read|create)","target":"GeneralLedger","expression":""}' 
 

which returns:

 {"expression":"","id":"5","operation":"(read|create)","target":"GeneralLedger"}
 

Finally, we create a permission for modifying posting rules of general-ledger, e.g.

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X POST "http://localhost:8080/api/security/permissions/banking" -d '{"operation":"(read|create|modify|delete)","target":"GeneralLedgerPostingRules","expression":""}' 
 

which returns:

 {"expression":"","id":"6","operation":"(read|create|modify|delete)","target":"GeneralLedgerPostingRules"}
 

Mapping Permissions to Roles

Next task is to map permissions to roles. First we assign permission to view or modify customer deposit accounts to Teller role:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/role_perms/banking/Teller" -d 'permissionIds=["2"]'
 

which returns all permission-ids for given role, e.g.

 ["2"]
 

Then we assign permission to view, create, modify or delete customer deposit accounts to CSR (as CSR extends Teller it will automatically will get all permissions of Teller):

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/role_perms/banking/CSR" -d 'permissionIds=["1"]'
 

Then we assign permissions to create general ledger to Accountant:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/role_perms/banking/Accountant" -d 'permissionIds=["5"]'
 

Then we assign permission to modify ledger-posting rules to AccountingManager:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/role_perms/banking/AccountingManager" -d 'permissionIds=["6"]' 
 

Mapping Users to Roles

A role is associated with one or more permissions and each user is assigned one or more role. First, we assign subject “tom” to Teller role:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subject_roles/banking/tom" -d 'rolenames=["Teller"]'
 

which returns list of all roles for given subject or user, e.g.

 ["Teller"]
 

Then we assign subject “cassy” to CSR role:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subject_roles/banking/cassy" -d 'rolenames=["CSR"]'
 

Next we assign subject “ali” to role of Accountant:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subject_roles/banking/ali" -d 'rolenames=["Accountant"]'
 

Then we assign role AccountingManager to “mike”:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subject_roles/banking/mike" -d 'rolenames=["AccountingManager"]'
 

Finally we assign subject “larry” to LoanOfficer role:

 curl -H "Content-Type: application/json" --user "default:super_admin:changeme" -X PUT "http://localhost:8080/api/security/subject_roles/banking/larry" -d 'rolenames=["LoanOfficer"]'
 

Authorization

Now we are ready to validate authorization based on above security policies. For example, let’s check if user “tom” can view deposit-accounts, e.g.

 curl -v --user "banking:tom:pass" "http://localhost:8080/api/authorize/banking?operation=read&target=DepositAccount"
 

On successful authorization, the API returns 200 http responose-code and on failure it returns 401 http response-code, e.g.

 < HTTP/1.1 200 OK
 

Then we check if tom, the teller can delete deposit-account, e.g.

 curl -v --user "banking:tom:pass" "http://localhost:8080/api/authorize/banking?operation=delete&target=DepositAccount"
 

which returns http-response-code 401, e.g.

 < HTTP/1.1 401 Unauthorized
 

Then we create if cassy, the CSR can delete deposit-account, e.g.

 curl -v --user "banking:cassy:pass" "http://localhost:8080/api/authorize/banking?operation=delete&target=DepositAccount"
 

which returns:

 < HTTP/1.1 200 OK
 

Then we check if ali, the accountant can view general-ledger, e.g.

 curl -v --user "banking:ali:pass" "http://localhost:8080/api/authorize/banking?operation=read&target=GeneralLedger"
 

which returns:

 < HTTP/1.1 200 OK
 

Next we check if mike, the accounting-manager can create general-ledger, e.g.

 curl -v --user "banking:mike:pass" "http://localhost:8080/api/authorize/banking?operation=create&target=GeneralLedger"
 

which returns:

 < HTTP/1.1 200 OK
 

Then we check if larry, the loan officer can create posting-rules of general-ledger, e.g.

 curl -v --user "banking:mike:pass" "http://localhost:8080/api/authorize/banking?operation=create&target=GeneralLedgerPostingRules"
 

which returns:

 < HTTP/1.1 200 OK
 

Next, ali tries to create posting rules via

 curl -v --user "banking:ali:pass" "http://localhost:8080/api/authorize/banking?operation=create&target=GeneralLedgerPostingRules"
 

which is denied:

 < HTTP/1.1 401 Unauthorized
 

Summary

Above examples demonstrate how PlexRBAC can be used to define and enforce flexible security policies. In next post, I will describe instance based security, regular expressions and Java APIs for PlexRBAC.

December 27, 2009

Building Security Systems

Filed under: Computing — admin @ 11:20 pm

Being software developer for over eighteen years, I have observed a number of recurring problems and one of those recurring problems is security system. Most systems you build will require some kind of security so in this post I will go over core concepts when adding security to your system.

User Registration

A pre-requisite for any security system is to allow users to register to the system and store those users in some database, LDAP, Active Directory, or storage system. Though, for an internal application this step may be unnecessary.

Authentication

The authentication allows systems to validate users based on password or other form of verification. For internal applications within a company, users may have to use multiple applications with their own authentication and each external website would also require unique authentication. This quickly becomes burdensome for both users and applications as users have to remember the passwords and systems have to maintain them. Thus, many companies employ some form of Single-Sign-On and I have used many solutions such as SiteMinder, IChain, Kerberos, Open SSO, Central Authentication Service (CAS), or other home built solutions. These Single-Sign-On systems use reverse proxy servers that sit in front of the application and intercepts all requests and automatically redirects users to login page if the users are not authenticated. When an internal system consists of multiple tiers such as services, it is often required to pass authentication tokens to those services. In J2EE systems, you can Common Secure Interoperability (CSIv2) protocol to pass the authentication to other tiers, which uses Security Attribute Service (SAS) protocol to perform client authentication and impersonation.

For external systems, Open ID is a way to go and I have used RPX to integrate Open ID for a number of sites I have developed such as http://wazil.com/, http://dealredhot.com/, etc.

There are a number of factors that make authentication a bit tricky such as when part of your system does not require authentication, you have to ensure the authentication policy is being used correctly. Also, in general authentication requires https instead of http, so you have to ensure that the site use those protocols consistently. In generaly, static contents such as css, javascript and images do not require authentication but often they are also put behind authentication by mistake.

Another factor related to authentication is session management. A session determines how long the user can access the system without login. Though, many systems provide remember-me feature, but often sessions require system resources on the server. It’s essential to keep the session short as it can effect scalability if it’s stored on the server. I generally prefer keeping the session very short and storing only user-id and a couple of other database-ids such as shopping-cart-id, request-id, etc. If they are short, they can also be stored in cookies that makes a stateless system so you can scale easily.

Authorization

Not all users are same in most systems, thus authorization allows you to provide access control to limit the usage based on permissions and access control. There are a number of ways to define authorization such as Access control list, Role-based access control, Capability-based security, etc. In most systems, I have used J2EE/EJB Security, Java Web Security, JAAS, Acegi, which is now part of Spring and home built systems. As security is a cross cutting concern, I prefer to define those declaratively in a common security file or with annotations. There is nothing worse than sporadic security code mixed with your business logic.

One of feature I have found lacked in most of open source and commercial tools is support for instance based security or dynamic security that verifies runtime properties. For example, in most RBAC systems you can define rule that a purchase order can be approved by a role “POApprover”, but it does not allow you to say that “POApprover” can only approve if the user is from the same department or if amount is less than $10,000, etc.

UI or Resource Protection

When users have various level of access, it is essential to hide the UI elements and resources that are not accessible. Though, I have seen some systems employ security by obscurity that only hide the resources without actually enforcing the permissions, but it’s a bad idea. This can be complicated when the access level is very fine grained such as when a single form has fields based on role and permissions.

Database Security

The security must be enforced in depth, ranging from the UI, business and database tier. The database operations must use security to prevent access to unauthorized data. For example, let’s assume a user can post and edit blogs, it is essential that the database only allows the user to modify his/her blog. Also, it is critical that any kind of sensitive data such as passwords or personal identification with encryption. This is another reason I like OpenId or SSO solution because you don’t need to maintain them.

Method/Message Security

The message security ensures that a user only invokes the operations that he/she is authorized. For example, Acegi provides an annotation based mechanism to protect unauthorized methods.

Data Integrity

Any communication based systems may need to use message authentication check (MAC) to detect changes to the data.

Confidentiality

Any communication based systems may need to encrypt sensitive data with HTTPS.

Non-repudiation

The system must audit users action so that they cannot repudiate them.

Summary

As achieving high level of security can be difficult and expensive so you need to treat security as a risk and employ the level of security that suits the underlying system. Finally, as I have found most RBAC systems lack, I have started my own open source project PlexRBAC to provide instance based security. Of course if you hare interested in assisting with the effort, you are welcome to join the project.

« Newer PostsOlder Posts »

Powered by WordPress