Long Polling – tutorial, part 2

how to mimic “server push” with “client pull”?

As You could already see at wikipedia, long pooling means just requests (in our case ajax requests) which can wait quite a long time for server response. Server doesn’t send a single bit of information until something interesting (for the request) appears.

There are two possible results:

  1. something appears (in our case – new post in database), so server sends this info, request hits a success and do something with received data
  2. nothing appears for some time, and request time-outs.

Both possible paths ends the same – request starts another one to wait for something to happen.

You probably already have some ideas how to solve it. If so – please try it (and if it differs from mine solution – I’d love to see it, please post it in comments.)

There are some pitfalls, though. I’ll show most of them.

assumptions

Thing to remember at this point is where request are always going. So it’s not a good way to send html code back and forth. XHttpRequests should expect JSON responses (cleaned of white-spaces if possible).
Another conclusion is: if there’s nothing to send from server it’s better for xhr to timeout than receive response “nothing new”. Even though this is not elegant solution (time-out is still some kind of error).

We should decide how much of information should be send to server in order to get new messages in the thread. The minimum is the thread’s root id and last message id (biggest one). With that information it is possible to get all messages that are in the subtree which is rooted in particular id and their id is bigger than the very last one. But in that case we at least need to make two queries: one to get root lft and rght values, second to get all his children (which ids are between roots lft and rght).

The second approach is to send all messages ids in thread and just get all messages that have parent_id in that set.

My approach is that one should use cleaner solution (1st one) because we don’t have enough data to decide which one is faster (when the application is in production mode You could do some research. Of course we could make some fancy simulations but it is not the point of this tutorial).

So create an action that takes two parameters (tree root id and last message id in the thread) and sending back new messages (if any) or do nothing (not even sending empty response).

//controllers/posts_controller.php
	function get_new($rootId, $lastMessageId){
		$root = $this->Post->read(null, $rootId);
		
		$posts = array();
		
		while(empty($posts)){
			$posts = $this->Post->find(
				"all",
				array(
					"conditions"=>array(
						"and"=> array(
							"Post.lft > " => $root["Post"]["lft"],
							"Post.lft < " => $root["Post"]["rght"],
							"Post.id >" => $lastMessageId
						)
					)
				)
			);
			if(empty($posts)){
				sleep(2); // sleep for two seconds
			}
		}
		$this->set("posts", $posts);
		$this->render("get_new", false);
	}

// views/posts/get_new.ctp

You can try it out in Your browser. But if You choose /posts/get_new/1/8 when 8 is the very last message in this thread it would choke your server up ;) For testing use parameters where at least one message will appear (in the example: /posts/get_new/1/7).

Server issue

This clash on server is connected to sessions. Apache waits to the end of every scripts until it saves session data, at the end it implicitly closes session. For some reason Apache cannot release thread for this request if session is not closed (even if You hit cancel in Your browser, what triggers kill for the processing request in the most of the cases). What you need is to add session_commit(); at the very beginning of actions which are long polled. (if You need to write to session – do it as fast as it’s possible before while loop).

//controllers/posts_controller.php
	function get_new($rootId, $lastMessageId){
		session_commit();
		$root = $this->Post->read(null, $rootId);
		//...

Now we have nice action which gives us JSON string with new messages. Lets load them to our thread.

First of all we’ll need root id and last post id stored in Thread object. Root id is in the first element’s parent_id, last post we’ll find while generating thread tree.
Let’s add those properties to Thread class:

//webroot/js/thread.js
(function(window, document, udefined){
	window.Thread = function(_posts) {
		this.posts = _posts;
		this.rootId = false; //<==
		this.lastId = false; //<==
		this.extendJQuery();
		return this;	
	};
	
})(window, document);

Store root's and last message ids in createThread function:

//webroot/js/thread.js
Thread.prototype.createThread = function(){
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id; //<==
	var _this = this; //need to remember this, because in each method this means actual object from collection we iterate through
	$(this.posts).each(function(k, v){
		if($("#"+v.Post.parent_id).find("ul").length == 0){
			if($("#"+v.Post.parent_id).find("div.reply").length == 0){
				_this.createReplyDiv().appendTo("#"+v.Post.parent_id);
			}
	        $("
    ", {class: "posts children"}).appendTo("#"+v.Post.parent_id); } $("
  • ", { id: v.Post.id, innerHTML: "
    "+v.Post.message+"
    " , class: "post child" }). append(_this.createReplyDiv()). appendTo("#"+v.Post.parent_id+">ul"); if(v.Post.id>_this.lastId){ //<== _this.lastId = v.Post.id; } }); return this; };

Now I want to get data from get_new action, but my thread object have no idea where to get it from. Let's allow to pass endpoint url to constructor.

//webroot/js/thread.js
(function(window, document, udefined){
	window.Thread = function(_posts, _updateEndpoint) {//<==
		this.posts = _posts;
		this.rootId = false;
		this.lastId = false;
		this.updateEndpoint = _updateEndpoint; //<==
		this.extendJQuery();
		return this;	
	};
	
})(window, document);

update Thread object creation:

//views/layouts/default.ctp
		$(document).ready(function(){
			if(!window.posts) return false;
			window.thread = new Thread(
				posts, 
				"<?php echo $html->url(
							array(
							'controller'=>'posts', 
							'action'=>'get_new')
				);?>"
			).createThread();	
			console.log(thread);
		});

and create update() method. It should get JSON with new posts, and in case of either success or time-out Thread.update() should be called again:

//webroot/js/thread.js
Thread.prototype.update = function(){
	if(!this.updateEndpoint || !this.rootId || !this.lastId){
		alert("Thread corrupted");
		return false;
	}
	_this = this;
	$.ajax({
		url: this.updateEndpoint +"/"+ this.rootId+ "/" + this.lastId,
		dataType: 'json',
		timeout: 5000,
		success: function(){
			_this.update();
		},
		error: function(){
			_this.update();
		}
	});
	return true;
}

bug #1

I've found a bug after adding post with id=10 to the thread. It appears that I was storing lastId - the biggest id but in... dictionary order (check in JS: "10">"9" => false). Please patch last if statement in Thread.addPost() like that:

//...
		if(parseInt(v.Post.id)>_this.lastId){
			_this.lastId = parseInt(v.Post.id);
		}
}

Call newly created method in Thread.createThread():

Thread.prototype.createThread = function(){
	var _this = this;
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id;
	$(this.posts).each(function(k, v){
		if($("#"+v.Post.parent_id).find("ul").length == 0){
			if($("#"+v.Post.parent_id).find("div.reply").length == 0){
				_this.createReplyDiv().appendTo("#"+v.Post.parent_id);
			}
        	$("
    ", {class: "posts children"}).appendTo("#"+v.Post.parent_id); } $("
  • ", { id: v.Post.id, innerHTML: "
    "+v.Post.message+"
    ", class: "post child" }). append(_this.createReplyDiv()). appendTo("#"+v.Post.parent_id+">ul"); if(parseInt(v.Post.id)>_this.lastId){ _this.lastId = parseInt(v.Post.id); } }); this.update(); //<== return this; }

You can check now if it's trying to get new data again and again. Try to add new post in other browser and see what happens with request...
As You can see - there's ok response but from now on every request is getting the same response, and the thread isn't updated. We'll fix that now. First extract the method that creates post elements from createThread method - we will reuse it...

Thread.createThread should look like that:

Thread.prototype.createThread = function(){
	if(this.posts.length <1){
		return this;
	}
	this.rootId = this.lastId = posts[0].Post.parent_id;
	var _this = this; //need to remember this, because in each method this means actual object from collection we iterate through
	$(this.posts).each(function(k, v){
		_this.addPost(v.Post.id, v.Post.parent_id, v.Post.message); //<==
	});
	this.update();
	return this;
};

and our new method Thread.addPost:

Thread.prototype.addPost = function(id, parentId, message){
	if($("#"+parentId).find("ul").length == 0){
		if($("#"+parentId).find("div.reply").length == 0){
			this.createReplyDiv().appendTo("#"+parentId);
		}
        $("
    ", {class: "posts children"}).appendTo("#"+parentId); } $("
  • ", { id: id, innerHTML: "
    "+message+"
    " , class: "post child" }). append(this.createReplyDiv()). appendTo("#"+parentId+">ul"); if(id>this.lastId){ this.lastId = id; } }

bug #2

I encountered even more important bug - we caused a memory leak in our server (just wait long enough with get_new requests running) - after xhr time-out the script is not terminating.
I added a simple counter to get_new method:

//controllers/posts.php
	function get_new($rootId, $lastMessageId){
		//...
		$counter = 2; //<==
		while(empty($posts)){
			$posts = $this->Post->find(
				"all",
				array(/*...*/)
			);
			if($counter>5){ //<==
				break; //<==
			}else{ //<==
				$counter += 2; //<==
			} //<==
			if(empty($posts)){
				sleep(2); // sleep for two seconds
			}
		}
		$this->set("posts", $posts);
		$this->render("get_new", '');
	}

Why is it incremented by two? It's because there's 2 seconds sleep, so I'm checking if the script slept for at least 5 seconds altogether (5 seconds is the timeout of ajax request).

To finish - call extracted method in success callback:

//webroot/js/thread.js
Thread.prototype.update = function(){
	if(!this.updateEndpoint || !this.rootId || !this.lastId){
		alert("Thread corrupted");
		return false;
	}
	_this = this;
	$.ajax({
		url: this.updateEndpoint +"/"+ this.rootId+ "/" + this.lastId,
		dataType: 'json',
		timeout: 5000,
		success: function(data){ //<==
			$(data).each(function(k,v){
				_this.addPost(v.Post.id, v.Post.parent_id, v.Post.message); //<==
			});
			_this.update();
		},
		error: function(){
			_this.update();
		}
	});
	return true;
}

That's all. Of course with longPooling (and soon with web sockets) you can implement some more cool stuff. You can display information about somebody typing a reply right now in some particular place.

There are some details to be polished, maybe newly added post should attract attention (blink?), but it's not the case of this tutorial.

I hope you enjoyed it, please let me know what you think about it.

Share Button

Long Polling – tutorial, part 0

I was encouraged by good response on my first tutorial, so started to work on another. Unfortunately I didn’t finish it before my vacation, so I almost lost track of what I was doing. So I decided to ship it to you in parts, so by doing every part before publishing I’ll be back on track, and find more mistakes, so hopefully it will be error-free. Enjoy.

What’s long pooling

Long pooling is one of the method that mimics “server push” behavior on stateless HTTP protocol (which is based on “server pull” principle) . Even though there are web sockets coming, I think it’s a good idea to get familiar with that kind of technique.

We’ll try to create just a bit of simple idea. Combination of chat and bulletin board, where You can post Your messages and can see other users messages appearing in “real time”.

We’ll do it with cakePHP on the server side and http://jquery.com/ on the client side. I assume You are a little bit familiar with that tools – I wouldn’t explain how to configure database connection in cakePHP etc.

The only features on this system are

  • creating new discussion
  • replying in a discussion without reloading a page
  • getting users to see other people replies without reloading the page

on top of that we’ll keep in mind that we are on small budget, so we want our application to be as light for the server as possible (to do that we’ll push as much work to client side as we can).

Preparing project

Let’s get last stable version of cake to get started fast:

$ git clone http://github.com/cakephp/cakephp.git longPolling

Then let’s create a database for our discussions

CREATE TABLE `posts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `lft` int(11) DEFAULT NULL,
  `rght` int(11) DEFAULT NULL,
  `message` text COLLATE utf8_unicode_ci NOT NULL,
  `modified` datetime NOT NULL,
  UNIQUE KEY `id` (`id`),
  KEY `parent_id` (`parent_id`),
  KEY `lft` (`lft`),
  KEY `rght` (`rght`),
  KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

As You can see, we’ll use “Nested set model” which is implemented by Treebehavior. For maximum simplicity discussions don’t have topics and so, and to start discussion one should just post first message outside of any discussion.

Let’s bake model, controller and views for CRUD – this will be a foundation we’ll be working on. Just don’t mind validation and associations, neither admin routing – it’s not crucial to the topic. Add Tree behavior to Post model.

Cleanup

Now, we have some mess in our code already. `add` and `edit` views duplicate themselves, so I’ll remove `add` view and force PostsController::add() to render `edit` view:

//posts_controller.php 
	function add() {
		if (!empty($this->data)) {
			$this->Post->create();
			if ($this->Post->save($this->data)) {
				$this->Session->setFlash(__('The post has been saved', true));
				$this->redirect(array('action' => 'index'));
			} else {
				$this->Session->setFlash(__('The post could not be saved. Please, try again.', true));
			}
		}
		$this->render("edit");
	}

and, just to make it not look silly – surround code that generates “delete” link with one if statement:

		<?php if(!empty($this->data["Post"]["id"])): ?>
			<li>Html->link(__('Delete', true), array('action' => 'delete', $this->Form->value('Post.id')), null, sprintf(__('Are you sure you want to delete # %s?', true), $this->Form->value('Post.id'))); ?>
		<?php endif; ?>

To finally polish it – remove user_id, lft, rght, modified fields from the form (because there will be automatically filled).

Oh! I almost forgot, to be able to pick some of the existing topics as parent for message you are adding – create that method in PostsController and call it in add and edit actions:

//posts_controller.php
function _getParentPosts(){
	$this->set('parents', $this->Post->find("list"));
}

Now create at least one discussion, so we can start working on some stuff. If it’s not fun for You just run this query:

INSERT INTO `posts` VALUES (1,NULL,NULL,1,8,'first message in the topic.','0000-00-00 00:00:00'),
(2,1,NULL,2,5,'my very first reply to the very first message!','0000-00-00 00:00:00'),
(3,1,NULL,6,7,'I just have some opinion about first message, and will share it with you... soon.','0000-00-00 00:00:00'),
(4,2,NULL,3,4,'I think you shouldn\'t make off-topics here','0000-00-00 00:00:00');

Now we’re up and running, and ready for the fun stuff.

Share Button

Tutorial: dashboard web 2.0 dla leniwych…


czyli dobrych programistów ;)

W moim pierwszym tutorialu napiszę jak wydajnie korzystać z tego co daje Ci cakePHP i jQuery. Aktualnie cake opiera się na współpracy z innym frameworkiem javascript – prototype. Ale już w wersji 1.3 core cake’a ma współpracować z jquery. Już nie mogę się doczekać. Ten tutorial oparłem na wersji 1.3.0-beta dlatego, że przy okazji jego pisania chciałem rzucić okiem na kolejną wersję tego świetnego frameworka php. Jednak nie będę korzystał z żadnych helperów jquery, zatem większość z tych rad powinna pasować do wersji 1.2 (w razie problemów – chętnie służę pomocą).
Jest to mój pierwszy tutorial, więc proszę o wskazówki, komentarze i odrobinę wyrozumiałości.

Efekt jaki chcemy osiągnąć można obejrzeć jako demo oraz źródła na świetnym hostingu, który gorąco wszystkim webdeveloperom i ich klientom polecam: vipserv.org.

W tym tekście zakładam, ze znasz podstawy cakePHP i javascript. Czyli przynajmniej zrobiłeś sławny blog tutorial , wiesz jak mniej więcej używać narzędzia bake, oraz potrafisz przynajmniej zniknąć element na stronie i zmienić jego kolor przy pomocy czystego javascript’u.

Dashboard na zakładkach

Zacznijmy od przygotowania przykładowej aplikacji. Wypiekamy (cake bake) aplikację złożoną z dwóch modeli Thing i Item (każda tabela składa się z id i pola tekstowego name).

Następnie tworzymy dla niej zbiorczy widok (tzw. dashboard, metoda all())

// /app/controllers/items_controller.php
class ItemsController extends AppController {
 //...
 function all() {}
}
// /app/views/items/all.ctp
<?php echo $this->requestAction(
            array(
               "controller"=> "items",
               "action"=> "index"),
            array("return")
      ); ?>
<?php echo $this->requestAction(
            array(
               "controller"=> "things",
               "action"=> "index"),
            array("return")
      ); ?>

Na razie nie przejmujcie się kwestią requestAction i wydajności. Zazwyczaj efekty requestAction należy umieszczać w cache’owanych elementach, ale w naszym przypadku już niedługo zrezygnujemy z samego requestAction.

widok zbiorczy - dashboard

tak to wygląda teraz

Zróbmy z tego widoku zakładki modyfikując widok all.ctp w następujący sposób:

<script type="text/javascript">
   $(document).ready(function() {
      $("#tabs").tabs();
   });
</script>


<div id="tabs">
   <ul>
      <li><a href="#tabs-1">Items</a></li>
      <li><a href="#tabs-2">Things</a></li>
   </ul>
   <div id="tabs-1">
      <?php echo $this->requestAction(
                  array(
                     "controller"=> "items",
                     "action"=> "index"),
                  array("return")
            ); ?>
   </div>
   <div id="tabs-2">
      <?php echo $this->requestAction(
                  array(
                     "controller"=> "things",
                     "action"=> "index"),
                  array("return")
            ); ?>
   </div>
</div>

Żeby wypieczone widoki były “otoczone” ramką z zakładki – trzeba w nich dodać taki fragment na samym końcu plików index.ctp:

<div style="float: none; clear: both;">&nbsp;</div>
Widok zakładek

tak wyglądają nasze zakładki

Dodajmy więcej elementów, żeby zobaczyć jak zachowuje się pagination…

insert into items(`name`) values ('Item 4'), ('Item 5'), ('Item 6'), ('Item 7')

i zmieńmy parametry stronicowania, żeby efekty stronicowania zobaczyć szybko ;)

class ItemsController extends AppController {
//...
	var $paginate = array(
	  'limit'=> 5
	);
//...

Widzimy, że w naszym zbiorczym widoku link ciągle prowadzi do items/index. Wolelibyśmy, żeby stronicowanie odbyło się w zakładkach.

Można oczywiście próbować rozwiązania via php. Oprogramować obsługę parametrów w metodzie all(), zmienić cel linków stronicowania i sortowania w widokach. I ogólnie dużo napracować się nad tym, żeby nasz kod wyglądał źle ;). Już za chwilę moc javascript i w szczególności jQuery przyjdzie nam z pomocą. Zanim to nastąpi – zamieńmy nasze zakładki na Ajaxowe zmieniając widok all.ctp:

// views/items/all.ctp
<script type="text/javascript">
   $(document).ready(function() {
      $("#tabs").tabs();
   });
</script>


<div id="tabs">
   <ul>
      <li>
         <?php echo $html->link(
                  "Items",
                  array(
                     "controller"=> "items",
                     "action"=> "index"
                  )
               );
         ?>
      </li>
      <li>
         <?php echo $html->link(
                  "Things",
                  array(
                     "controller"=> "things",
                     "action"=> "index"
                  )
               );
         ?>
      </li>
   </ul>
</div>

…prościzna, prawda? Kwestię requestAction też już mamy z głowy. Jednak pojawił się problem – wnętrze zakładek pojawiło się w powtórzonym layoucie.
Szybko naprawimy to w app_controller.php przy pomocy RequestHanlder:

// /app/app_controller.php
var $components = array("RequestHandler");
function beforeFilter(){
    if($this->RequestHandler->isAjax()){
      $this->layout = "ajax";
    }
}

Wrócmy do kwestii stronicowania (i sortowania przy okazji). Chcemy, aby kolejne strony ładowały się w danej zakładce. Wykorzystajmy do tego potęgę jQuery.
Dodajemy taki prosty skrypt

// views/items/all.ctp
   $('#tabs a[href*=/sort:],#tabs a[href*=/page:]').live('click', function(){
       $('#tabs>div:visible').load($(this).attr('href'));
          return false;
   });

Jeśli interesuje Cię co się w tym fragmencie dzieje, zapraszam na do innego wpisu. W dużym skrócie: łapiemy linki, które mają w adresie “sort:” i “page:” i zmuszamy je aby przeładowały zawartość zakładki, zamiast całej strony.

Share Button