Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
StorageManager
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 12
870
0.00% covered (danger)
0.00%
0 / 1
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 putInCache
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 exists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getFromCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 persistCacheEntry
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 getStorage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setStorage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 set
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 has
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 del
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 persist
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
19 *
20 */
21
22namespace oat\taoQtiTest\models\runner;
23
24use oat\oatbox\service\ConfigurableService;
25use oat\tao\model\state\StateStorage;
26
27/**
28 * Class StorageManager
29 *
30 * Manage the storage in order to centralize its access.
31 * The reading of data can be done at any time, the first call will put the data in memory cache.
32 * Each change will only update the cache, and mark it to be processed upon persisting.
33 * The actual writing should be done once, at the end of the request, by invoking the `persist()` method.
34 *
35 * @package oat\taoQtiTest\models\classes\runner
36 * @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
37 */
38class StorageManager extends ConfigurableService
39{
40    public const SERVICE_ID = 'taoQtiTest/StorageManager';
41
42    /**
43     * The data does not exist in the storage
44     */
45    public const STATE_NOT_FOUND = -1;
46
47    /**
48     * The data is aligned with the storage
49     */
50    public const STATE_ALIGNED = 0;
51
52    /**
53     * The data is pending write to the storage
54     */
55    public const STATE_PENDING_WRITE = 1;
56
57    /**
58     * The data is pending delete from the storage
59     */
60    public const STATE_PENDING_DELETE = 2;
61
62    /**
63     * Link to the actual storage adapter
64     * @var StateStorage
65     */
66    protected $storage;
67
68    /**
69     * In memory cache for read/pending data
70     * @var array
71     */
72    protected $cache = [];
73
74    /**
75     * Gets a key that will be used to cache data.
76     *
77     * @param string $userId
78     * @param string $callId
79     * @return string
80     */
81    protected function getCacheKey($userId, $callId)
82    {
83        return $userId . '/' . $callId;
84    }
85
86    /**
87     * Puts data in the cache. Maintain the link to the userId/callId pair.
88     * Also keep the dirty state that will be used when persisting the data to the actual storage.
89     *
90     * @param string $key
91     * @param string $userId
92     * @param string $callId
93     * @param string $data
94     * @param int $state
95     */
96    protected function putInCache($key, $userId, $callId, $data, $state = self::STATE_ALIGNED)
97    {
98        $this->cache[$key] = [
99            'userId' => $userId,
100            'callId' => $callId,
101            'state' => $state,
102            'data' => $data
103        ];
104    }
105
106    /**
107     * Checks if a dataset exists for the provided key.
108     *
109     * @param string $key
110     * @return bool
111     */
112    protected function exists($key)
113    {
114        return isset($this->cache[$key])
115            && in_array($this->cache[$key]['state'], [self::STATE_ALIGNED, self::STATE_PENDING_WRITE]);
116    }
117
118    /**
119     * Gets a dataset from the cache.
120     *
121     * @param string $key
122     * @return mixed
123     */
124    protected function getFromCache($key)
125    {
126        if ($this->exists($key)) {
127            return $this->cache[$key]['data'];
128        }
129        return null;
130    }
131
132    /**
133     * Persists a cache entry and update its status.
134     *
135     * @param string $key
136     * @return bool
137     */
138    protected function persistCacheEntry($key)
139    {
140        $success = true;
141        if (isset($this->cache[$key])) {
142            $cache = $this->cache[$key];
143
144            switch ($cache['state']) {
145                case self::STATE_PENDING_WRITE:
146                    $success = $this->getStorage()->set($cache['userId'], $cache['callId'], $cache['data']);
147                    if (!$success) {
148                        throw new \common_exception_Error(
149                            'Can\'t write into test runner state storage at ' . static::class
150                        );
151                    }
152                    $this->cache[$key]['state'] = self::STATE_ALIGNED;
153                    break;
154
155                case self::STATE_PENDING_DELETE:
156                    $success = $this->getStorage()->del($cache['userId'], $cache['callId']);
157                    if ($success) {
158                        unset($this->cache[$key]);
159                    }
160                    break;
161            }
162        }
163        return $success;
164    }
165
166    /**
167     * @return StateStorage
168     */
169    public function getStorage()
170    {
171        if (!$this->storage) {
172            $this->storage = $this->getServiceLocator()->get(StateStorage::SERVICE_ID);
173        }
174        return $this->storage;
175    }
176
177    /**
178     * @param StateStorage $storage
179     * @return StorageManager
180     */
181    public function setStorage(StateStorage $storage)
182    {
183        $this->storage = $storage;
184        return $this;
185    }
186
187    /**
188     * Applies a dataset to be stored.
189     *
190     * @param string $userId
191     * @param string $callId
192     * @param string $data
193     * @return boolean
194     */
195    public function set($userId, $callId, $data)
196    {
197        $key = $this->getCacheKey($userId, $callId);
198        $cache = $this->getFromCache($key);
199        if (is_null($cache) || $cache != $data) {
200            $this->putInCache($key, $userId, $callId, $data, self::STATE_PENDING_WRITE);
201        }
202        return true;
203    }
204
205    /**
206     * Gets a dataset from the store using the provided keys.
207     * Will return null if the dataset doesn't exist.
208     *
209     * @param string $userId
210     * @param string $callId
211     * @return string
212     */
213    public function get($userId, $callId)
214    {
215        $key = $this->getCacheKey($userId, $callId);
216        if (!isset($this->cache[$key])) {
217            $data = $this->getStorage()->get($userId, $callId);
218            $state = is_null($data) ? self::STATE_NOT_FOUND : self::STATE_ALIGNED;
219            $this->putInCache($key, $userId, $callId, $data, $state);
220        }
221
222        return $this->getFromCache($key);
223    }
224
225    /**
226     * Whenever or not a dataset exists.
227     *
228     * @param string $userId
229     * @param string $callId
230     * @return boolean
231     */
232    public function has($userId, $callId)
233    {
234        $key = $this->getCacheKey($userId, $callId);
235        if (!isset($this->cache[$key])) {
236            return $this->getStorage()->has($userId, $callId);
237        }
238        return $this->exists($key);
239    }
240
241    /**
242     * Marks the the dataset to be removed from the storage.
243     *
244     * @param string $userId
245     * @param string $callId
246     * @return boolean
247     */
248    public function del($userId, $callId)
249    {
250        $key = $this->getCacheKey($userId, $callId);
251        $this->putInCache($key, $userId, $callId, null, self::STATE_PENDING_DELETE);
252        return true;
253    }
254
255    /**
256     * Sends the changes to the storage.
257     *
258     * @param string $userId
259     * @param string $callId
260     * @return bool
261     */
262    public function persist($userId = null, $callId = null)
263    {
264        if ($userId && $callId) {
265            $keys = [$this->getCacheKey($userId, $callId)];
266        } else {
267            $keys = array_keys($this->cache);
268        }
269
270        $success = true;
271        foreach ($keys as $key) {
272            if (!$this->persistCacheEntry($key)) {
273                $success = false;
274            }
275        }
276        return $success;
277    }
278}