Shahzad Bhatti Welcome to my ramblings and rants!

August 12, 2010

Implementing a Single Sign-on solution for WordPress (PHP) and Rails applications using Central Authentication Service (CAS)

Filed under: SSO — admin @ 2:36 pm

I am starting a new Rails project that would be using WordPress for CMS functionality and I needed a way to share the user sessions between WordPress and Rails. Though, I found a couple of WordPress plugins, but I decided to use Central Authentication Service (CAS). I found two server implementations, where the first one was in Java and second one was in Ruby. I had some experience with the Java based CAS server back in 2005, so I decided to use it. Here are the key installation and configuration steps for setting up the Single-Sign-On solution:

Create Rails Project

I had already installed Rails 2.3, so I created a simple project using:

 rails ssoapp
 

Install WordPress

I downloaded latest 3.0 version of WordPress and unzipped it under ssoapp/public directory.

Configuring PHP for Apache

I decided to use Apache for hosting WordPress and Rails applications. As I was using Mac, it already had Apache installed on my machine. All, I had to do was to uncomment following line from /etc/apache2/httpd.conf file:

 LoadModule php5_module        libexec/apache2/libphp5.so
 

I then restarted Apache using

 sudo /usr/sbin/apachectl restart
 

I created a database for wordpress and defined a username/password as follows:

 create database wp
 CREATE USER 'wp'@'localhost' IDENTIFIED BY 'wp';
 GRANT ALL ON *.* TO 'wp'@'localhost';
 

I specified database settings in wp-config.php as follows:

 define('DB_NAME', 'wp');
 
 define('DB_USER', 'wp');
 
 define('DB_PASSWORD', 'wp');
 
 define('DB_HOST', 'localhost');
 

and created an admin account by pointing my browser to http://localhost/wp/wp-admin/install.php.

Installing Phusion Passenger on Apache

I installed passenger gem as follows:

 gem install passenger
 

And installed passenger module for Apache using:

 passenger-install-apache2-module
 

I then added following lines to /etc/apache2/httpd.conf file:

 LoadModule passenger_module /Users/sbhatti/.rvm/gems/ruby-1.9.1-p378/gems/passenger-2.2.15/ext/apache2/mod_passenger.so                                                                                                                                                                                          
 PassengerRoot /Users/sbhatti/.rvm/gems/ruby-1.9.1-p378/gems/passenger-2.2.15
 PassengerRuby /Users/sbhatti/.rvm/rubies/ruby-1.9.1-p378/bin/ruby
 

I created a file called ssoapp.conf under /etc/apache2/users with following contents:

 <VirtualHost localhost:80>
       ServerName localhost
       DocumentRoot /Users/sbhatti/src/yoga-rails/public
       RailsEnv development
       <Directory /Users/sbhatti/src/yoga-rails/public>
          Allow from all
          Options Indexes -MultiViews
       </Directory>
       <Location /wp>
          PassengerEnabled off 
          AllowOverride all 
       </Location>
       <Location /cas>
          PassengerEnabled off 
          AllowOverride all 
       </Location>
 </VirtualHost>
 

Configuring SSL for Apache

I needed SSL for some functionality so I created self-signed keys and certificates a follows:

 openssl genrsa -des3 -out server.key 1024
 openssl req -new -key server.key -out server.csr 
 

and copied those files to /etc/apache2 directory. I then modified /etc/apache2/httpd.conf by uncommenting following line:

 LoadModule ssl_module libexec/apache2/mod_ssl.so
 

and adding

 Listen 443
 

I then modified /etc/apache2/users/ssoapp.conf and added following contents:

 <IfModule mod_ssl.c>
   AddType application/x-x509-ca-cert .crt
   AddType application/x-pkcs7-crl .crl
 
   SSLProtocol all -SSLv2
   SSLPassPhraseDialog builtin
   SSLSessionCache dbm:/var/run/ssl_scache
   SSLSessionCacheTimeout 300
   SSLMutex file:/var/run/ssl_mutex
   SSLRandomSeed startup builtin
 <VirtualHost localhost:443>
       ServerName localhost
       DocumentRoot /Users/sbhatti/src/yoga-rails/public
     ServerAdmin sbhatti@peak6.com
     ErrorLog /tmp/error_log
     TransferLog /tmp/access_log
     SSLEngine on
     SSLProtocol all -SSLv2
     SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
     SSLCertificateFile /etc/apache2/server.crt
     SSLCertificateKeyFile /etc/apache2/server.key
     <Files ~ "\.(cgi|shtml|phtml|php3?)$">
       SSLOptions +StdEnvVars
     </Files>
       <Directory /Users/sbhatti/src/yoga-rails/public>
          Allow from all
          Options Indexes -MultiViews
         SSLOptions +StdEnvVars
       </Directory>
       <Location /wp>
          PassengerEnabled off
          AllowOverride all
       </Location>                                                                                                                                                                                                                                                                                                          
       <Location /cas>
          PassengerEnabled off 
          AllowOverride all 
       </Location>
     SetEnvIf User-Agent ".*MSIE.*" \
     nokeepalive ssl-unclean-shutdown \
     downgrade-1.0 force-response-1.0
     CustomLog /var/log/httpd/ssl_request_log \
     "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
 </VirtualHost>
 </IfModule>
 

Basically, above configuration allows all HTTP and HTTPS requests be sent to my SSO rails app except /wp requests, which are sent to the WordPress.

Password Encoding

I decided to use WordPress users table (wp_users) as primary users table. WordPress by default uses PHPPass library for encrypting passwords. The latest WordPress uses combination of salt, MD5 and iterations for encrypting, but I wanted to use simple MD5 for now, so I edited ./wp-includes/pluggable.php and replaced default hash encoding to MD5 as follows:

 function wp_hash_password($password) {
    return md5($password, FALSE);
 }
 

I then reset the password in Mysql database as follows:

 update wp_users set user_pass = md5('hello') where user_login = 'admin';
 

Setting CAS Server

I downloaded and unzipped CAS Server. I needed to configure CAS server to use the wp_users table for authenticating users, so I added following dependencies to the cas-server-webapp/pom.xml:

         <dependency>
                 <groupId>mysql</groupId>
                 <artifactId>mysql-connector-java</artifactId>
                 <version>5.0.5</version>
         </dependency>
         <dependency>
                 <groupId>commons-dbcp</groupId>
                 <artifactId>commons-dbcp</artifactId>
                 <version>1.4</version>
         </dependency>
         <dependency>
                 <groupId>${project.groupId}</groupId>
                 <artifactId>cas-server-support-jdbc
                 </artifactId>
                 <version>${project.version}</version>
         </dependency>
 

I then added following beans to the cas-server-webapp/src/main/webapp/WEB-INF/deployerConfigContext.xml:

         <bean id="md5PasswordEncoder"
                 class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
                 <constructor-arg index="0" value="MD5" />
         </bean>
         <bean id="wpDataSource" class="org.apache.commons.dbcp.BasicDataSource">
                 <property name="driverClassName">
                         <value>com.mysql.jdbc.Driver</value>
                 </property>
                 <property name="url">
                         <value>jdbc:mysql://localhost:3306/wp
                         </value>
                 </property>
                 <property name="username">
                         <value>wp</value>
                 </property>
                 <property name="password">
                         <value>wp</value>
                 </property>
         </bean>
 

and removed default authentication class org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler and added following bean instead:

         <property name="authenticationHandlers">
                 <list>
         ...
                         <bean
                                 class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler">
                                 <property name="tableUsers">
                                         <value>wp_users</value>
                                 </property>
                                 <property name="fieldUser">
                                         <value>user_login</value>
                                 </property>
                                 <property name="fieldPassword">
                                         <value>user_pass</value>
                                 </property>
                                 <property name="dataSource" ref="wpDataSource" />
                                 <property name="passwordEncoder" ref="md5PasswordEncoder" />
                         </bean>
 
                 </list>
         </property>
 

Above bean configuration specifies MD5 for hashing passwords.

Creating WAR file for CAS-Server

I then created the CAS war file by typing:

 mvn clean install
 

Install and Configure Tomcat

The CAS-server requires Java application server so I downloaded and installed latest Apache Tomcat. Next, I needed to setup SSL for secure login so I created another set of certificate and private key using keytool as follows:

 keytool -genkey -alias ssoapp -keypass ssoapp -keystore ssoapp.ks -storepass ssoapp
 

I copied ssoapp.ks to the conf directory under Apache tomcat and modified conf/server.xml and added following contents:

         <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
                 maxThreads="150" scheme="https" secure="true" clientAuth="false"
                 sslProtocol="TLS" keystoreFile="${catalina.home}/conf/ssoapp.bin"
                 keystorePass="ssoapp" />
 

I then started the Apache Tomcat using:

 bin/startup.sh 
 

Configuring CAS-client for WordPress

First, I downloaded phpCAS client library and unzipped it under public directory of my Rails app. I used wpCas plugin for WordPress, which uses phpCAS library for connecting to the CAS server. I installed the plugin under wp-contents/plugins directory and logged in to WordPress as admin. I clicked the plugins link from left navigation and then activated the plugin. After activating it, I clicked the settings link from the left navigation and configured the CAS as follows:

Now, the moment of truth. I opened another instance of browser and pointed to http://localhost/wp, which showed my blog.

I then clicked on “Log in” link, which showed me certificate warning due to self-signed certificate. After accepting it, it took me to the CAS login page, e.g.

Voila, after typing my username and password, it took me back to the WordPress.

Setting up CAS-Client for Rails

I used rubycas-client plugin for Rails and installed it as follows:

   ./script/plugin install git://github.com/gunark/rubycas-client.git
 

I then modified config/environment.rb file and added:

   require 'casclient'
   require 'casclient/frameworks/rails/filter'                                                                                                                                                                                                                                                                              
   CASClient::Frameworks::Rails::Filter.configure(
     :cas_base_url => "https://localhost:8443/cas"
   )
 

One gotcha in above configuration is that you have to place it at the end of configuration file as you get load error if you place it inside initializer block.
I then added before_filter CASClient::Frameworks::Rails::Filter to all controllers that needed authentication and added CASClient::Frameworks::Rails::Filter.logout(self) where I needed to logout. After changing my controller, I went back to the Rails app and sure enough it worked like a charm!

Summary

Though, there were many small pieces to get this working, but I am happy with this solution. This would help create seamless experience to the users, though the other half of the experience depends on unified user interface.

References



August 10, 2010

NoSql databases bring “Stored Procedures” back in fashion

Filed under: Business — admin @ 5:26 pm

One of best tip I learned from my post-graduate research in Parallel & Distributed area was to bring the computation closer to the data. However, most applications in the real world are designed as three or more tiers that separate databases from the application server, where the business logic resides. Though, stored procedures have long been used in client server architecture, dataware services, reporting, and other forms to run the business logic closer to the database, but they are generally shunned due to the maintenance issues. I find it interesting that NoSQL databases are bringing back the stored procedures in the form of map/reduce queries. NoSQL databases come in various forms such as key-value stores, document stores, column stores, and graph stores. They are primarily influenced by the Brewer’s CAP Theorem and use BASE (basically available, soft state, eventually consistent) transactions as opposed to ACID (atomicity, consistency, isolation, durability) transactions. NoSQL databases are designed for horizontal scalability and are able to support large data by partitioning it. NoSQL offer rich queries based on map/reduce, which are generally written in javascript or other scripting languages. These queries provide powerful mechanism to define the business logic for filtering or aggregating results, which are then executed inside the database or closer to the data. Thus, NoSQL databases are able to provide much better performance as a side effect if the application logic is transferred to the host where the data resides. Everything old is new again and stored procedures are back in the fashion.

References:


August 5, 2010

Implementing Apple Remote Push Notification using Objective-C and Ruby

Filed under: iPhone development — admin @ 3:44 pm

Recently, I added the remote push capabilities to an iPhone/iPad application at work. It turned out to be a bit more involved process than I expected so I would document key steps of the process.

Overview of Remote Push Notification

The Apple Push Notification works like SMS, where the application providers can send short messages to the users (upto 256 bytes). The message consists of JSON structure as follows:

 { 
   "aps" : {  "alert" : "your-message", "sound" : "filename" : "badge" : 5,     },     
   // optionally more structures, e.g. {"acme1" : "bar"}
 }
 

The alert key defines the contents of the message, which is displayed to the user. You can play sound by specifying the sound filename (which is already bundled with the app), however you can leave it blank or use ‘default’ for playing default sound. Finally, the badge will allow notification to show a number next to your application icon. Also, you can optionally add action buttons that can take user to a particular screen in your application (though I won’t show it here).

The format for sending notification is as follows:

You can read Apple documentation for more inforamtion on the binary format.

The architecture of Push Notification consists of three key pieces:

  • iOS Application – that enables push notification and registers the device for notification
  • Application Server – is responsible for generating the messages and publishing them to the Apple Push Notification Service (APNS)
  • Apple Push Notification Service (APNS) – is responsible for delivering the messages to the devices where the application is installed

Here is the architecture diagram from Apple documentation on Push Notification:

Enabling Push Notification for the application

I assume you have already created an application with developer certificate, app-id and provisioning profile. However, Apple Push Notification requires another set of certificates for development and production. So, login to the iPhone developer site and select iPhone Provisioning Portal from the upper right side and then choose App IDs link.

Select “Enable for Apple Push Notification service” checkbox, then click on Configure link:

It will guide you through creating certificate, where you will have to create a certificate request by opening “Keychain Access”, then selecting “Certificate Assistant” option from the menu and choosing “Request a Certificate from Certificate Authority”. You can then upload the generated request file to the portal. Once the certificate is generated you should see the option to download the certificate, e.g.

After downloading the certificate, you can double click or drag it to the “Keychain Access” and it will add the certificate to the Keychain.

Publishing Push Notifications on the Application Server

The push notification requires a server that connects to the APNS service and sends notification messages. You will need to use same set of keys and certificates that you created on the Apple development portal. So open “Keychain Access” and select Keys from the left options, then select the private key for the push notification. You can then export the key by right clicking, which will create a file with .12 file extension. Note that it will ask you for a password for encrypting the key (type in anything as we will remove it later). Next, select “My Certificates” from the left options and select the certificate for push notification. Then repeat the process of exporting by right clicking and saving the certificate. You will see two files with .12 extensions, which can be converted into .pem files as follows:

 openssl pkcs12 -clcerts -nokeys -out apns_cert.pem -in 
 openssl pkcs12 -nocerts -out apns_key.pem -in 
 

You can remove password with the the following command:

 openssl rsa -in apns_key.pem -out apns_key_unenc.pem 
 

Then merge two files with following command:

 cat apns_cert.pem apns_key_unenc.pem > apns.pem
 

I used Rails on the server side, so I defined a few models and controllers to store devices and messages, where the device model had an attribute ‘token’ and the message model had three attributes: alert, badge and sound. Each device also has an attribute deactivated_at, which is set to the date when the device is disabled. Next, I created a short Ruby library for sending notifications. The library provides two methods send_message for sending notifications and get_feedbacks for checking disabled devices so that you can remove them from your database. The send_message method accepts a message model as an argument and then opens a connection to the APNS service. Apple recommends using a single connection for publishing notifications to all devices, so it adds all devices within a single a connection.

 require 'socket'                                                                                                                                                                                                                                                                                                           
 require 'openssl'
 
 class ApnsPublisher
   def self.send_message(apns_message)
     json_payload =  {"aps" => {"alert" => apns_message.alert, "sound" => apns_message.sound, "badge" => apns_message.badge}}.to_json.to_s
 
     open_connection(APNS_SERVER, 2195) do |conn, sock|
       ApnsDevice.find_in_batches(:batch_size => 500 ) do |devices| 
         devices.each do |device|
           next unless device.deactivated_at.nil?
           unless ApnsDevicesMessage.find_by_apns_device_id_and_apns_message_id(device.id, apns_message.id)
             token = device.token.gsub(/\s+/,'')
             byte_token = [token].pack("H*")
             message = "\0\0 #{byte_token}\0#{json_payload.length.chr}#{json_payload}"                                                                                                                                                                                                                                      
             raise "message #{message} is too big" if message.size.to_i > 256 
             conn.write(message)
             f.write(message)
             ApnsDevicesMessage.create(:apns_device_id => device.id, :apns_message_id => apns_message.id, :delivered_at => Time.new.utc)
           end                                                                                                                                                                                                                                                                                                              
         end 
       end 
     end 
   end 
 
   def self.get_feedbacks()
     open_connection(APNS_FEEDBACK_SERVER, 2196) do |conn, sock|
       while line = sock.gets
         line.strip!
         feedback = line.unpack('N1n1H140')
         token = feedback[2].scan(/.{0,8}/).join('').strip
         device = ApnsDevice.find_by_token(token)
         if device
           device.update_attribute(deactivated_at, Time.at(feedback[0])
         end 
       end 
     end 
   end 
 private
   def self.open_connection(host, port, passphrase='')
     cert = File.read(APNS_CERT_FILE)
     ctx = OpenSSL::SSL::SSLContext.new
     ctx.key = OpenSSL::PKey::RSA.new(cert, passphrase)
     ctx.cert = OpenSSL::X509::Certificate.new(cert)
   
     sock = TCPSocket.new(host, port)
     ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
     ssl.sync = true
     ssl.connect
     yield ssl, sock 
   
     ssl.close
     sock.close
   end 
 end
 

You can use delayed_job plugin to run the get_feedbacks method periodically. Also, I added a service for registering and adding the devices, which is called from the iPhone devices (but not shown here).

Configuration

You will need to define following properties for development:

 APNS_SERVER = "gateway.sandbox.push.apple.com"
 APNS_FEEDBACK_SERVER = "feedback.sandbox.push.apple.com"
 APNS_CERT_FILE = File.join(RAILS_ROOT, 'config', 'apns_dev.pem')
 

and following properties for production:

 APNS_SERVER = "gateway.push.apple.com"
 APNS_FEEDBACK_SERVER = "feedback.push.apple.com"
 APNS_CERT_FILE = File.join(RAILS_ROOT, 'config', 'apns_prod.pem')
 

Registering devices for notification

You can register the device by adding following method in your didFinishLaunchingWithOptions of the primary delegate class:

 
         [[UIApplication sharedApplication] 
                 registerForRemoteNotificationTypes:( UIRemoteNotificationTypeAlert |UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
 
 

and then registering with following callback methods:

 #pragma mark push notifications
 - (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
         NSString *token = [[devToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<> "]];
         token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];
         DeviceRegisterer *registrar = [[DeviceRegisterer alloc] init];
         [registrar registerDeviceWithToken:token];
 }
 
 
 - (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
         NSLog(@"failed to regiser %@", err);
 }
 
 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
         NSLog(@"notification options %@", userInfo);
 }
 
 

I used ASIHTTPRequest library for invoking REST service I wrote for registering devices (on Rails side), e.g.

DeviceRegisterer.h

 @class ASIFormDataRequest;                                                                                                                                                                                                                                                                                                 
 
 
 @interface DeviceRegisterer : NSObject {
         ASIFormDataRequest *request;
 
 }
 
 @property (retain, nonatomic) ASIFormDataRequest *request;
 
 - (void)registerDeviceWithToken:(NSString *)token;
 
 @end
 

DeviceRegisterer.m

 
 #import "DeviceRegisterer.h"
 
 #import "ASIFormDataRequest.h"
 
 @implementation DeviceRegisterer
 @synthesize request;
 
 - (void)registerDeviceWithToken:(NSString *)token {
         if (self.request == nil) {
                 NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/admin/apns_devices.json", API_BASE_DOMAIN]];
                 [self setRequest:[ASIFormDataRequest requestWithURL:url]];
                 [request setPostValue:token forKey:@"token"];
 
                 [request setTimeOutSeconds:30];
                 [request setDelegate:self]; 
                 [request setDidFailSelector:@selector(registerFailed:)];
                 [request setDidFinishSelector:@selector(registerFinished:)];
                 
                 [request startAsynchronous];            
         }
 }
 
 
 - (void)registerFailed:(ASIHTTPRequest *)theRequest {
         NSLog(@"registerFailed %@", [theRequest error]);
 }
 
 - (void)registerFinished:(ASIHTTPRequest *)theRequest {
         NSLog(@"registerFinished %d",[theRequest postLength]);
 }
 
 - (void)dealloc {
         [request cancel];
         [request release];
         [super dealloc];
 }
 
 @end
 

Summary

In nutshell, push notification is powerful feature that can help your users engage with your application, though it must be used with caution so that users are not annoyed and in turn remove your application or disable it. It also requires a lot of moving parts for sending notification, registering, getting feedback on the devices. I found that Apple does not provide a great debugging options when testing the push notification. For example, first problem I encoutered with testing was that my profile on XCode was old and wasn’t updated after I enabled push notification. I had to delete my old profile and then refresh it from the Organizer. Also, when you send notifications to the APNS, you don’t get any response code or errors. This caused some frustration when I wasn’t getting messages due to slightly wrong JSON format. Fortunately, when your device is connected you can select the device from the XCode Organizer and view the Console tab for debugging information. I was able to view the error (which a bit vague) and then got everything working after fixing the JSON structure.



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.


June 3, 2010

A few lessons from Seth Godin’s book – Linchpin: Are You Indispensable?

Filed under: Business — admin @ 10:25 pm

I just finished reading Seth Godin’s new book Linchpin: Are You Indispensable?. Seth shows how the white-collar jobs, which supposed to save the middle class are being eliminated either by machines or outsourcing with cheap labors. He shows that you can either continue to live your life as a faceless cog or choose to become Linchpin. Here are some of the lessons I learned from this book:

Industrial Revolution is Over

The race to make average stuff for average people in huge quantities is almost over.

This book shows the industrial revolution is changing and in order to survive in the new era of economy, you have to become linchpin or indispensable. In last three hundred years, the industrialization began by standardizing the tasks so that it can be performed by easily replaceable labor or so called cogs. It relied on two layers: management and labor, where management breaks production of goods into tiny tasks, which are performed by the labor. The management wins when it can get the most work for the least pay. The system taught workers to follow the instructions and you don’t have to think. Though, that system worked but has been falling apart in the face of competition, outsourcing and globalization. The attendance-based compensation (ABC) is over. Th old American dream that taught to keep your head down, follow instructions, work hard and you will be rewarded is dead. The mass production treats everything such as labor and material as interchangeable. However, in global market, the competition is fierce and cheap strategy doesn’t scale very well.
Instead of easily replaced laborers or cogs, you can choose to become Linchpin by differentiating yourself from the rest and focusing on humanity, connection and art. The web has made it easier to be productive and create or invent. The new American dream is to be remarkable, generous, create art and connect with people.


Education System is a Sham


In capitalist market, the companies make money by hiring obedient and competent workers as cheaply as you can and using productivity advantage to earn more profit. Andrew Carnegie saw that limited amount of education to get them to cooperate. The school system throughout the world encourages mediocre obedience and is driven by fear as when we learn things in fear. Seth shows public school system is designed to prepare us for factories, where we are just replaceable cogs and care little about our jobs or customers. The same factory model created consumer culture that uses consumption as a shortcut to happniess. Instead, school should teach solving interesting problems and leading.


Becoming a Linchpin


In order to become a linchpin or indispensable, you must embrace an artist and genius within you. Seth recommends avoiding asympototic goals such as bowling, where there is a ceiling of how good you can be. Also, for an artist, the economy is not just zero sum game, instead he/she can increase the pie. Seth cites Richard Florida’s survey of top ten reason for employees to do best work as follows:

  • challenge and responsibility
  • flexibility
  • stable work environment
  • money
  • professional development
  • peer recognition
  • stimulatng colleagues and bosses
  • exciting job content
  • organization culture
  • location and community

All of above reasons except money are internal that we can control. Seth encourages readers to find the work that suits your passion. He uses Emotional labor term, originally coined by Arlie Hochschild to connect with the work. Though, you may get a little compensation in return of emotional labor, but you get inward reward. Instead of day’s work for day’s job or the poverty mentality that treats life as zero sum game, you give gift and build bonds. Seth shows that the easier work is to quantify, the less it’s worth and more humanity you bring to your work, the better results you will receive. Seth cites Krulak’s law for building strong relations with your customers, i.e.,

The closer you get to the front, the more power you have over the brand.
 


Resistance to Change


Seth gives plenty of examples and demonstrates that real artists ship, however shipping is hard due to trashing/tweaking and coordination. According to Seth, the biggest resistance to the change is our lizard brain. He explains how we all have two brains: primeval brain or lizard brain and gray matter or recently developed brain. The lizard brain has animal instincts such as hungry, scared, angry and horny, whereas newer brain allows big thoughts, generosity, speech, and art. Lizard brain seek compfort and obedience, and avoids risks, public speaking and generosity.


Good is enemy of perfect


Seth encourages readers to become excellent and not perfect as art is never defect-free. He cites Bre Pettis, who says that there are three states of being: not knowing, action and completion. He says accept that everything is draft as it helps to get it done.


Generosity


Exchanging gifts is an ancient tradition. Seth shows that artists who give gifts win as becoming a linchpin is not an act of selfishness. Seth also shows how usury was prohibited in Bible as interest-free loan was kind of gift. This changed when Martin Luther lifted the sanction to get support for the Protestant Reformation. Seth writes:

For the last five hundred years, the best way to succeed has been to treat everyone as a stranger you could do business with.

Seth cites Metcalfe’s law states that the value of a network increases with the square of the number of nodes on the network. The new social media platforms such as Twitter, Facebook, Blogsphere, and Internet is changing the circle of the gift system and he shows that there are three cicles of gifts, the first circle represents true gifts to family and friends. The second circle is for commerce, they pay for souvenir edition and the third circle is your tribe, followers, fans or friendlies.


There is no map


Seeing the future is hard because we are attached to the world and want stability and fear change. Seth gives plenty of examples of record industry and newspaper industry who have been too attached with their legacy model and failed to adjust in the new economy. In order to become linchpin, you need to draw a map and lead instead of being passive. You need to find a job that matches your passion.


Culture of connections

How to Make a Personal Connection with Customers
The industrialization removed human connection between different parties. The social media and Internet is changing that, now companies can connect directly with their customers and receive their input. Often, when companies negotiate with other companies, the key point of distinction is the perceived connection between the prospect and the organization. The salesman who relies only on the script would fail, instead you have to rely on honest signals and genuine gifts to make connections.


Seven attributes of Linchpin

Linchpins are geniuses, artists and givers of gifts, who extert emotional labor and make their own map. Here are seven abilities of the linchin:

  • Providing a unique interface between members of the organization
  • Delivering unique creativity
  • Managing a situation or organization of great complexity
  • Leading customers
  • Inspiring staff
  • Providing deep domain knowledge
  • Possessing a unique talent

Conclusion

We have been in declining economy for a while and many of the white collar and blue collar jobs lost in last few years won’t come back. I found a lot of Seth’s advice similar to agile movement in software development and My Job Went to India. I also wrote about Taylorism in my blog IT Sweatshops, where I deplored Taylorism based command and control structure in a lot of companies even the one that claim to adopt agile methodologies. Seth even says that you don’t need a resume as it hides the fact that you are linchpin. Instead have a project that an employer can see or blog that people can follow. I find this book offers very practical and timely advice for future market. In the job market, You need to differentiate yourself and have a trail of breadcrumbs of your previous work. Being an average is over, instead you have to be a linchpin and live without a map.

May 24, 2010

Validating receipts from Apple iPhone store in Ruby

Filed under: iPhone development — admin @ 1:41 pm

Recently, I had to write a service on Ruby-on-Rails project to validate an in-store purchase for iPhone application. Though, it was fairly straight forward, but I am posting the code in case someone else needs it:

  1 require 'net/http'                                                                                                                                                                                                                                                            
 
  2 require 'net/https'
  3 require 'uri'
  4 
  5 class AppleReceiptVerifier 
 
  6   #
  7   ### Verifies Apple receipt submitted by iPhone 
  8   ### See http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Overview%20of%20the%20Store%20Kit%20API/OverviewoftheStoreKitAPI.html#//apple_ref/doc/uid/TP40008267-CH100-SW14
 
  9   #
 10   def self.verify(b64_receipt)
 11     url = URI.parse(APPLE_RECEIPT_VERIFY_URL)
 
 12     http = Net::HTTP.new(url.host, url.port)
 13     http.use_ssl = true 
 14     http.verify_mode = OpenSSL::SSL::VERIFY_NONE
 
 15     valid = false
 16     json_request = {'receipt-data' => b64_receipt}.to_json
 17     resp, resp_body = http.post(url.path, json_request.to_s, {'Content-Type' => 'application/x-www-form-urlencoded'})
 
 18     if resp.code == '200'
 19       json_resp = JSON.parse(resp_body)
 20       if json_resp['status'] == 0
 
 21         valid = true 
 22       end 
 23     end 
 24     valid
 25   end 
 
 26 end
 27 
 28 
 

Note that the receipt returned by iPhone APIs is not base 64 encoded so you will need to encode it before calling the service. Also, for testing, APPLE_RECEIPT_VERIFY_URL will point to the sandbox environment, i.e., https://sandbox.itunes.apple.com/verifyReceipt and for real purchase it will point to https://buy.itunes.apple.com/verifyReceipt. Finally, you can learn more from the Apple documentation on purchase model and on validating receipts.


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


April 29, 2010

First Arduino project

Filed under: Arduino — admin @ 6:04 pm

I recently bout Arduino Starter Kit that comes with Arduino Duemilanove ATmega328 and a bunch of LEDs, buttons, resisters and wires. I then downloaded the Arduino software from http://www.arduino.cc/en/Main/Software. I downloaded the Mac OS X version and installed it on my Mac Pro laptop. Once the software was installed, I launched the software and connected USB cable to the Arduino board as below:

Arudino software comes with a number of small applications and I tried their blink application by selecting File->Examples->Digital->Blink from the top menu. I then selected my board by selecting Tools->Board->ATmega328. Then I tried to look for USB connection under Tools->Serial Board, but didn’t see any. I googled for Arduino with Mac OS, and found USB driver, it required reboot so after reboot I was able to see it, e.g.

I inserted the LED to pin 13 that has builtin resistor, where the longer leg with positive charge went into the hole 13 and short leg went into the ground hole next to it. I then used the software to upload the application to the Arduino, e.g.

Voila, I was able to see the LED blinking, success.

April 24, 2010

Favorite fifteen tips from “Rework” book by Jason Fried and DHH

Filed under: Business — admin @ 2:04 pm

I have long been following Jason Fried of 37Signals and read his first book Getting Real. Jason along with DHH have put together many of their ideas from their blog Signal vs. Noise into a new book Rework. I just finished reading it and though it reiterates many ideas from the earlier book “Getting Real” and their blogs, it’s worth re-reading those ideas as many of business companies today still runs on old fallacies. The book consists of thirteen sections and over eighty ideas, here are my favorite ideas from the book:

Failure is not a rite of passage

Overcoming Failure in Life: The Failure Checklist

I have heard the advice from startup folks about “Fail early and fail often.” On the contrary, this book shows that the people who learn from mistakes will make new mistakes, instead success shows what actually works. Another related advice in the book is “Reason to quit”, which shows when you can quit and choose something else. When I read Founders at Work: Stories of Startups’ Early Days, it also showed that most startups don’t stick to their original ideas and move to other ideas based on early feedback.

Planning is Guessing

Key Challenges in Test Estimation and Planning | Trusted Partner for Software Testing, Test ...

This is related to another advice from the book “Your estimates suck” as Planning and Estimation is hard especially in software business. I have written about Software Estimation in my earlier blogs, however most places still equate estimates with commitments. Jason and DHH reminds us again that estimates are just guesses that were made based on the best information available at the time.

Workaholism


This is another unorthodox advice that is contradictory to how most software projects are run. Most companies measure workers’ dedication on how many hours they put even when they are not actually producing desired outcome. This is also common when managers treat estimates as commitments and refuse to admit reality when things change. We are all familiar with iron triangle of schedule/cost/functionality or sometime referred to as cost/quality/schedule or cost/resources/schedule. Often business folks are unwilling to change schedule and functionality, which often requires working late hours. This is also related to Heroism, which I have blogged before and go to sleep, as work-holism can result in sleep deprivation, which reduces creativity and productivity.

Scratch your own itch

Most successful businesses started with hobbies or personal interests or problems and there are tons of examples of this. This advice is also related to eat your own dog food, though not mentioned in this book.

Start making something


Jason and DHH reminds us another great point that ideas are cheap and the real question is how well you execute them.

Draw a line in the sand


One of the key characteristics of Ruby on Rails software that DHH produced is having strong opinions that limits variations. Similarly, 37Signals is known for their simple design and limited features. You can differentiate yourself from others by standing for something.

Outside money is Plan Z


Both DHH and Jason often talked about downside of getting money from venture capitalists and I agree that these days you can start most software startups with minimal money and raising money can be very distracting. Another related tip that “building a flip is building to flop”, which is often what startup founders hope to get out.

Start at the epicenter


This book recommends focusing on your core product. Though, this book briefly mentions this topic but there is a great presentation of Video of Geoffrey Moore at Business of Software 2009 that talks about similar topic. This advice is also related to other tips from the book such as “don’t copy”, “decommoditize your product”, “focus on you instead of they”, i.e., focus on your core strengths and not your competitors.

Focus on what won’t change


This is great advice for building business that will last. I remember when I started working at Amazon, we were told the core values of Amazon that included having a large selection, cheap prices, customer service and everything we built started from outside-in focus, i.e., it started with customers.

Get it out here


This is similar to common advice from the startup and agile community, i.e. release early and release often.

Interruption is the enemy of productivity

Interruption Is God's Invitation | Desiring God

More and more research is showing that our brain can’t focus on one thing at a time, and constant interruption and multi-tasking hampers your productivity. This is also somewhat related to office space is setup as many agile practices encourage more open space with pair programming and I have found that it prevents concentration. I found that private office pattern offered from Organizational Patterns of Agile Software Development provides less interruption.

Meetings are toxic


This is another hallmark idea of 37Signals and the book contains a number of tips on making your productive such as fixed time, fewer people, clear agenda, beginning with a specific problem and ending with action items and making someone responsible for them.

Good enough is fine


37Signals is known for their simple design and fewer features. This is related other advice in the book such as “embrace the constraints”, “throw less at the problem”, “underdo your competitor”, “say no” and “be a curator”. When you have limited resources, you can become more creative. Also, you are better off building half a product, not a half assed product.

Make tiny decisions


The authors encourage to make tiny decisions as big decisions are hard to make and hard to change. This advice is related to other tips such as “decisions are progress”, which encourages you to always make progress and “quick wins”, which encourages you to build momentum by accomplishing small tasks.

Build an audience


The authors encourage to build audience that come back to you by writing blogs, tweets and speaking. This is also related to “sell your by-products”, “emulate chefs”, “emulate drug dealers” and “out-teach your competitors”.

Conclusion

Though, I skipped many gems of advice on hiring, culture and marketing but I suggest you read the book for many practice advice on building a long lasting and successful business.

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.

« Newer PostsOlder Posts »

Powered by WordPress