Completed
Push — develop ( 8852f9...b0ea9b )
by
unknown
11:33 queued 03:35
created

WriteLockListener::onKernelController()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 42
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 1
Metric Value
dl 0
loc 42
ccs 0
cts 27
cp 0
rs 8.439
c 1
b 0
f 1
cc 6
eloc 20
nc 9
nop 1
crap 42
1
<?php
2
/**
3
 * listener that implements write locks on data altering requests with PUT and PATCH methods
4
 * and adds random waits to certain operations.
5
 */
6
namespace Graviton\RestBundle\Listener;
7
8
use Doctrine\Common\Cache\CacheProvider;
9
use Monolog\Logger;
10
use Symfony\Component\HttpFoundation\RequestStack;
11
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
12
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
13
use Symfony\Component\HttpFoundation\Request;
14
15
/**
16
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
17
 * @license  https://opensource.org/licenses/MIT MIT License
18
 * @link     http://swisscom.ch
19
 */
20
class WriteLockListener
21
{
22
23
    /**
24
     * @var Logger
25
     */
26
    private $logger;
27
28
    /**
29
     * @var RequestStack
30
     */
31
    private $requestStack;
32
33
    /**
34
     * @var CacheProvider
35
     */
36
    private $cache;
37
38
    /**
39
     * on these methods, we will create a lock and wait
40
     *
41
     * @var array
42
     */
43
    private $lockingMethods = [
44
        Request::METHOD_PUT,
45
        Request::METHOD_PATCH
46
    ];
47
48
    /**
49
     * on these methods, we will just wait..
50
     *
51
     * @var array
52
     */
53
    private $waitingMethods = [
54
        Request::METHOD_GET,
55
        Request::METHOD_DELETE
56
    ];
57
58
    /**
59
     * all methods we are interested in
60
     *
61
     * @var array
62
     */
63
    private $interestedMethods = [];
64
65
    /**
66
     * on these urls, we make a randomwait to randomly delay multiple incoming requests
67
     *
68
     * @var array
69
     */
70
    private $randomWaitUrls = [];
71
72
    /**
73
     * @var string
74
     */
75
    private $cacheKeyPrefix = 'writeLock-';
76
77
    /**
78
     * @var int
79
     */
80
    private $maxTime = 10;
81
82
    /**
83
     * @var int minimal delay in milliseconds
84
     */
85
    private $randomDelayMin = 200;
86
87
    /**
88
     * @var int maximal delay in milliseconds
89
     */
90
    private $randomDelayMax = 500;
91
92
    /**
93
     * @param Logger        $logger         logger
94
     * @param RequestStack  $requestStack   request stack
95
     * @param CacheProvider $cache          cache
96
     * @param array         $randomWaitUrls urls we randomly wait on
97
     */
98
    public function __construct(
99
        Logger $logger,
100
        RequestStack $requestStack,
101
        CacheProvider $cache,
102
        array $randomWaitUrls
103
    ) {
104
        $this->logger = $logger;
105
        $this->requestStack = $requestStack;
106
        $this->cache = $cache;
107
        $this->interestedMethods = array_merge($this->lockingMethods, $this->waitingMethods);
108
        $this->randomWaitUrls = $randomWaitUrls;
109
    }
110
111
    /**
112
     * all "waiting" methods wait until no lock is around.. "writelock" methods wait and create a lock
113
     *
114
     * @param FilterControllerEvent $event response listener event
115
     *
116
     * @return void
117
     */
118
    public function onKernelController(FilterControllerEvent $event)
119
    {
120
        $currentMethod = $this->requestStack->getCurrentRequest()->getMethod();
121
122
        // ignore not defined methods..
123
        if (!in_array($currentMethod, $this->interestedMethods)) {
124
            return;
125
        }
126
127
        $url = $this->requestStack->getCurrentRequest()->getPathInfo();
128
        $cacheKey = $this->cacheKeyPrefix.$url;
129
130
        // should we do a random delay here? only applies to writing methods!
131
        if (in_array($currentMethod, $this->lockingMethods) &&
132
            $this->doRandomDelay($url)
133
        ) {
134
            $delay = rand($this->randomDelayMin, $this->randomDelayMax) * 1000;
135
            $this->logger->info("LOCK CHECK DELAY BY ".$delay." = ".$cacheKey);
136
137
            usleep(rand($this->randomDelayMin, $this->randomDelayMax) * 1000);
138
        }
139
140
        $this->logger->info("LOCK CHECK START = ".$cacheKey);
141
142
        // check for existing one
143
        while ($this->cache->fetch($cacheKey) === true) {
144
            usleep(250000);
145
        }
146
147
        $this->logger->info("LOCK CHECK FINISHED = ".$cacheKey);
148
149
        if (in_array($currentMethod, $this->waitingMethods)) {
150
            // current method just wants to wait..
151
            return;
152
        }
153
154
        // create new
155
        $this->cache->save($cacheKey, true, $this->maxTime);
156
        $this->logger->info("LOCK ADD = ".$cacheKey);
157
158
        $event->getRequest()->attributes->set('writeLockOn', $cacheKey);
159
    }
160
161
    /**
162
     * release the lock
163
     *
164
     * @param FilterResponseEvent $event response listener event
165
     *
166
     * @return void
167
     */
168
    public function onKernelResponse(FilterResponseEvent $event)
169
    {
170
        $lockName = $event->getRequest()->attributes->get('writeLockOn', null);
171
        if (!is_null($lockName)) {
172
            $this->cache->delete($lockName);
173
            $this->logger->info("LOCK REMOVED = ".$lockName);
174
        }
175
    }
176
177
    /**
178
     * if we should randomly wait on current request
179
     *
180
     * @param string $url the current url
181
     *
182
     * @return boolean true if yes, false otherwise
183
     */
184
    private function doRandomDelay($url)
185
    {
186
        return array_reduce(
187
            $this->randomWaitUrls,
188
            function ($carry, $value) use ($url) {
189
                if ($carry !== true) {
190
                    return (strpos($url, $value) === 0);
191
                } else {
192
                    return $carry;
193
                }
194
            }
195
        );
196
    }
197
}
198