_binary From f852713f1f254fdd7e30dbee17682f0dce393b99 Mon Sep 17 00:00:00 2001
From: Rushikesh Padia <padiarushi3012@gmail.com>
Date: Thu, 27 Apr 2023 04:07:55 -0700
Subject: [PATCH] Improved Cache

---
 src/library/FileCache.php                 | 228 +++---------------
 src/library/cache/Cache.php               |  66 ++++++
 src/library/cache/CacheMetricsWrapper.php | 116 ++++++++++
 src/library/cache/MarkersCache.php        | 270 ++++++++++++++++++++++
 tests/FileCacheTest.php                   |  83 +++++++
 5 files changed, 567 insertions(+), 196 deletions(-)
 create mode 100644 src/library/cache/Cache.php
 create mode 100644 src/library/cache/CacheMetricsWrapper.php
 create mode 100644 src/library/cache/MarkersCache.php
 create mode 100644 tests/FileCacheTest.php

diff --git a/src/library/FileCache.php b/src/library/FileCache.php
index 42447b951..2457d605f 100644
--- a/src/library/FileCache.php
+++ b/src/library/FileCache.php
@@ -22,7 +22,9 @@
  *
  * END LICENSE
  *
+ *
  * @author Chris Pollett chris@pollett.org
+ * @author Rushikesh Padia padiarushi3012@gmail.com
  * @license https://www.gnu.org/licenses/ GPL3
  * @link https://www.seekquarry.com/
  * @copyright 2009 - 2023
@@ -30,116 +32,45 @@
  */
 namespace seekquarry\yioop\library;
 
-use seekquarry\yioop\configs as C;
-use seekquarry\yioop\models\datasources as D;
+use seekquarry\yioop\library\cache\Cache;
+use seekquarry\yioop\library\cache\MarkersCache;
+use seekquarry\yioop\library\cache\CacheMetricsWrapper;
 
 /** For Yioop global defines */
 require_once __DIR__ . "/../configs/Config.php";
+
 /**
  * Library of functions used to implement a simple file cache
+ * The class acts as a proxy to internal cache implementation
  *
  * @author Chris Pollett
+ * @author Rushikesh Padia
  */
-class FileCache
+class FileCache implements Cache
 {
     /**
-     * File used to serve last cache request
-     * @var string
-     */
-    public $cache_file;
-    /**
-     * Folder name to use for this FileCache
-     * @var string
-     */
-    public $dir_name;
-    /**
-     * Reference to WebSite object that the current code is running under
-     * @var object
-     */
-    public $web_site;
-    /**
-     * Total number of bins to cycle between
-     */
-    const NUMBER_OF_BINS = 50;
-    /**
-     * Maximum number of files in a bin
+     * @var Cache Implementation to be used for caching
      */
-    const MAX_FILES_IN_A_BIN = 5000;
+    private Cache $cache;
     /**
-     * Creates the directory for the file cache, sets how frequently
-     * all items in the cache expire
-     *
-     * @param string $dir_name folder name of where to put the file cache
-     * @param WebSite an optional object that might be used to serve webpages
-     *      when Yioop run in CLI mode. This object has fileGetContents and
-     *      filePutContents methods which allow RAM caching of files.
+     * @param string $dir_name name of directory
+     * @param mixed|null $web_site Website
      */
-    public function __construct($dir_name, $web_site = null)
+    public function __construct(string $dir_name, mixed $web_site = null)
     {
-        $this->dir_name = $dir_name;
-        if (!is_dir($this->dir_name)) {
-            mkdir($this->dir_name);
-            chmod($this->dir_name, 0777);
-            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS) . "Manager";
-            $db = new $db_class();
-            $db->setWorldPermissionsRecursive($this->dir_name, true);
-        }
-        $this->web_site = $web_site;
-    }
-    /**
-     * Either a wrapper for file_get_contents, or if a WebSite object is being
-     * used to serve pages, it reads it in using blocking I/O
-     * file_get_contents() and caches it before return its string contents.
-     * Note this function assumes that only the web server is performing I/O
-     * with this file. filemtime() can be used to see if a file on disk has been
-     * changed and then you can use $force_read = true below to force re-
-     * reading the file into the cache
-     *
-     * @param string $filename name of file to get contents of
-     * @param bool $force_read whether to force the file to be read from
-     *      persistent storage rather than the cache
-     * @return string contents of the file given by $filename
-     */
-    public function fileGetContents($filename, $force_read = false)
-    {
-        if (!empty($this->web_site)) {
-            return $this->web_site->fileGetContents($filename, $force_read);
-        }
-        return file_get_contents($filename);
-    }
-    /**
-     * Either a wrapper for file_put_contents, or if a WebSite object is being
-     * used to serve pages, writes $data to the persistent file with name
-     * $filename. Saves a copy in the RAM cache if there is a copy already
-     * there.
-     *
-     * @param string $filename name of file to write to persistent storages
-     * @param string $data string of data to store in file
-     */
-    public function filePutContents($filename, $data)
-    {
-        if (!empty($this->web_site)) {
-            return $this->web_site->filePutContents($filename, $data);
-        }
-        return file_put_contents($filename, $data);
+        $cache = new MarkersCache($dir_name, $web_site);
+        $this->cache = new CacheMetricsWrapper($cache);
     }
     /**
      * Retrieve data associated with a key that has been put in the cache
      *
-     * @param string $key the key to look up
-     * @return mixed the data associated with the key if it exists, false
+     * @param mixed $key the key to look up
+     * @return mixed|false the data associated with the key if it exists, false
      *     otherwise
      */
-    public function get($key)
+    public function get(mixed $key): mixed
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        $this->cache_file = $checksum_dir . "/c" . webencode($key);
-        if (file_exists($this->cache_file)) {
-            $this->updateCache($key);
-            return unserialize($this->fileGetContents($this->cache_file));
-        }
-        return false;
+        return $this->cache->get($key);
     }
     /**
      * Stores in the file cache a key-value pair
@@ -147,123 +78,28 @@ class FileCache
      * @param string $key to associate with value
      * @param mixed $value to store
      */
-    public function set($key, $value)
+    public function set(string $key, mixed $value): void
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        if (!file_exists($checksum_dir)) {
-            mkdir($checksum_dir);
-            chmod($checksum_dir, 0777);
-        }
-        $cache_file = "$checksum_dir/c".webencode($key);
-        $this->updateCache($key);
-        $this->filePutContents($cache_file, serialize($value));
+        $this->cache->set($key, $value);
     }
     /**
-     * Makes a 0 - self::NUMBER_OF_BINS value out of the provided key
-     *
-     * @param string $key to convert to a random value between
-     *     0 - self::NUMBER_OF_BINS
-     * @return int value between 0 and self::NUMBER_OF_BINS
-     */
-    public function checksum($key)
-    {
-        $len = strlen($key);
-        $value = 0;
-        for ($i = 0; $i < $len; $i++) {
-            $value += ord($key[$i]);
-        }
-        return ($value % self::NUMBER_OF_BINS);
-    }
-    /**
-     * Deletes cache key value files and ram copies of key values stored in the
+     * Deletes cache key value files and ram copies of key values stored in
      * this file cache
      */
-    public function clear()
+    public function clear(): void
     {
-        if (!empty($this->web_site)) {
-            $this->web_site->clearFileCache();
-        }
-        if (is_dir($this->dir_name)) {
-            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS)."Manager";
-            $db = new $db_class();
-            $db->unlinkRecursive($this->dir_name, false);
-        }
+        $this->cache->clear();
     }
     /**
-     * Used to mark a cache item, and keep track of rounds according to the
-     * marker algorithm. This function determine if the cache is too full
-     * and if so eject an item.
+     * Returns the calculated performance metrics of the cache.
      *
-     * @param string $key that was just read from or written to. Might need
-     *  to be marked according to Marker algorithm
+     * @return array an associative array containing 'total', 'hits',
+     * and 'misses' representing total number of queries, total number
+     * of hits and misses respectively.
      */
-    protected function updateCache($key)
+    public function getPerformanceMetrics(): array
     {
-        $checksum_block = $this->checksum($key);
-        $checksum_dir = $this->dir_name . "/$checksum_block";
-        $marker_file = "$checksum_dir/cache_markers.txt";
-        if (!file_exists($checksum_dir)) {
-            mkdir($checksum_dir);
-            chmod($checksum_dir, 0777);
-        }
-        if (file_exists($marker_file)) {
-            $data = unserialize($this->fileGetContents($marker_file));
-        } else {
-            $data = [];
-        }
-        if (empty($data['MARKED'])) {
-            $data['MARKED'] = [];
-        }
-        if (empty($data['UNMARKED'])) {
-            $data['UNMARKED'] = [];
-        }
-        $now = time();
-        if (empty($data['TIME'])) {
-            $data['TIME'] = $now;
-        }
-        if (empty($data['UNMARKED'][$key]) && empty($data['MARKED'][$key])) {
-            $data['UNMARKED'][$key] = true;
-        }
-        if (!empty($data['UNMARKED'][$key])) {
-            $data['MARKED'][$key] = true;
-            unset($data['UNMARKED'][$key]);
-        }
-        $num_marked = count($data['MARKED']);
-        if ($num_marked > self::MAX_FILES_IN_A_BIN) {
-            $data['UNMARKED'] = array_merge($data['UNMARKED'], $data['MARKED']);
-            $data['MARKED'] = [];
-        }
-        $num_unmarked = count($data['UNMARKED']);
-        $total_in_cache = $num_marked + $num_unmarked;
-        if ($total_in_cache > self::MAX_FILES_IN_A_BIN) {
-            $num_delete = $total_in_cache - self::MAX_FILES_IN_A_BIN;
-            $num_unmarked_delete = min($num_unmarked, $num_delete);
-            for ($i = 0; $i < $num_unmarked_delete; $i++) {
-                $keys = array_keys($data['UNMARKED']);
-                $eject_key = mt_rand(0, $num_unmarked - 1);
-                unset($data['UNMARKED'][$eject_key]);
-                $num_unmarked--;
-                $delete_file = $checksum_dir . "/" . webencode($eject_key);
-                if (file_exists($delete_file)) {
-                    unlink($delete_file);
-                }
-            }
-            if ($now - $data["TIME"] > C\MIN_QUERY_CACHE_TIME) {
-                $in_cache_files = array_flip(glob($checksum_dir . "/c*"));
-                $keys = array_keys($data['UNMARKED']);
-                foreach ($keys as $check_key) {
-                    $check_file = $checksum_dir . "/" . webencode($check_key);
-                    if (isset($in_cache_files[$check_file])) {
-                        unset($in_cache_files[$check_file]);
-                    }
-                }
-                foreach ($in_cache_files as $to_delete => $num) {
-                    unlink($to_delete);
-                }
-                $data["TIME"] = $now;
-            }
-        }
-        $this->filePutContents($marker_file, serialize($data));
+        return $this->cache->getPerformanceMetrics();
     }
+
 }
diff --git a/src/library/cache/Cache.php b/src/library/cache/Cache.php
new file mode 100644
index 000000000..78295dc2e
--- /dev/null
+++ b/src/library/cache/Cache.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+
+namespace seekquarry\yioop\library\cache;
+
+/**
+ * A common interface for all cache implementations
+ *
+ * @author Rushikesh Padia
+ */
+interface Cache
+{
+    /**
+     * Method to get the value of given key from the cache.
+     * Based on the implementation, state of the cache is updated if hit occurs
+     * else false is returned.
+     *
+     * @param $key string key for which value is required
+     * @return mixed|false returns value if key is present in the cache, else false
+     */
+    public function get(string $key): mixed;
+    /**
+     * Method to add value to the cache.
+     * Based on the implementation, state of the cache is updated.
+     * If cache is full, a key is removed based on the eviction policy
+     *
+     * @param $key string key against which value has to be stored
+     * @param $value mixed value to be stored in cache
+     */
+    public function set(string $key, mixed $value): void;
+    /**
+     * Method to remove all the values from the cache
+     *
+     * @return null
+     */
+    public function clear();
+}
\ No newline at end of file
diff --git a/src/library/cache/CacheMetricsWrapper.php b/src/library/cache/CacheMetricsWrapper.php
new file mode 100644
index 000000000..629934a44
--- /dev/null
+++ b/src/library/cache/CacheMetricsWrapper.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\library\cache;
+
+/**
+ * A wrapper class to collect performance metrics of cache implementations
+ *
+ * @author Rushikesh Padia
+ */
+class CacheMetricsWrapper implements Cache
+{
+    /**
+     * @var Cache Any implementation of cache
+     */
+    private Cache $cache;
+    /**
+     * @var int Number of hits
+     */
+    private int $hits;
+    /**
+     * @var int Number of misses
+     */
+    private int $misses;
+    /**
+     * @var int Total number of requests
+     */
+    private int $total;
+
+    /**
+     * @param Cache $cache Any implementation of cache
+     */
+    public function __construct(Cache $cache)
+    {
+        $this->cache = $cache;
+        $this->hits = 0;
+        $this->misses = 0;
+        $this->total = 0;
+    }
+    /**
+     * Calculates hits and misses based on the return value from the
+     * wrapped class
+     * @param string $key cache key
+     * @return mixed|false cache value
+     */
+    public function get(string $key): mixed
+    {
+        $value = $this->cache->get($key);
+        if ($value !== false) {
+            $this->hits++;
+        } else {
+            $this->misses++;
+        }
+        $this->total++;
+        return $value;
+    }
+    /**
+     * Delegates call to the wrapped class which sets value in cache
+     * @param string $key cache key
+     * @param mixed $value cache value
+     */
+    public function set(string $key, mixed $value): void
+    {
+        $this->cache->set($key, $value);
+    }
+    /**
+     * Delegates call to the wrapped class which clears the cache
+     */
+    public function clear()
+    {
+        return $this->cache->clear();
+    }
+    /**
+     * Returns the calculated performance metrics of the cache.
+     *
+     * @return array an associative array containing 'total', 'hits',
+     * and 'misses' representing total number of queries,
+     * total number of hits and misses respectively.
+     */
+    public function getPerformanceMetrics(): array
+    {
+        return array(
+            'total' => $this->total,
+            'hits' => $this->hits,
+            'misses' => $this->misses
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/library/cache/MarkersCache.php b/src/library/cache/MarkersCache.php
new file mode 100644
index 000000000..4bbb927aa
--- /dev/null
+++ b/src/library/cache/MarkersCache.php
@@ -0,0 +1,270 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Chris Pollett chris@pollett.org
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+
+namespace seekquarry\yioop\library\cache;
+
+use seekquarry\yioop\library\WebSite;
+use seekquarry\yioop\configs as C;
+use function seekquarry\yioop\library\webencode;
+
+/** For Yioop global defines */
+require_once __DIR__ . "/../../configs/Config.php";
+
+class MarkersCache implements Cache
+{
+    /**
+     * File used to serve last cache request
+     * @var string
+     */
+    public $cache_file;
+    /**
+     * Folder name to use for this FileCache
+     * @var string
+     */
+    public $dir_name;
+    /**
+     * Total number of bins to cycle between
+     */
+    const NUMBER_OF_BINS = 50;
+    /**
+     * Maximum number of files in a bin
+     */
+    const MAX_FILES_IN_A_BIN = 5000;
+    /**
+     * PPH web server for RAM Caching
+     * @var WebSite|null
+     */
+    private ?WebSite $web_site;
+    /**
+     * Creates the directory for the file cache, sets how frequently
+     * all items in the cache expire
+     *
+     * @param string $dir_name folder name of where to put the file cache
+     * @param WebSite $web_site an optional object that might be used to serve
+     *      webpages when Yioop run in CLI mode. This object has
+     *      fileGetContents and filePutContents methods which allow
+     *      RAM caching of files.
+     */
+    public function __construct($dir_name, $web_site = null)
+    {
+        $this->dir_name = $dir_name;
+        if (!is_dir($this->dir_name)) {
+            mkdir($this->dir_name);
+            chmod($this->dir_name, 0777);
+            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS) . "Manager";
+            $db = new $db_class();
+            $db->setWorldPermissionsRecursive($this->dir_name, true);
+        }
+        $this->web_site = $web_site;
+    }
+    /**
+     * Either a wrapper for file_get_contents, or if a WebSite object is being
+     * used to serve pages, it reads it in using blocking I/O
+     * file_get_contents() and caches it before return its string contents.
+     * Note this function assumes that only the web server is performing I/O
+     * with this file. filemtime() can be used to see if a file on disk has been
+     * changed and then you can use $force_read = true below to force re-
+     * reading the file into the cache
+     *
+     * @param string $filename name of file to get contents of
+     * @param bool $force_read whether to force the file to be read from
+     *      persistent storage rather than the cache
+     * @return string contents of the file given by $filename
+     */
+    public function fileGetContents($filename, $force_read = false)
+    {
+        if (!empty($this->web_site)) {
+            return $this->web_site->fileGetContents($filename, $force_read);
+        }
+        return file_get_contents($filename);
+    }
+    /**
+     * Either a wrapper for file_put_contents, or if a WebSite object is being
+     * used to serve pages, writes $data to the persistent file with name
+     * $filename. Saves a copy in the RAM cache if there is a copy already
+     * there.
+     *
+     * @param string $filename name of file to write to persistent storages
+     * @param string $data string of data to store in file
+     */
+    public function filePutContents(string $filename, string $data): bool|int
+    {
+        if (!empty($this->web_site)) {
+            return $this->web_site->filePutContents($filename, $data);
+        }
+        return file_put_contents($filename, $data);
+    }
+    /**
+     * Retrieve data associated with a key that has been put in the cache
+     *
+     * @param string $key the key to look up
+     * @return mixed|false the data associated with the key if it exists, false
+     *     otherwise
+     */
+    public function get(string $key): mixed
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        $this->cache_file = $checksum_dir . "/c" . webencode($key);
+        if (file_exists($this->cache_file)) {
+            $this->updateCache($key);
+            return unserialize($this->fileGetContents($this->cache_file));
+        }
+        return false;
+    }
+    /**
+     * Stores in the file cache a key-value pair
+     *
+     * @param string $key to associate with value
+     * @param mixed $value to store
+     */
+    public function set(string $key, mixed $value): void
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        if (!file_exists($checksum_dir)) {
+            mkdir($checksum_dir);
+            chmod($checksum_dir, 0777);
+        }
+        $cache_file = "$checksum_dir/c".webencode($key);
+        $this->updateCache($key);
+        $this->filePutContents($cache_file, serialize($value));
+    }
+    /**
+     * Makes a 0 - self::NUMBER_OF_BINS value out of the provided key
+     *
+     * @param string $key to convert to a random value between
+     *     0 - self::NUMBER_OF_BINS
+     * @return int value between 0 and self::NUMBER_OF_BINS
+     */
+    public function checksum(string $key): int
+    {
+        $len = strlen($key);
+        $value = 0;
+        for ($i = 0; $i < $len; $i++) {
+            $value += ord($key[$i]);
+        }
+        return ($value % self::NUMBER_OF_BINS);
+    }
+    /**
+     * Deletes cache key value files and ram copies of key values stored in the
+     * file cache
+     */
+    public function clear(): void
+    {
+        if (!empty($this->web_site)) {
+            $this->web_site->clearFileCache();
+        }
+        if (is_dir($this->dir_name)) {
+            $db_class = C\NS_DATASOURCES . ucfirst(C\DBMS)."Manager";
+            $db = new $db_class();
+            $db->unlinkRecursive($this->dir_name, false);
+        }
+    }
+    /**
+     * Used to mark a cache item, and keep track of rounds according to the
+     * marker algorithm. This function determine if the cache is too full
+     * and if so eject an item.
+     *
+     * @param string $key that was just read from or written to. Might need
+     *  to be marked according to Marker algorithm
+     */
+    protected function updateCache(string $key): void
+    {
+        $checksum_block = $this->checksum($key);
+        $checksum_dir = $this->dir_name . "/$checksum_block";
+        $marker_file = "$checksum_dir/cache_markers.txt";
+        if (!file_exists($checksum_dir)) {
+            mkdir($checksum_dir);
+            chmod($checksum_dir, 0777);
+        }
+        if (file_exists($marker_file)) {
+            $data = unserialize($this->fileGetContents($marker_file));
+        } else {
+            $data = [];
+        }
+        if (empty($data['MARKED'])) {
+            $data['MARKED'] = [];
+        }
+        if (empty($data['UNMARKED'])) {
+            $data['UNMARKED'] = [];
+        }
+        $now = time();
+        if (empty($data['TIME'])) {
+            $data['TIME'] = $now;
+        }
+        if (empty($data['UNMARKED'][$key]) && empty($data['MARKED'][$key])) {
+            $data['UNMARKED'][$key] = true;
+        }
+        if (!empty($data['UNMARKED'][$key])) {
+            $data['MARKED'][$key] = true;
+            unset($data['UNMARKED'][$key]);
+        }
+        $num_marked = count($data['MARKED']);
+        if ($num_marked > self::MAX_FILES_IN_A_BIN) {
+            $data['UNMARKED'] = array_merge($data['UNMARKED'], $data['MARKED']);
+            $data['MARKED'] = [];
+        }
+        $num_unmarked = count($data['UNMARKED']);
+        $total_in_cache = $num_marked + $num_unmarked;
+        if ($total_in_cache > self::MAX_FILES_IN_A_BIN) {
+            $num_delete = $total_in_cache - self::MAX_FILES_IN_A_BIN;
+            $num_unmarked_delete = min($num_unmarked, $num_delete);
+            for ($i = 0; $i < $num_unmarked_delete; $i++) {
+                $keys = array_keys($data['UNMARKED']);
+                $eject_key = mt_rand(0, $num_unmarked - 1);
+                unset($data['UNMARKED'][$eject_key]);
+                $num_unmarked--;
+                $delete_file = $checksum_dir . "/" . webencode($eject_key);
+                if (file_exists($delete_file)) {
+                    unlink($delete_file);
+                }
+            }
+            if ($now - $data["TIME"] > C\MIN_QUERY_CACHE_TIME) {
+                $in_cache_files = array_flip(glob($checksum_dir . "/c*"));
+                $keys = array_keys($data['UNMARKED']);
+                foreach ($keys as $check_key) {
+                    $check_file = $checksum_dir . "/" . webencode($check_key);
+                    if (isset($in_cache_files[$check_file])) {
+                        unset($in_cache_files[$check_file]);
+                    }
+                }
+                foreach ($in_cache_files as $to_delete => $num) {
+                    unlink($to_delete);
+                }
+                $data["TIME"] = $now;
+            }
+        }
+        $this->filePutContents($marker_file, serialize($data));
+    }
+}
\ No newline at end of file
diff --git a/tests/FileCacheTest.php b/tests/FileCacheTest.php
new file mode 100644
index 000000000..649237ecc
--- /dev/null
+++ b/tests/FileCacheTest.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * SeekQuarry/Yioop --
+ * Open Source Pure PHP Search Engine, Crawler, and Indexer
+ *
+ * Copyright (C) 2009 - 2023  Chris Pollett chris@pollett.org
+ *
+ * LICENSE:
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ * END LICENSE
+ *
+ *
+ * @author Rushikesh Padia padiarushi3012@gmail.com
+ * @license https://www.gnu.org/licenses/ GPL3
+ * @link https://www.seekquarry.com/
+ * @copyright 2009 - 2023
+ * @filesource
+ */
+namespace seekquarry\yioop\tests;
+
+use seekquarry\yioop\configs as C;
+use seekquarry\yioop\library\FileCache;
+use seekquarry\yioop\library\UnitTest;
+
+/**
+ * Used to test that the FileCache class properly caches data
+ *
+ * @author Rushikesh Padia
+ */
+class FileCacheTest extends UnitTest
+{
+    /**
+     * We'll set up one Cache for testing purpose
+     */
+    public function setUp(): void
+    {
+            $dir = C\TEST_DIR . "/test_files/cache";
+        $this->test_objects["CACHE"] = new FileCache($dir);
+    }
+    /**
+     * Since a Trie is not a PersistentStructure we don't need to do
+     * anything to tear it down
+     */
+    public function tearDown()
+    {
+    }
+    /**
+     * Check if we add something into our Cache is retrieved with the key
+     */
+    public function addTestCase(): void
+    {
+        $this->test_objects['CACHE']->set("testKey", "testValue");
+        $this->assertEqual($this->test_objects['CACHE']->get("testKey"),
+            "testValue",
+            "Get key method should return correct value from the cache");
+    }
+    /**
+     * Check that if cache is cleared
+     */
+    public function clearTestCase(): void
+    {
+        $this->test_objects['CACHE']->set("testKey1", "testValue1");
+        $this->test_objects['CACHE']->set("testKey2", "testValue2");
+        $this->test_objects['CACHE']->clear();
+        $this->assertFalse($this->test_objects['CACHE']->get("testKey1"),
+            "If key not in Cache should get false");
+        $this->assertFalse($this->test_objects['CACHE']->get("testKey2"),
+            "If key not in Cache should get false");
+    }
+}
\ No newline at end of file
-- 
2.25.1

