<?php
    /*   
     Copyright (c) 2012, Paul G Talaga
     All rights reserved.
     
     Redistribution and use in source and binary forms, with or without
     modification, are permitted provided that the following conditions are met:
     * Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
     * Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.

     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     DISCLAIMED. IN NO EVENT SHALL Paul G Talaga BE LIABLE FOR ANY
     DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */
    
    // Wrapper class to memcache, provides directory based cache locality using 
	//	a centralized memcache cluster for note storage which keeps track of where
    //  all actual data is stored or replicated.  This centralized cluster is 
	//	specified with rack 0, and the Local rack can't be 0!  Hard coded is a 
	//	replication
    //  value so local copies don't take over everying.  This makes deletion a 
	//	little easier and also allows clients to choose the 'closest' local copy 
	//	to use.
    //  The first listed cluster is where the truth is, so if any error occurs, 
	//	replace the value from there.
    // Due to this being in PHP, we can't issue simultaneous requests to all 
	//	racks at once (cross pools).  Thus, we'll just have to deal with the 
	//	slowdown until a C implementation is done.
    // Compression is turned OFF for centralized note storage so append works 
	//	as desired.
    // For atomic centralized note update we need append, thus 
	//	we need PECL Memcache > 3.0.0. 
    // TODO: Think about what expire time to use on local copy.  
	//	Actual expire value not available, maybe store in note? 
    include_once('Mem_RackAware.php');
    class Memcache_Dir extends Memcache_RackAware{
        // constructor function
        function __construct(){
            $this->dupLimit = 2;
            // A note is of the form <rack>,<rack>,...,<rack> up to dupLimit.
            $this->rackConstruct(); 
			// constructor for RackAware, provides $this->local which is the rack 
            // number this machine should use, and $this->racks which
            // is an array of Memcache instances indexed by rack number.  
			//	So $this->racks[$this->local] would be the closest memcache
            // instance.
                                    //echo "Dir\n";
        }
        
        // addServer     provided by Memcache_RackAware
 /**************************************************************/    
        function add($key,$value,$flags = 0,$expire = 0){  
            // Add item with key
            // First check to see if its in local by doing an append of ''.  
			// This will return true if it exists (and won't touch it, or 
			//	change expire)
            //  or false if it doesn't exist.
            if($this->racks[$this->local]->append($key,'') === TRUE){
                return false;   // exists in local cache, so must exist in 
								// centralized cluster as well
            }
            // now add note to centralized, return false if it fails (stored elsewhere)
            if(!$this->racks[0]->add($key,$this->local,$flags,$expire)){ 
				// Just store this rack number, replicated racks will use comma
                return false; // exists in centralized cluster, so return false
            }
            // Now we've got to put the data in local, so use a set to be sure
            $this->racks[$this->local]->set($key,$value,$flags,$expire);
            return true;
        }
/**************************/         
        function connect($host,$port,$timeout = 1){
            return self::addServer($host,$port,TRUE,1,$timeout);
        }
/**************************/         
        function delete($key,$timeout = 0){
            // Delete data with key.  We'll only return failure if 
			//	centralized failed
            // Get duplication info
            $dup = $this->racks[0]->get($key);
            if($dup === FALSE){
                return false;   // doesn't exists in centralized cluster, 
								//	so return false
            }
            // Delete centralized and duplicates
           $this->racks[0]->delete($key,$timeout);
           foreach(explode(',',$dup) as $d){
                $this->racks[$d]->delete($key,$timeout);
           }
           return TRUE;
        }
/**************************/         
        function flush(){
            // flush all racks!
           foreach($this->racks as $r){
                $r->flush();
            }
        }
/**************************/         
        function get($key, &$flags = 0, &$cas = ''){ 
			// Validate all these flags and cas stuff works
            // Get local, otherwise get note and get it
            $ret = $this->racks[$this->local]->get($key,$flags,$cas);
            if($ret !== FALSE){
                return $ret;
            }
            // go get note
            $note = $this->racks[0]->get($key);
            if($note === FALSE){
                return FALSE;
            }
            $rep = explode(',',$note);
            $order = range(0,count($rep) - 1);
            // here we could optimize by choosing a close copy
            shuffle($order);
            $succ = 0;
            foreach($order as $o){
                $ret = $this->racks[$rep[$o]]->get($key,$flags,$cas);
                if($ret !== FALSE){$succ = 1; break;}
            }
            if($succ == 0){return FALSE;}
            // move local?
            if(count($rep) < $this->dupLimit){
                $this->racks[0]->append($key,',' . $this->local);
                $this->racks[$this->local]->set($key,$ret); // never expire               
            }
           return $ret; 
        }
/**************************/ 
        function replace($key, $value,$flag = 0,$expire = 0){
            // Replace should return false if the item doesn't already exist.
            // So check to see if a note exists on the central, and if so just 
			//	do a set so it always works
            if($this->racks[0]->get($key) !== FALSE){
                return $this->set($key,$value,$flag,$expire);
            }
            return false;
        }
/**************************/ 
        function decrement($key, $amt = 1, $exptime = 0){
            // First get note, if it exists do a decrement on all, and if the 
			//	others don't match the first, set them to match
            // Optionaly we could remove all dups and let gets re-distribute
            $flags = 0;
            $note = $this->racks[0]->get($key,$flags);
            if($note === FALSE){
                return FALSE;
            }
            $rep = explode(',',$note);
            $correct = $this->racks[$rep[0]]->decr($key,$amt,$exptime);
            foreach($rep as $r){
                if($r =! $rep[0]){
                    $n = $this->racks[$r]->decr($key,$amt,$exptime);
                    if($n =! $correct){
                        $this->racks[$r]->set($key,$correct,$exptime);
                    }
                }
            }
            return $correct;
        }
/**************************/        
        function increment($key, $amt = 1, $exptime = 0){
            // First get note, if it exists do a increment on all, and if the 
			//	others don't match the first, set them to match
            // Optionaly we could remove all dups and let gets re-distribute
            $flags = 0;
            $note = $this->racks[0]->get($key,$flags);
            if($note === FALSE){
                return FALSE;
            }
            $rep = explode(',',$note);
            $correct = $this->racks[$rep[0]]->incr($key,$amt,$exptime);
            foreach($rep as $r){
                if($r =! $rep[0]){
                    $n = $this->racks[$r]->incr($key,$amt,$exptime);
                    if($n =! $correct){
                        $this->racks[$r]->set($key,$correct,$exptime);
                    }
                }
            }
            return $correct;
        }
/**************************/         
        function set($key,$value,$flags = 0,$expire = 0){
            // a Set clears out duplicates!
            // we must get the note before overwriting it so to delete
            $note = $this->racks[0]->get($key);
            if($note !== FALSE){
                $rep = explode(',',$note);
                foreach($rep as $r){
                    $this->racks[$r]->delete($key);
                }
            }
            // now add note to centralized
            if(!$this->racks[0]->set($key,$this->local,$flags,$expire)){ 
				// Just store this rack number, replicated racks will use comma
                return FALSE;
            }
            // Now we've got to put the data in local
            if(!$this->racks[$this->local]->set($key,$value,$flags,$expire)){
                // hm, local failed, so remove central note
                $this->racks[0]->delete($key);
                return FALSE;
            }
            //we'll return false if any returned false, though you won't know 
			//	which rack failed.
            return TRUE;
        }
        
        function findServer($key){
            return $this->racks[$this->local]->findServer($key);
        }

}
?>
