Letsgetdugg

Random tech jargon

I finally got around to open sourcing our scala memcached implementation that we use at fabulously40 for session storage.

Since wicket sessions can vary greatly in size, using the standard memcached server implementation became impractical due to the slab allocator.

The current code on github lacks the ehcache store and an Actor IoHandler adapter. The internal SMemcached application at fabulously40 uses a private caching API so we can hook up various caching backend storage implementations such as mysql, postgresql, ehcache or even another memcached server. You can grab the TCache project on github that SMemcached uses to unify caching under a single API. This gives SMemcached a lot of flexibility when it comes to caching your data.

fyi. TCache stands for “Tanek” Cache, Tanek means cache in russian.

The project works quite well, but don’t use it in production just yet since there is no data expiration for cached data in the HashMap storage implementation. This is just a technical preview. Do use it in production, this is what we use at Fabulously40 ;-)

http://github.com/victori/smemcached

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with , , ,

Want horoscopes on your site? Give our web service a try.

http://widgets.fabulously40.com/horoscope.json?sign=cancer http://widgets.fabulously40.com/horoscope.yml?sign=cancer http://widgets.fabulously40.com/horoscope.xml?sign=cancer

To pull by specific date…

YYYY-MM-DD month format

http://widgets.fabulously40.com/horoscope.json?sign=aries&date=2009-05-03 http://widgets.fabulously40.com/horoscope.yml?sign=aries&date=2009-05-03 http://widgets.fabulously40.com/horoscope.xml?sign=aries&date=2009-05-03
Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis

I would be sitting on a gold mine.

SLOC	Directory	SLOC-by-Language (Sorted)
37890   src             java=37890
5026    contrib-utilities java=5026
4457    contrib-crud    java=4457
2259    contrib-mootools java=2259
1235    contrib-generaldao java=1235
1185    contrib-emailmanager java=1185
986     jetty-memcache  java=986
961     contrib-cache   java=961
787     jmemcached      java=786,sh=1
640     contrib-thumbnail java=640
503     contrib-blueprint java=503
181     contrib-snapshot java=181
32      contrib-nicedit java=32

Totals grouped by language (dominant language first):
java:         56141 (100.00%)
sh:               1 (0.00%)

Total Physical Source Lines of Code (SLOC)                = 56,142
Development Effort Estimate, Person-Years (Person-Months) = 13.73 (164.80)
 (Basic COCOMO model, Person-Months = 2.4 * (KSLOC**1.05))
Schedule Estimate, Years (Months)                         = 1.45 (17.39)
 (Basic COCOMO model, Months = 2.5 * (person-months**0.38))
Estimated Average Number of Developers (Effort/Schedule)  = 9.47
Total Estimated Cost to Develop                           = $ 1,855,214
 (average salary = $56,286/year, overhead = 2.40).
SLOCCount, Copyright (C) 2001-2004 David A. Wheeler
SLOCCount is Open Source Software/Free Software, licensed under the GNU GPL.
SLOCCount comes with ABSOLUTELY NO WARRANTY, and you are welcome to
redistribute it under certain conditions as specified by the GNU GPL license;
see the documentation for details.
Please credit this data as "generated using David A. Wheeler's 'SLOCCount'."
Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with

One of the first few things I have tackled while creating fabulously40 was stateless pagination. I wanted to keep all the public facing pages stateless to avoid session overhead. Wicket makes pagination brain dead simple at the cost of session use. I wanted to keep the simple programming model yet be completely stateless. Here is my solution to the problem.

Wicket a stateful framework, is actually really good at being stateless.

Example code on how to use it.

update: I would suggest using MixedParamHybridCodingStrategy with the stateless paging navigation to keep links clean.

StatelessDataView<Photo> dv;
add(dv = new StatelessDataView<Photo>("pics", getPhotoProvider(), amount, pageParams) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected void populateItem(final Item<Photo> arg0) {
              }
});

// call back page class, page paramaters passed, the dataview and 12 pagination links.
add(new StatelessSimplePagingNavigator("nav", PhotosPage.class, pageParams, dataView, 12));

The dataview

package com.base.components;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.IDataProvider;

public abstract class StatelessDataView<T> extends DataView<T> {
        private static final long serialVersionUID = 1L;
        private PageParameters pp;

        @Override
        protected boolean getStatelessHint() {
                return true;
        }

        protected int getPageNumber(final String param) {
                String numResult = param;
                if(numResult.contains(".wicket-")) {
                        numResult = numResult.substring(0,numResult.indexOf(".wicket-"));
                }
                return Integer.valueOf(numResult);
        }

        public StatelessDataView(final String id, final IDataProvider dataProvider, final PageParameters pp) {
                super(id, dataProvider);
                this.pp = pp;
                if (pp.getString(id) != null) {
                        int pageNum = getPageNumber(pp.getString(id));
                        if(pageNum != -1 && pageNum >= 0 && pageNum <= getPageCount()) {
                                setCurrentPage(getPageNumber(pp.getString(id)));
                        }
                }
        }

        public StatelessDataView(final String id, final IDataProvider dataProvider, final int itemsPerPage,
                        final PageParameters pp) {
                super(id, dataProvider, itemsPerPage);
                this.pp = pp;
                if (pp.getString(id) != null) {
                        int pageNum = getPageNumber(pp.getString(id));
                        if(pageNum != -1 && pageNum >= 0 && pageNum <= getPageCount()) {
                                setCurrentPage(getPageNumber(pp.getString(id)));
                        }
                }
        }

}

The paging navigator

package com.base.components;

import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.navigation.paging.IPageable;
import org.apache.wicket.markup.html.navigation.paging.IPagingLabelProvider;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigation;

public class StatelessSimplePagingNavigator extends SimplePagingNavigator {
        private static final long serialVersionUID = 1L;
        private Class clazz;
        private PageParameters pp;
        private int current_page;
        private int page_count;
        private String pageable_id;
        private String anchor = null;

        @Override
        protected boolean getStatelessHint() {
                return true;
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize) {
                this(id, clazz, pp, pageable, viewsize, false);
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize, final boolean anchorSelf) {
                super(id, pageable, viewsize, anchorSelf);
                this.clazz = clazz;
                this.pp = pp;
                this.current_page = pageable.getCurrentPage();
                this.page_count = pageable.getPageCount();
                this.pageable_id = ((Component) pageable).getId();
        }

        public StatelessSimplePagingNavigator(final String id, final Class clazz, final PageParameters pp,
                        final IPageable pageable, final int viewsize, final String anchor) {
                super(id, pageable, viewsize, false);
                this.clazz = clazz;
                this.pp = pp;
                this.current_page = pageable.getCurrentPage();
                this.page_count = pageable.getPageCount();
                this.pageable_id = ((Component) pageable).getId();
                this.anchor = anchor;
        }

        @Override
        protected void onBeforeRender() {
                super.onBeforeRender();
                addOrReplace(newStatelessPagingNavigationLink("next", pageable_id, current_page, 1));
                addOrReplace(newStatelessPagingNavigationLink("prev", pageable_id, current_page, -1));
        }

        @Override
        protected PagingNavigation newNavigation(final IPageable pageable, final IPagingLabelProvider labelProvider) {
                PagingNavigation pg = new PagingNavigation("navigation", pageable, labelProvider) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected Link newPagingNavigationLink(final String id, final IPageable pageable, final int pageIndex) {
                                Component c = (Component) pageable;
                                // PageParameters p1 = new PageParameters();
                                // p1.putAll(pp);
                                pp.put(c.getId(), String.valueOf(pageIndex));
                                BookmarkablePageLink<Object> lnk = new BookmarkablePageLink<Object>(id, clazz, pp) {
                                        private static final long serialVersionUID = 1L;

                                        @Override
                                        protected void onComponentTag(final ComponentTag arg0) {
                                                super.onComponentTag(arg0);
                                                if (anchor != null) {
                                                        if(arg0.getString("href") != null) {
                                                                String href = arg0.getString("href").toString();
                                                                String atag = anchor.contains("#") ? anchor : "#" + anchor;
                                                                arg0.put("href", href + atag);
                                                        }
                                                }
                                        }

                                        @Override
                                        public boolean isEnabled() {
                                                return (current_page != pageIndex);
                                        }
                                };
                                if (isAnchorSelf()) {
                                        lnk.setAnchor(StatelessSimplePagingNavigator.this);
                                }
                                return lnk;
                        }
                };
                pg.setViewSize(getViewsize());
                return pg;
        }

        @Override
        protected Link<Object> newPagingNavigationIncrementLink(final String id, final IPageable pageable, final int increment) {
                Component c = (Component) pageable;
                final int page = Integer.valueOf(pp.getString(c.getId()));
                pp.put(c.getId(), String.valueOf(page + increment));
                return new BookmarkablePageLink<Object>(id, clazz, pp) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        public boolean isVisible() {
                                return (page + increment) >= 0;
                        }
                };
        }

        protected Link<Object> newStatelessPagingNavigationLink(final String id, final String pageableId, final int currentPage,
                        final int increment) {
                // PageParameters p1 = new PageParameters();
                // p1.putAll(pp);
                pp.put(pageableId, String.valueOf(currentPage + increment));
                return new BookmarkablePageLink<Object>(id, clazz, pp) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        public boolean isVisible() {
                                return (currentPage + increment) < page_count && (currentPage + increment) >= 0;
                        }
                };
        }

        @Override
        protected Link<Object> newPagingNavigationLink(final String id, final IPageable pageable, final int pageNumber) {
                Component c = (Component) pageable;
                // PageParameters p1 = new PageParameters();
                // p1.putAll(pp);
                pp.put(c.getId(), String.valueOf(pageNumber));
                return new BookmarkablePageLink(id, clazz, pp);
        }

}

Which inherits from

package com.base.components;

import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.html.navigation.paging.IPageable;
import org.apache.wicket.markup.html.navigation.paging.IPagingLabelProvider;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigation;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigator;

public class SimplePagingNavigator extends PagingNavigator {
        private static final long serialVersionUID = 1L;

        private int viewsize = 0;
        private boolean anchorSelf = false;

        public SimplePagingNavigator(final String id, final IPageable pageable, final int viewsize) {
                this(id, pageable, viewsize, false);
        }

        public SimplePagingNavigator(final String id, final IPageable pageable, final int viewsize, final boolean anchorSelf) {
                super(id, pageable);
                setOutputMarkupId(true);
                this.setViewsize(viewsize);
                this.setAnchorSelf(anchorSelf);
        }

        @Override
        protected void onBeforeRender() {
                if (get("navigation") != null) {
                        remove("navigation");
                }
                if (get("prev") != null) {
                        remove("prev");
                }
                if (get("next") != null) {
                        remove("next");
                }
                super.onBeforeRender();
                if (get("first") != null) {
                        remove("first");
                }
                if (get("last") != null) {
                        remove("last");
                }
                if (getViewsize() != 0) {
                        getPagingNavigation().setViewSize(getViewsize());
                }
        }

        @Override
        protected PagingNavigation newNavigation(final IPageable pageable, final IPagingLabelProvider labelProvider) {
                PagingNavigation pg = new PagingNavigation("navigation", pageable, labelProvider) {
                        private static final long serialVersionUID = 1L;

                        @Override
                        protected Link newPagingNavigationLink(final String id, final IPageable pageable, final int pageIndex) {
                                Link lnk = (Link) super.newPagingNavigationLink(id, pageable, pageIndex);
                                if (isAnchorSelf()) {
                                        lnk.setAnchor(SimplePagingNavigator.this);
                                }
                                return lnk;
                        }
                };
                pg.setViewSize(getViewsize());
                return pg;
        }

        @Override
        public boolean isVisible() {
                if (getPageable() != null) {
                        return (getPageable().getPageCount() > 1);
                }
                return true;
        }

        public void setAnchorSelf(final boolean anchorSelf) {
                this.anchorSelf = anchorSelf;
        }

        public boolean isAnchorSelf() {
                return anchorSelf;
        }

        public void setViewsize(final int viewsize) {
                this.viewsize = viewsize;
        }

        public int getViewsize() {
                return viewsize;
        }

}

<wicket:panel>
        <a wicket:id="prev">Previous</a>
    <span wicket:id="navigation">
                  <a wicket:id="pageLink" href="#"><span wicket:id="pageNumber">5</span></a>
    </span>
    <a wicket:id="next">Next</a>
  </wicket:panel>
Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with ,

I have dumped varnish as our primary cache due to multiple failures of service. I have tried to make it work but varnish kept insisting on producing 503 XID backend failures on perfectly healthy backends. I have tried doing all types of crazy configuration hacks such as forcing varnish to retry backends via a round-robin director. It did not work out all too well since the round trip added latency when varnish had to re-fetch the document multiple times. The final straw that broke the camel’s back was when varnish configured for a 256mb malloc store grew to an astonishing size of 780mb+ RSS.

I have switched to squid-3 and so far it has been stable and fast. I will later post a matching squid configuration to the one below that does the same thing.

Squid-3 will require this patch for it to compile on Solaris.

Varnish on Solaris is a dud.

List of failures

1. Producing 503 responses for perfectly healthy backends. Backend never even gets contacted.
2. Growing to a crazy size when using the malloc implementation.
3. Segfaulting every hour on the hour with the newest trunk r4080+

Here is the configuration I have used. Feel free to use it if varnish works for you.

#
# This is a basic VCL configuration file for varnish.  See the vcl(7)
# man page for details on VCL syntax and semantics.
#
# $Id: default.vcl 1818 2007-08-09 12:12:12Z des $
#

# Default backend definition.  Set this to point to your content
# server.

 # my wonderful re-try hack, that kinda works.
 director www_dir round-robin {
     { .backend = { .connect_timeout = 2s; .host="127.0.0.1"; .port="8001"; }  }
     { .backend = { .connect_timeout = 2s; .host="127.0.0.1"; .port="8001"; }  }
     { .backend = { .connect_timeout = 2s; .host="127.0.0.1"; .port="8001"; }  }
     { .backend = { .connect_timeout = 2s; .host="127.0.0.1"; .port="8001"; }  }
 }

#backend default { .host = "127.0.0.1"; .port = "8089"; .connect_timeout = 2s; }

sub vcl_recv {
 remove req.http.X-Forwarded-For;
 set req.http.X-Forwarded-For = client.ip;
 set req.grace = 2m;

    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpeg|jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|ico|swf|flv|dmg)") {
            # No point in compressing these
            remove req.http.Accept-Encoding;
        } elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elsif (req.http.Accept-Encoding ~ "deflate") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            # unkown algorithm
            remove req.http.Accept-Encoding;
        }
    }

# don't trust MSIE6
# if (req.http.user-agent ~ "MSIE [1-6]\.") {
#     remove req.http.Accept-Encoding;
# }

 if (req.http.host == "jira.fabulously40.com") {
   pipe;
 }

 if (req.request == "GET" || req.request == "HEAD") {
	if ( req.url ~ "\.(xml|gif|jpg|swf|css|png|jpeg|tiff|tif|svg|ico|pdf|ico|swf)") {
		remove req.http.cookie;
		lookup;
	}
	# avoid caching jsps
	if ( req.url ~ "\.js([^p]|$)" ) {
		remove req.http.cookie;
		lookup;
	}
 }

 # don't bother caching large files
 if(req.url ~ "\.(mp3|flv|mov|mp4|mpg|mpeg|avi|dmg)") {
     pipe;
 }

 if (req.request != "GET" && req.request != "HEAD") {
     pipe;
 }

 if (req.request == "POST") {
     pipe;
 }

 if (req.http.Expect || req.http.Authorization || req.http.Authenticate || req.http.WWW-Authenticate) {
    pipe;
 }

 # pipe pages with these cookies set
 if (req.http.cookie && req.http.cookie ~ "_.*_session=") {
     pipe;
 }
 if (req.http.cookie && req.http.cookie ~ "JSESSIONID=") {
     pipe;
 }
 if (req.http.cookie && req.http.cookie ~ "PHPSESSID=") {
     pipe;
 }
 if (req.http.cookie && req.http.cookie ~ "wordpress_logged_in") {
     pipe;
 }

 lookup;
}

sub vcl_error {
	# retry on errors
    if (obj.status == 503) {
        if ( req.restarts < 12 ) {
             restart;
         }
     }
}

sub vcl_fetch {

	# don't cache when these cookies are in place
	if(beresp.http.Location || beresp.http.WWW-Authenticate) {
	    pass;
	}
	if(beresp.http.cookie && beresp.http.cookie ~ "JSESSIONID=") {
	    pass;
	}
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "JSESSIONID=") {
	    pass;
	}
	if(beresp.http.cookie && beresp.http.cookie ~ "_.*_session=") {
	    pass;
	}
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "_.*_session=") {
	    pass;
	}
	if(beresp.http.cookie && beresp.http.cookie ~ "PHPSESSID=") {
	    pass;
	}
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "PHPSESSID=") {
	    pass;
	}
	if(beresp.http.cookie && beresp.http.cookie ~ "wordpress_logged_in") {
	    pass;
	}
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "wordpress_logged_in") {
	    pass;
	}
	if(beresp.http.Cache-Control && beresp.http.Cache-Control ~ "no-cache") {
	    pass;
	}
	if(beresp.http.Pragma && beresp.http.Pragma ~ "no-cache") {
	    pass;
	}

# avoid defaults since we *want* pages cached with cookies
#	if (!beresp.cacheable) {
#	    pass;
#	}
#	if (beresp.http.Set-Cookie) {
#		pass;
#	}

	#cache for 30 minutes..
	if((beresp.http.Cache-Control !~ "max-age" || beresp.http.Cache-Control !~ "s-maxage") && beresp.ttl < 1800s) {
		set beresp.ttl = 1800s;
	}
	set beresp.grace = 2m;

	# anonymous users get 10 min delay
	if(beresp.http.Content-Type && beresp.http.Content-Type ~ "html" && (beresp.http.Cache-Control !~ "max-age" ||  beresp.http.Cache-Control !~ "s-maxage")) {
	    set beresp.ttl = 600s;
	}

	# remove server affinity cookie from cached pages.
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "X-SERVERID=") {
	    remove beresp.http.Set-Cookie;
	}
	if(beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "SERVERID=") {
	    remove beresp.http.Set-Cookie;
	}
	if(beresp.http.X-Backend) {
	    remove beresp.http.X-Backend;
	}

	deliver;
}
Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with , , ,

I am about to run out the house for the Labor Day weekend, quickie post on my new coding url strategy.

MixedParamHybridUrlCodingStrategy lets you keep stateful multi-pagemap URLs clean while using mixed parameters.

Example…

mount(new MixedParamHybridUrlCodingStrategy("/questions",QuestionsPage.class,false,new String[]{"cat"}));

This will mount “/questions/stupid-category” and convert it to…

new PageParameters("cat","stupid-category");
package com.base.target.coding;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.IRequestTarget;
import org.apache.wicket.PageParameters;
import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
import org.apache.wicket.request.target.coding.HybridUrlCodingStrategy;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.apache.wicket.util.value.ValueMap;

public class MixedParamHybridUrlCodingStrategy extends HybridUrlCodingStrategy {
        private final String[] parameterNames;

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass) {
                super(mountPath, pageClass);
                this.parameterNames = new String[] {};
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final boolean refresh) {
                super(mountPath,pageClass,refresh);
                this.parameterNames = new String[] {};
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final String[] params) {
                super(mountPath,pageClass,false);
                this.parameterNames = params;
        }

        public MixedParamHybridUrlCodingStrategy(final String mountPath, final Class pageClass,final boolean refresh,final String[] params) {
                super(mountPath,pageClass,refresh);
                this.parameterNames = params;
        }

        @Override
        protected void appendParameters(final AppendingStringBuffer url, final Map parameters)
        {

                Set parameterNamesToAdd = new HashSet(parameters.keySet());
                // Find index of last specified parameter
                boolean foundParameter = false;
                int lastSpecifiedParameter = parameterNames.length;
                while (lastSpecifiedParameter != 0 && !foundParameter)
                {
                        foundParameter = parameters.containsKey(parameterNames[–lastSpecifiedParameter]);
                }

                if (foundParameter)
                {
                        for (int i = 0; i <= lastSpecifiedParameter; i++)
                        {
                                String parameterName = parameterNames[i];
                                final Object param = parameters.get(parameterName);
                                String value = param instanceof String[] ? ((String[])param)[0] : (String)param;
                                if (value == null)
                                {
                                        value = "";
                                }
                                if (!url.endsWith("/"))
                                {
                                        url.append("/");
                                }
                                url.append(urlEncodePathComponent(value));
                                parameterNamesToAdd.remove(parameterName);
                        }
                }

                if (!parameterNamesToAdd.isEmpty())
                {
                        boolean first = true;
                        final Iterator iterator = parameterNamesToAdd.iterator();
                        while (iterator.hasNext())
                        {
                                url.append(first ? ‘?’ : ‘&’);
                                String parameterName = (String)iterator.next();
                                final Object param = parameters.get(parameterName);
                                String value = param instanceof String[] ? ((String[])param)[0] : (String)param;
                                url.append(urlEncodeQueryComponent(parameterName)).append("=").append(
                                                urlEncodeQueryComponent(value));
                                first = false;
                        }
                }

                String pageMap = (String)parameters.get(WebRequestCodingStrategy.PAGEMAP);
                if (pageMap != null)
                {
                        pageMap = WebRequestCodingStrategy.encodePageMapName(pageMap);
                        if (!url.endsWith("/"))
                        {
                                url.append("/");
                        }
                        url.append(WebRequestCodingStrategy.PAGEMAP).append("/").append(
                                        urlEncodePathComponent(pageMap));
                }
        }

        @Override
        public CharSequence encode(final IRequestTarget requestTarget) {
                String url = String.valueOf(super.encode(requestTarget));
                if(url.contains(".wicket")) {
                        // Rewrite URL from..  /foo?bar=5.wicket-xxx to /foo.wicket-xxx?bar=5
                        return url.replaceAll("(.*)\\?(.*)\\.(wicket-[0-9]+)$", "$1.$3?$2");
                }
                return url;
        }

        @Override
        protected ValueMap decodeParameters(final String urlFragment, final Map urlParameters)
        {

                PageParameters params = new PageParameters();
                if(urlFragment == null) {
                        return params;
                }

                // Add all url parameters and normalize
                for(Iterator<Map.Entry> itr = urlParameters.entrySet().iterator();itr.hasNext();) {
                        Map.Entry<String,String[]> e = itr.next();
                        if(e.getValue() != null) {
                                String val = (e.getValue())[0];
                                if(val != null) {
                                        if(val.contains(".wicket")) {
                                                val = val.substring(0,val.indexOf(".wicket"));
                                                urlParameters.put(e.getKey(), val);
                                        }
                                }
                        }
                }
                params.putAll(urlParameters);

                String urlPath = urlFragment;
                if (urlPath.startsWith("/"))
                {
                        urlPath = urlPath.substring(1);
                }

                if (urlPath.length() > 0)
                {
                        String[] pathParts = urlPath.split("/");
                        if (pathParts.length > parameterNames.length)
                        {
                                throw new IllegalArgumentException(
                                "Too many path parts, please provide sufficient number of path parameter names");
                        }

                        for (int i = 0; i < pathParts.length; i++)
                        {
                                if(pathParts[i].contains(".wicket")) {
                                        pathParts[i].substring(0,pathParts[i].indexOf(".wicket"));
                                }
                                if (WebRequestCodingStrategy.PAGEMAP.equals(pathParts[i]))
                                {
                                        params.put(WebRequestCodingStrategy.PAGEMAP,
                                                        WebRequestCodingStrategy.decodePageMapName(urlDecodePathComponent(pathParts[i])));
                                }
                                params.put(parameterNames[i], urlDecodePathComponent(pathParts[i]));
                        }

                }
                return params;
        }

}

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with ,

This is a late night post, so I am just going to make it short. This patch lets you use QueuedThreadPool with the Grizzly Connector. This is a monkey patch, getMaxThreads() should be moved up into the Thread Interface.

--- GrizzlyConnection-old.java Sat May 2 01:08:02 2009 +++ GrizzlyConnector.java Sat May 2 00:56:37 2009 @@ -51,6 +51,8 @@ import org.mortbay.jetty.webapp.WebAppContext; import org.mortbay.log.Log; import org.mortbay.thread.BoundedThreadPool; +import org.mortbay.thread.QueuedThreadPool; +import org.mortbay.thread.ThreadPool; /* ------------------------------------------------------------------------------- */ /** @@ -178,8 +180,13 @@ controller.setProtocolChainInstanceHandler(instanceHandler); Pipeline pipeline = new DefaultPipeline(); - pipeline.setMaxThreads( - ((BoundedThreadPool)getServer().getThreadPool()).getMaxThreads()); + if(getServer().getThreadPool() instanceof BoundedThreadPool) { + pipeline.setMaxThreads( + ((BoundedThreadPool)getServer().getThreadPool()).getMaxThreads()); + } else { + pipeline.setMaxThreads( + ((QueuedThreadPool)getServer().getThreadPool()).getMaxThreads()); + } controller.setPipeline(pipeline); }
Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with , ,

There seems to be some interest Catalyst vs Rails vs Django benchmark. The older benchmark is quite old, it was done in 2007. A lot has changed since then. I am re-running the numbers once again to see what has changed. This time around the hardware is faster and the benchmark is slightly more simple. I am just stress testing the controller response performance of the two frameworks.

Benchmark System:
Quad Core Xeon x5355 @ 2.66GHz,8 Gigs Ram,OpenSolaris SNV98

Quick Summary:
Catalyst 5.8/Perl 5.10: 611.78req/sec (Single Process,bsdmalloc)
Catalyst 5.8/Perl 5.10: 1485.53req/sec (Multi Process,bsdmalloc)
Rails 2.3.2/MRI Ruby 1.8.7: 259.93req/sec (Single Process,bsdmalloc)
Rails 2.3.2/JRuby 1.3-dev: 311.71req/sec (Single-Threaded,bsdmalloc)
Rails 2.3.2/JRuby 1.3-dev: 992.32req/sec (Multi-Threaded,libumem)
Rails 2.3.2/MRI Ruby 1.9.1: 603.92req/sec (Single Process,bsdmalloc)

Jump to conclusion….

Catalyst 5.8 / Perl 5.10
Compiled: SUNCC -xO5 -xipo -fast -xtarget=native

# ab -n1000 -c100 http://somedomain:3000/ This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright 2006 The Apache Software Foundation, http://www.apache.org/ Benchmarking localhost (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Finished 1000 requests Server Software: Server Hostname: somedomain Server Port: 3000 Document Path: / Document Length: 11 bytes Concurrency Level: 100 Time taken for tests: 0.673159 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 159000 bytes HTML transferred: 11000 bytes Requests per second: 1485.53 [#/sec] (mean) Time per request: 67.316 [ms] (mean) Time per request: 0.673 [ms] (mean, across all concurrent requests) Transfer rate: 230.26 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 2.3 0 35 Processing: 15 62 10.8 62 103 Waiting: 15 59 11.6 61 98 Total: 15 62 10.6 63 103 Percentage of the requests served within a certain time (ms) 50% 63 66% 66 75% 69 80% 71 90% 74 95% 76 98% 86 99% 91 100% 103 (longest request)

Rails 2.3.2 / Ruby 1.8.7
Compiled: SUNCC -xO5 -xipo -fast -xtarget=native

# ab -n1000 -c100 http://somedomain:3000/main/index This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking somedomain (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: Mongrel Server Hostname: somedomain Server Port: 3000 Document Path: /main/index Document Length: 11 bytes Concurrency Level: 100 Time taken for tests: 3.847 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 290003 bytes HTML transferred: 11000 bytes Requests per second: 259.93 [#/sec] (mean) Time per request: 384.718 [ms] (mean) Time per request: 3.847 [ms] (mean, across all concurrent requests) Transfer rate: 73.61 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 1.1 0 16 Processing: 10 369 65.2 389 428 Waiting: 9 368 65.3 388 427 Total: 10 369 65.2 390 428 Percentage of the requests served within a certain time (ms) 50% 390 66% 396 75% 398 80% 400 90% 404 95% 407 98% 413 99% 417 100% 428 (longest request)

Rails 2.3.2 / JRuby 1.3-dev build 6586 (Multi-Threaded), libumem
Platform: JDK7 B56

# ab -n1000 -c100 http://somedomain.com:3000/main/index
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking somedomain.com (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests

Server Software:
Server Hostname:        somedomain.com
Server Port:            3000

Document Path:          /main/index
Document Length:        11 bytes

Concurrency Level:      100
Time taken for tests:   1.008 seconds
Complete requests:      1000
Failed requests:        1
   (Connect: 0, Receive: 0, Length: 1, Exceptions: 0)
Write errors:           0
Non-2xx responses:      1
Total transferred:      253875 bytes
HTML transferred:       11936 bytes
Requests per second:    992.32 [#/sec] (mean)
Time per request:       100.773 [ms] (mean)
Time per request:       1.008 [ms] (mean, across all concurrent requests)
Transfer rate:          246.02 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.7      0      13
Processing:    10   79  18.8     80     122
Waiting:        9   79  18.8     79     122
Total:         10   80  18.9     80     122

Percentage of the requests served within a certain time (ms)
  50%     80
  66%     88
  75%     94
  80%     98
  90%    102
  95%    108
  98%    113
  99%    114
 100%    122 (longest request)

Rails 2.3.2 / JRuby 1.3-dev build 6586 (Single-Threaded)
Platform: JDK7 B56

# ab -n1000 -c100 http://somedomain:3000/main/index This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking somedomain (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: Server Hostname: somedomain Server Port: 3000 Document Path: /main/index Document Length: 11 bytes Concurrency Level: 100 Time taken for tests: 3.208 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 253000 bytes HTML transferred: 11000 bytes Requests per second: 311.71 [#/sec] (mean) Time per request: 320.810 [ms] (mean) Time per request: 3.208 [ms] (mean, across all concurrent requests) Transfer rate: 77.01 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 2.5 0 31 Processing: 37 304 56.4 318 350 Waiting: 36 304 56.5 318 349 Total: 37 305 56.5 318 352 Percentage of the requests served within a certain time (ms) 50% 318 66% 326 75% 330 80% 332 90% 336 95% 341 98% 345 99% 348 100% 352 (longest request)

Rails 2.3.2 / Ruby 1.9.1
Compiled: GCC -O3 -fomit-frame-pointer (SunCC failed to compile)

# ab -n1000 -c100 http://somedomain:3000/main/index This is ApacheBench, Version 2.3 <$Revision: 655654 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking somedomain (be patient) Completed 100 requests Completed 200 requests Completed 300 requests Completed 400 requests Completed 500 requests Completed 600 requests Completed 700 requests Completed 800 requests Completed 900 requests Completed 1000 requests Finished 1000 requests Server Software: thin Server Hostname: somedomain Server Port: 3000 Document Path: /main/index Document Length: 11 bytes Concurrency Level: 100 Time taken for tests: 1.656 seconds Complete requests: 1000 Failed requests: 0 Write errors: 0 Total transferred: 267001 bytes HTML transferred: 11000 bytes Requests per second: 603.92 [#/sec] (mean) Time per request: 165.585 [ms] (mean) Time per request: 1.656 [ms] (mean, across all concurrent requests) Transfer rate: 157.47 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.6 0 8 Processing: 28 160 39.0 187 222 Waiting: 13 145 38.5 143 209 Total: 28 160 39.0 187 222 Percentage of the requests served within a certain time (ms) 50% 187 66% 190 75% 191 80% 191 90% 196 95% 201 98% 221 99% 221 100% 222 (longest request)

Conclusion

Seems like Catalyst has the edge in controller performance compared to Rails on MRI Ruby 1.8.7. Catalyst’s controller processing is 135% faster than Rails in single process performance and 471% faster as a forking multi process. It is nice to see that the Catalyst team addressed the controller performance short comings of the earlier versions of Catalyst. Like any benchmark take it with a grain of salt. In a real application your data access layer will most likely be the bottle neck.

Rails 2.3.2 under JRuby with threading enabled ran 283% faster than with MRI Ruby 1.8.7. I am anxiously waiting on JDK7 B57 with invoke dynamic support, this should help push JRuby’s performance even further. I guess I know what deployment option I will choose when deploying Rails.

Pick your poison, both frameworks provide excellent controller response performance. Keep in mind scaling is all about architecture and not how fast your controller’s responses are. That said, having an efficient framework does help ;-)

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with , , , ,

I have recently blogged about swapping malloc implementations for the JVM to help boost multi-threaded performance. Well there is yet another malloc implementation that solaris comes with that is optimized for single threaded performance; bsdmalloc. I just recently switched our perl interpreter to use bsdmalloc and got 33% faster performance with our perlbal proxy.

You can try out multiple malloc implementations by setting LD_PRELOAD environment variable.

LD_PRELOAD="/usr/lib/libbsdmalloc.so" perl somecode.pl

So here is the rule of thumb for which malloc implementation to use for your application.

libumem = For multithreaded applications. umem avoids thread heap contention and is highly optimized for multi-threaded applications.

bsdmalloc = For single threaded applications. PHP/Perl/Python and Ruby will fall into this category.

Applying the right malloc implementation to your resource intensive application can see a nice performance benefit.

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
Tagged with ,

I wrote a quick micro benchmark to test out ruby threads. Apparently ruby can’t make use of multiple CPUs with it’s threading implementation. I guess you have to resort to forking to scale up to multiple cpu cores while using mri ruby. However, there is an alternative solution, just use JRuby. JRuby utilizes all cores when running the benchmark.

Ruby 1.8.7 - Compiled with SunCC SSX0903 (-xO5 -fast -xipo) Total number of insane floating point divisions in 10 seconds is 5969107
Ruby 1.9.1 / Compiled with GCC 4.3.2 (-O3 -fomit-frame-pointer) Total number of insane floating point divisions in 10 seconds is 8596894

Ran as: jruby –fast cpuMax.rb
177% increase in performance

JRuby 1.3-dev / JDK7 b56 Total number of insane floating point divisions in 10 seconds is 15915896

Ran as: jruby –fast -J-Djruby.compile.mode=JIT -J -Djruby.jit.threshold=0 -J-server cpuMax.rb
374% increase in performance

JRuby 1.3-dev / JDK7 b56 Total number of insane floating point divisions in 10 seconds is 28334441

Looking at mpstat, I can see the MRI ruby implementation is not utilizing all 4 cores.

Ruby 1.8.7 MRI

CPU minf mjf xcal  intr ithr  csw icsw migr smtx  srw syscl  usr sys  wt idl
  0  828   0   13   428  204  487   26   93   16    0  1903    4   5   0  91
  1 2682   0    3    39    2  280   32   81   12    2  1189   13   2   0  85
  2 1902   0    0    34   11  259   16   57   13    0  1094   11   3   0  86
  3 1017   0    3   192  150  111   34   38    8    0   676   92   2   0   6
CPU minf mjf xcal  intr ithr  csw icsw migr smtx  srw syscl  usr sys  wt idl
  0    2   0    5   422  205  388   11   75    5    0   627    4   1   0  95
  1  161   0   13    20    2  196   11   61    8    0  1405    2   2   0  96
  2  292   0    6    32   15  272   10   57    7    0   700    2   1   0  97
  3    0   0    0   108   65   74   35   28    3    0   346   99   0   0   1

Now here is the JRuby implementation.

CPU minf mjf xcal  intr ithr  csw icsw migr smtx  srw syscl  usr sys  wt idl
  0  229   0  202   755  193 1977  365  329   94    1  2869   90   2   0   8
  1  328   0   86   371    1 1817  226  303  125    0  2809   86   2   0  12
  2  294   0  128   326    0 1771  248  287  109    0  2290   88   2   0  10
  3  320   0  172   402   62 1848  246  241  116    0  2238   86   3   0  11
CPU minf mjf xcal  intr ithr  csw icsw migr smtx  srw syscl  usr sys  wt idl
  0  317   0  297   700  192 2047  323  341  136    1  2819   89   2   0   9
  1  320   0   61   279    2 1611  134  195  130    0  1960   85   2   0  13
  2  288   0  235   379    0 1941  291  299  115    0  2462   87   2   0  11
  3  308   0   78   316   43 1688  142  159  104    0  1706   85   2   0  13

I think I will stick to JRuby for production use.

#!/usr/bin/ruby

require ‘thread’
threads = []
counter = 0
mutex = Mutex.new

4.times do
     threads << Thread.new {
        x=0
        y=0
        time=Time.new

        while 1 do
                if Time.new - time >= 10 then
                        break
                else
                        x=1.00/24000000000.001
                        y+=1
                end
        end
        mutex.synchronize { counter+=y.to_i }
    }
end
threads.each { |t| t.join }

puts "Total number of insane floating point divisions in 10 seconds is "+counter.to_s

Share and Enjoy:
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google
  • Design Float
  • description
  • Reddit
  • Slashdot
  • TwitThis
  • Tweet Bird

  • Recent Posts

  • Recent Comments

  • Archives

  • Categories

  • Tags

  • Meta