new default eTemplate request storate in EGroupware cache with an expiration time of 4 hours.

Benefit over old default is, that we get a short etemplate_exec_id giving better performance for ajax requests.
Drawback is currently poor garbadge collection only removing requests not used in 4 hours or which call egw_framework::window_close on server-side.
We can improve garbadge-collection by binding to window on(before)unload sending a (synchronious) request to server to remove concerned eT2 request.
This commit is contained in:
Ralf Becker 2014-01-15 16:46:16 +00:00
parent f9c2c73432
commit b56175a0f4
5 changed files with 243 additions and 40 deletions

View File

@ -51,11 +51,7 @@ class etemplate_new extends etemplate_widget_template
if ($name) $this->read($name,$template='default',$lang='default',$group=0,$version='',$load_via); if ($name) $this->read($name,$template='default',$lang='default',$group=0,$version='',$load_via);
// generate new etemplate request object, if not already existing // generate new etemplate request object, if not already existing
if(!self::$request) if(!isset(self::$request)) self::$request = etemplate_request::read();
{
self::$request = etemplate_request::read();
self::$request->content = array();
}
} }
/** /**
@ -294,6 +290,10 @@ class etemplate_new extends etemplate_widget_template
self::$response->generic('et2_validation_error', self::$validation_errors); self::$response->generic('et2_validation_error', self::$validation_errors);
exit; exit;
} }
// tell request call to remove request, if it is not modified eg. by call to exec in callback
self::$request->remove_if_not_modified();
//error_log(__METHOD__."(,".array2string($content).')'); //error_log(__METHOD__."(,".array2string($content).')');
//error_log(' validated='.array2string($validated)); //error_log(' validated='.array2string($validated));
$content = ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); $content = ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated));
@ -346,6 +346,9 @@ class etemplate_new extends etemplate_widget_template
error_log(__METHOD__."(,".array2string($content).')'); error_log(__METHOD__."(,".array2string($content).')');
error_log(' validated='.array2string($validated)); error_log(' validated='.array2string($validated));
// tell request call to remove request, if it is not modified eg. by call to exec in callback
self::$request->remove_if_not_modified();
return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated)); return ExecMethod(self::$request->method, self::complete_array_merge(self::$request->preserv, $validated));
} }

View File

@ -7,7 +7,7 @@
* @subpackage api * @subpackage api
* @link http://www.egroupware.org * @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de> * @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright (c) 2007-13 by Ralf Becker <RalfBecker@outdoor-training.de> * @copyright (c) 2007-14 by Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -64,6 +64,8 @@
* } * }
* *
* For an example look in link_widget::ajax_search() * For an example look in link_widget::ajax_search()
*
* @property-read boolean $data_modified true if data was modified and therefore needs saving
*/ */
class etemplate_request class etemplate_request
{ {
@ -79,6 +81,12 @@ class etemplate_request
* @var boolean * @var boolean
*/ */
protected $data_modified=false; protected $data_modified=false;
/**
* Flag that stored data should be removed by destructor, if not modified.
*
* @var boolean
*/
protected $remove_if_not_modified=false;
/** /**
* mcrypt resource * mcrypt resource
* *
@ -105,6 +113,9 @@ class etemplate_request
/** /**
* Factory method to get a new request object or the one for an existing request * Factory method to get a new request object or the one for an existing request
* *
* New default is to use egw_cache to store requests and no longer request or
* session documented below:
*
* If mcrypt AND gzcompress is available this factory method chooses etemplate_request, * If mcrypt AND gzcompress is available this factory method chooses etemplate_request,
* which stores the request data encrypted in a hidden var directly in the form, * which stores the request data encrypted in a hidden var directly in the form,
* over etemplate_request_session, which stores the data in the session (and causing * over etemplate_request_session, which stores the data in the session (and causing
@ -117,13 +128,19 @@ class etemplate_request
{ {
if (is_null(self::$request_class)) if (is_null(self::$request_class))
{ {
// new default to use egw_cache to store requests
self::$request_class = 'etemplate_request_cache';
/* old default to use request if mcrypt and gzcompress are available and session if not
self::$request_class = check_load_extension('mcrypt') && function_exists('gzcompress') && self::$request_class = check_load_extension('mcrypt') && function_exists('gzcompress') &&
self::init_crypt() ? __CLASS__ : 'etemplate_request_session'; self::init_crypt() ? __CLASS__ : 'etemplate_request_session';
*/
} }
if (self::$request_class != __CLASS__) if (self::$request_class != __CLASS__)
{ {
return call_user_func(array(self::$request_class,'read'),$id); $request = call_user_func(self::$request_class.'::read', $id);
} }
else
{
$request = new etemplate_request(); $request = new etemplate_request();
if (!is_null($id)) if (!is_null($id))
@ -150,10 +167,20 @@ class etemplate_request
if (!$request->data) if (!$request->data)
{ {
error_log(__METHOD__."() id not valid!"); error_log(__METHOD__."() id not valid!");
return false; $request = false;
} }
//error_log(__METHOD__."() size of request = ".bytes($id)); //error_log(__METHOD__."() size of request = ".bytes($id));
} }
}
if (!$request) // eT2 request/session expired
{
// redirect to index-url of app
error_log(__METHOD__."('$id', ...) eT2 request not found --> redirect to index-url");
list($app) = explode('.', $_GET['menuaction']);
$index_url = isset($GLOBALS['egw_info']['apps'][$app]['index']) ?
'/index.php?menuaction='.$GLOBALS['egw_info']['apps'][$app]['index'] : '/'.$app.'/index.php';
egw_framework::redirect_link($index_url);
}
return $request; return $request;
} }
@ -174,14 +201,14 @@ class etemplate_request
*/ */
public function &id() public function &id()
{ {
$id = serialize($this->data); $data = serialize($this->data);
// compress the data if available // compress the data if available
if (self::$compression_level && function_exists('gzcompress')) if (self::$compression_level && function_exists('gzcompress'))
{ {
//$len_uncompressed = bytes($id); //$len_uncompressed = bytes($id);
//$time = microtime(true); //$time = microtime(true);
$id = gzcompress($id,self::$compression_level); $data = gzcompress($data, self::$compression_level);
//$time = number_format(1000.0 * (microtime(true) - $time),1); //$time = number_format(1000.0 * (microtime(true) - $time),1);
//$len_compressed = bytes($id); //$len_compressed = bytes($id);
//error_log(__METHOD__."() compressed from $len_uncompressed to $len_compressed bytes in $time ms"); //error_log(__METHOD__."() compressed from $len_uncompressed to $len_compressed bytes in $time ms");
@ -189,9 +216,9 @@ class etemplate_request
// encrypt the data if available // encrypt the data if available
if (self::init_crypt()) if (self::init_crypt())
{ {
$id = mcrypt_generic(self::$mcrypt,$id); $data = mcrypt_generic(self::$mcrypt, $data);
} }
$id = base64_encode($id); $id = base64_encode($data);
//error_log(__METHOD__."() #$this->id: size of request = ".bytes($id));//.", id='$id'"); //error_log(__METHOD__."() #$this->id: size of request = ".bytes($id));//.", id='$id'");
//self::debug(); //self::debug();
@ -293,6 +320,7 @@ class etemplate_request
if ($this->data[$var] !== $val) if ($this->data[$var] !== $val)
{ {
$this->data[$var] = $val; $this->data[$var] = $val;
//error_log(__METHOD__."('$var', ...) data of id=$this->id changed ...");
$this->data_modified = true; $this->data_modified = true;
} }
} }
@ -305,6 +333,8 @@ class etemplate_request
*/ */
public function &__get($var) public function &__get($var)
{ {
if ($var == 'data_modified') return $this->data_modified;
return $this->data[$var]; return $this->data[$var];
} }
@ -416,4 +446,14 @@ class etemplate_request
self::$mcrypt = null; self::$mcrypt = null;
} }
} }
/**
* Mark request as to destroy, if it does not get modified before destructor is called
*
* If that function is called, request is removed from storage, further modification will work!
*/
public function remove_if_not_modified()
{
$this->remove_if_not_modified = true;
}
} }

View File

@ -0,0 +1,147 @@
<?php
/**
* eGroupWare - eTemplate request object storing the data in EGroupware instance cache
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright (c) 2014 by Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$
*/
/**
* Class to represent the persitent information stored on the server for each eTemplate request
*
* The information is stored in EGroupware tree cache with a given fixed expiration time.
* We use tree cache with install-id as part of key to not loose all requests, if admin
* clears instance cache (Admin >> Clear cache and register hooks)!
*
* To enable the use of this handler, you have to set (in etemplate/inc/class.etemplate_request.inc.php):
*
* etemplate_request::$request_class = 'etemplate_request_cache';
*
* The request object should be instancated only via the factory method etemplate_request::read($id=null)
*
* $request = etemplate_request::read();
*
* // add request data
*
* $id = $request->id();
*
* b) open or modify an existing request:
*
* if (!($request = etemplate_request::read($id)))
* {
* // request not found
* }
*
* Ajax requests can use this object to open the original request by using the id, they have to transmit back,
* and register further variables, modify the registered ones or delete them AND then update the id, if it changed:
*
* if (($new_id = $request->id()) != $id)
* {
* $response->addAssign('etemplate_exec_id','value',$new_id);
* }
*
* For an example look in link_widget::ajax_search()
*/
class etemplate_request_cache extends etemplate_request
{
/**
* Expiration time of 4 hours
*/
const EXPIRATION = 7200;
/**
* request id
*
* @var string
*/
protected $id;
/**
* Private constructor to force the instancation of this class only via it's static factory method read
*
* @param string $_id
*/
private function __construct($_id=null)
{
$this->id = $_id ? $_id : self::request_id();
//error_log(__METHOD__."($_id) this->id=$this->id");
}
/**
* return the id of this request
*
* @return string
*/
public function id()
{
//error_log(__METHOD__."() id=$this->id");
return $this->id;
}
/**
* Factory method to get a new request object or the one for an existing request
*
* @param string $id=null
* @return etemplate_request|boolean the object or false if $id is not found
*/
static function read($id=null)
{
$request = new etemplate_request_cache($id);
if (!is_null($id))
{
//error_log(__METHOD__."() reading $id");
if (!($request->data = egw_cache::getTree($GLOBALS['egw_info']['server']['install_id'].'_etemplate', $id)))
{
error_log("Error reading etemplate request data for id=$id!");
return false;
}
}
//error_log(__METHOD__."(id=$id) returning ".array2string($request));
return $request;
}
/**
* creates a new unique request-id
*
* @return string
*/
static function request_id()
{
return uniqid($GLOBALS['egw_info']['flags']['currentapp'].'_'.$GLOBALS['egw_info']['user']['account_lid'].'_',true);
}
/**
* saves content,readonlys,template-keys, ... via eGW's appsession function
*
* As a user may open several windows with the same content/template wie generate a location-id from microtime
* which is used as location for request to descriminate between the different windows. This location-id
* is then saved as a hidden-var in the form. The above mentions session-id has nothing to do / is different
* from the session-id which is constant for all windows opened in one session.
*/
function __destruct()
{
if ($this->remove_if_not_modified && !$this->data_modified && isset($this->data['last_saved']))
{
//error_log(__METHOD__."() destroying $this->id");
egw_cache::unsetTree($GLOBALS['egw_info']['server']['install_id'].'_etemplate', $this->id);
}
elseif (($this->data_modified ||
// if half of expiration time is over, save it anyway, to restart expiration time
isset($this->data['last_saved']) && (time()-$this->data['last_saved']) > self::EXPIRATION/2))
{
//error_log(__METHOD__."() saving $this->id");
$this->data['last_saved'] = time();
if (!egw_cache::setTree($GLOBALS['egw_info']['server']['install_id'].'_etemplate', $this->id, $this->data, self::EXPIRATION))
{
error_log("Error storing etemplate request data for id=$this->id!");
}
}
}
}

View File

@ -7,7 +7,7 @@
* @subpackage api * @subpackage api
* @link http://www.egroupware.org * @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de> * @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright (c) 2009 by Ralf Becker <RalfBecker@outdoor-training.de> * @copyright (c) 2009-14 by Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -139,7 +139,13 @@ class etemplate_request_files extends etemplate_request
*/ */
function __destruct() function __destruct()
{ {
if (!file_put_contents($filename = self::$directory.'/'.$this->id,serialize($this->data))) if ($this->remove_if_not_modified && !$this->data_modified)
{
//error_log(__METHOD__."() destroying $this->id");
@unlink(self::$directory.'/'.$this->id);
}
elseif (!$this->destroyed && $this->data_modified &&
!file_put_contents($filename = self::$directory.'/'.$this->id,serialize($this->data)))
{ {
error_log("Error opening '$filename' to store the etemplate request data!"); error_log("Error opening '$filename' to store the etemplate request data!");
} }

View File

@ -7,7 +7,7 @@
* @subpackage api * @subpackage api
* @link http://www.egroupware.org * @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de> * @author Ralf Becker <RalfBecker@outdoor-training.de>
* @copyright (c) 2007-9 by Ralf Becker <RalfBecker@outdoor-training.de> * @copyright (c) 2007-14 by Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$ * @version $Id$
*/ */
@ -119,8 +119,15 @@ class etemplate_request_session extends etemplate_request
*/ */
function __destruct() function __destruct()
{ {
if ($this->data_modified) $GLOBALS['egw']->session->appsession($this->id,'etemplate',$this->data); if ($this->remove_if_not_modified && !$this->data_modified)
{
//error_log(__METHOD__."() destroying $this->id");
egw_cache::unsetSession('etemplate', $this->id);
}
elseif (!$this->destroyed && $this->data_modified)
{
egw_cache::setSession('etemplate', $this->id, $this->data);
}
if (!$this->garbage_collection_done) if (!$this->garbage_collection_done)
{ {
$this->_php4_request_garbage_collection(); $this->_php4_request_garbage_collection();