2
\$\begingroup\$

I am designing a basic in-memory cache storage with a thin CRUD (actually CRD) interface. The design is inspired by backend solutions such as Parse and StackMob.

Main characteristics:

  • Cache consists of tables(schemas) storing hashes.

  • The keys(fields) in a table are fixed(enforced).

  • Maximum memory efficiency for storing large data.

Here is the design I came up with, which is tested and fully functional.

There are too many Exceptions for my taste, but somehow I need to enforce things that "should not happen". Another thought was to log errors silently and then ignore them. However, that means my code using this API will have to account for those errors, increasing the complexity.

The basic methods are create, readOne, read, and delete. CRUD usually includes update but I find it useless as it can be accessed by combining 'read', 'delete', 'create' and requires additional decision how to handle concurrency.

I'm curious to get feedback and thankful for any critiques or remarks regarding what can be improved.

/** Local In-Memory Cache: Tables referred by their string names
 * Storing hashes:
 *    all hashes must have 'id' as key
 *    all hashes must have keys other than 'id'
 *    and hashes in the same table must have the same key structure (enforced)
 * Basic CRUD interface + 'readOne' for the first entry + 'realDelete'
 * optimized to save memory: arrays packed into |-separated strings and gz(de/in)flated
 */
class LocStore {
   // ass. array 'table' => string of keys
   private $_keys = array();
   // assoc. array of strings indexed by table and id
   private $_values = array(); 

   /** create entry, enforce the same keys and new id
    * @param string $table
    * @param ass. array $hash, must have 'id' not null
    */
   public function create ($table, array $hash) {
      $id = $hash['id'];
      if (! $id) throw new Exception('Id is null');
      unset($hash['id']);
      if (! $hash) throw new Exception('Empty hash beside id');
      // sort by keys alphabetically 
      ksort($hash);
      $keyString = $this->_toString(array_keys($hash));
      if ( empty($this->_keys[$table]) ) {
         $this->_keys[$table] = $keyString;         
      } elseif ($this->_keys[$table] != $keyString) {
         throw new Exception('Array keys mismatch');
      }
      if ( isset($this->_values[$table]) && ! empty($this->_values[$table][$id]) ) {
         throw new Exception("Id '$id' already exists");
      }
      $this->_values[$table][$id] = $this->_toString(array_values($hash));
      // for chaining
      return $this;
   }

   // read one entry, empty array if nothing is there
   public function readOne ($table) {
      if ( empty($this->_values[$table]) ) return array();
      reset($this->_values[$table]);
      $id = key($this->_values[$table]);
      return $this->read($table, $id);
   }

   // read by id, empty array if not found
   public function read ($table, $id) {
      if ( empty($this->_values[$table]) || empty($this->_values[$table][$id]) ) return array();
      $keys = $this->_toArray($this->_keys[$table]);
      $values = $this->_toArray($this->_values[$table][$id]);
      $result = array_combine($keys, $values);
      $result['id'] = $id;
      return $result;
   }

   // delete by id, exception if not found
   public function delete ($table, $id) {
      if ( empty($this->_values[$table]) || empty($this->_values[$table][$id]) ) {
         throw new Exception('Deleting non-existant entry');
      }
      unset($this->_values[$table][$id]);
      return $this;
   }

   public function readDelete($table, $id) {
      $hash = $this->read($table, $id);
      if ($hash) $this->delete($table, $id);
      return $hash;
   }

   private function _toString (array $array) {
      $json = json_encode($array);
      return gzdeflate($json);
   }

   private function _toArray ($string) {
      $string = gzinflate($string);
      return json_decode($string);
   }

}
\$\endgroup\$
12
  • \$\begingroup\$ Out of interest why not just use memcached? \$\endgroup\$
    – Dave
    Nov 28, 2013 at 16:03
  • \$\begingroup\$ I've considered memcached or other in-memory databases. I actually could not see the reason to use any of them. I understand there is no compression out of the box, API is not suitable for tables, the code is long and hidden inside thousands of other files in Zend. I'd still need to rewrite and adapt, and I can do just that with much shorter code and full control of what it does. \$\endgroup\$ Nov 30, 2013 at 14:22
  • \$\begingroup\$ @Dave Is there anything I miss or overlook that way? \$\endgroup\$ Nov 30, 2013 at 14:29
  • \$\begingroup\$ The whole point of memcached is to cache sql query results so that instead of having to hit the mysqld every time you simply pull the result set from memcached instead saving disk IO and load on the mysqld doing it in php like this means that your php script memory usage will skyrocket you'll probably find you have to edit your php.ini to allow larger memory usage by script 64mb is default i think also on pages with large datasets then you'll be increasing load time massively due to your gzip part as it has to zip/unzip massive datasets AND page your mysqld. \$\endgroup\$
    – Dave
    Nov 30, 2013 at 17:06
  • \$\begingroup\$ not to mention you have to handle persistance of your cache object across page loads which is either storing it in a session or some how committing it to a text file or db anyway. While there's nothing massively wrong with your code (that I can see at least) there's also no benefit to your application by doing what you're doing only detractors \$\endgroup\$
    – Dave
    Nov 30, 2013 at 17:07

1 Answer 1

3
\$\begingroup\$

A cache purpose's is to store data for faster response to a future request. An implementation mixing data caching with data validation is more like an in-memory database; the cache's concern is to temporarily store key (that usually is a combination of previous request's parameters) value pairs for faster response.

if ( empty($this->_values[$table]) ) return array(); returning a default value when there isn't any value associated with the requested key is another concern out of scope of a cache, regardless of its type (in-memory or on-disk).

Evicting a cache entry is a concern that shouldn't result in an exception since the caller is interested in getting a value for the key it's passing, either from the cache or from wherever it is stored. The cache's existence should be transparent for the caller since the application logic is same whether on not the cache is used. The following is out of scope for a cache implementation:

if ( empty($this->_values[$table]) || empty($this->_values[$table][$id]) ) {
    throw new Exception('Deleting non-existant entry');
}

Nevertheless an improvement to reduce the exception handling could be to throw RuntimeException or LogicException so they are handled according to the caller's preference.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.