Custom Model caching causing infinite loop & Seg fault

Hi all,

I've written a caching wrapper for find() and findFirst(). However, strangely and in very unique circumstances, when caching is enabled it causes an infinite loop that triggers tons of queries and results in a seg fault. Disabling caching makes the problem go away.

I can't see anything in my code that should cause this, so I'm hoping someone else can:

Relevant code:

class Base extends \Phalcon\Mvc\Model{

    /** @var array A cache of queries so they don't have to be run multiple times
     * @see find()
    */
    private static $_find_query_cache      = [];
    private static $_findFirst_query_cache = [];

    /** @var caching Whether or not find() caches by default.  May be useful if doing a bunch of updates
             on records you know will be requeried.  Note that this applies to ALL models, not just a specific
             model.

             Change with doCache();
    */
    private static $caching = TRUE;

    public static $queryCount = 0;

    public static function doCache($cache){
        self::$caching = $cache;
    }

    public static function getCacheKeys($mode = 'all'){
        return ($mode = 'all') ? array_keys(self::$_find_query_cache) : array_keys(self::$_findFirst_query_cache);
    }

    /**
     * Extending default find() to add a caching option.  Caching option can be overridden with
     * cache directives (below) or by setting the class static "caching" with doCache()
     *
     * @param  array $params Same params as default find() except it also also accepts cache directive keys
     *                       that take effect regardless of their value (ie: fresh=>FALSE will still apply)
     *                       * fresh - will force a re-query.  New result will be cached.
     *                       * ninja - will force a re-query.  New result will not be cached.
     *
     * @return mixed Whatever default find() returns.  May return NULL if my if conditions are broken
     */
    public static function find($params = NULL){
        return self::findBody($params,'all');
    }

    /**
     * Just like find() above, but for findFirst()
     */
    public static function findFirst($params = NULL){
        return self::findBody($params,'one');
    }

    /**
     * Does the caching work for find() and findFirst()
     * @see self::find()
     */
    public static function findBody($params,$mode = 'all'){
        self::$queryCount++;

        // the DI gets passed as a parameter sometimes, but it's not serializable
        // so it needs to be removed
        $serializable_params = (!is_array($params))
                               ? $params
                               : array_filter($params,function($key){
                                    return $key != 'di';
                                 }, \ARRAY_FILTER_USE_KEY);

        $key    = static::class.serialize($serializable_params);
        $result = NULL;
        // Can't use assignment by reference and the ternary operator - PHP gets messed up.
        $cache  =& self::$_find_query_cache;
        if($mode == 'one'){
            $cache =& self::$_findFirst_query_cache;
        }

        // Return a cached copy if possible
        if(!isset($params['fresh']) && !isset($params['ninja']) && self::$caching && isset($cache[$key])){
            return $cache[$key];
        }
        else{
            $result = ($mode == 'all') ? parent::find($params) : parent::findFirst($params);
        }

        // Store the result if possible
        if(self::$caching && !isset($params['ninja'])){
            $cache[$key] = $result;
        }

        return $result;
    }
}

As an added strangeness, the queries that get repeatedly run are a SELECT COUNT(*) AS rowcount ... query, and 2 UPDATE queries - which don't involve the cache at all.

fix this line, is always true

public static function getCacheKeys($mode = 'all'){
    return ($mode === 'all') ? array_keys(self::$_find_query_cache) : array_keys(self::$_findFirst_query_cache);
}

Thanks for finding that. Obviously that was a bug, but it appears to be unrelated - the problem persists.

fix this line, is always true

public static function getCacheKeys($mode = 'all'){
  return ($mode === 'all') ? array_keys(self::$_find_query_cache) : array_keys(self::$_findFirst_query_cache);
}


2.2k

i wrote a class that should auto cache your models check it out hope this helps