RedisCollector::resetStatistics()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 20
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 28
rs 9.6
1
<?php
2
3
namespace BeyondCode\LaravelWebSockets\Statistics\Collectors;
4
5
use BeyondCode\LaravelWebSockets\Helpers;
6
use BeyondCode\LaravelWebSockets\Statistics\Statistic;
7
use Illuminate\Cache\RedisLock;
8
use Illuminate\Support\Facades\Redis;
9
use React\Promise\PromiseInterface;
10
11
class RedisCollector extends MemoryCollector
12
{
13
    /**
14
     * The Redis manager instance.
15
     *
16
     * @var \Illuminate\Redis\RedisManager
17
     */
18
    protected $redis;
19
20
    /**
21
     * The set name for the Redis storage.
22
     *
23
     * @var string
24
     */
25
    protected static $redisSetName = 'laravel-websockets:apps';
26
27
    /**
28
     * The lock name to use on Redis to avoid multiple
29
     * collector-to-store actions that may result
30
     * in multiple data points set to the store.
31
     *
32
     * @var string
33
     */
34
    protected static $redisLockName = 'laravel-websockets:collector:lock';
35
36
    /**
37
     * Initialize the logger.
38
     *
39
     * @return void
40
     */
41
    public function __construct()
42
    {
43
        parent::__construct();
44
45
        $this->redis = Redis::connection(
0 ignored issues
show
Documentation Bug introduced by
It seems like Illuminate\Support\Facad...onnection', 'default')) of type Illuminate\Redis\Connections\Connection is incompatible with the declared type Illuminate\Redis\RedisManager of property $redis.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
46
            config('websockets.replication.modes.redis.connection', 'default')
47
        );
48
    }
49
50
    /**
51
     * Handle the incoming websocket message.
52
     *
53
     * @param  string|int  $appId
54
     * @return void
55
     */
56
    public function webSocketMessage($appId)
57
    {
58
        $this->ensureAppIsInSet($appId)
59
            ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'websocket_messages_count', 1);
0 ignored issues
show
Bug introduced by
The method getStatsRedisHash() does not exist on BeyondCode\LaravelWebSoc...ontracts\ChannelManager. It seems like you code against a sub-type of BeyondCode\LaravelWebSoc...ontracts\ChannelManager such as BeyondCode\LaravelWebSoc...ers\RedisChannelManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

59
            ->hincrby($this->channelManager->/** @scrutinizer ignore-call */ getStatsRedisHash($appId, null), 'websocket_messages_count', 1);
Loading history...
60
    }
61
62
    /**
63
     * Handle the incoming API message.
64
     *
65
     * @param  string|int  $appId
66
     * @return void
67
     */
68
    public function apiMessage($appId)
69
    {
70
        $this->ensureAppIsInSet($appId)
71
            ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'api_messages_count', 1);
72
    }
73
74
    /**
75
     * Handle the new conection.
76
     *
77
     * @param  string|int  $appId
78
     * @return void
79
     */
80
    public function connection($appId)
81
    {
82
        // Increment the current connections count by 1.
83
        $this->ensureAppIsInSet($appId)
84
            ->hincrby(
85
                $this->channelManager->getStatsRedisHash($appId, null),
86
                'current_connections_count', 1
87
            )
88
            ->then(function ($currentConnectionsCount) use ($appId) {
89
                // Get the peak connections count from Redis.
90
                $this->channelManager
91
                    ->getPublishClient()
0 ignored issues
show
Bug introduced by
The method getPublishClient() does not exist on BeyondCode\LaravelWebSoc...ontracts\ChannelManager. It seems like you code against a sub-type of BeyondCode\LaravelWebSoc...ontracts\ChannelManager such as BeyondCode\LaravelWebSoc...ers\RedisChannelManager. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
                    ->/** @scrutinizer ignore-call */ 
92
                      getPublishClient()
Loading history...
92
                    ->hget(
93
                        $this->channelManager->getStatsRedisHash($appId, null),
94
                        'peak_connections_count'
95
                    )
96
                    ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
97
                        // Extract the greatest number between the current peak connection count
98
                        // and the current connection number.
99
                        $peakConnectionsCount = is_null($currentPeakConnectionCount)
100
                            ? $currentConnectionsCount
101
                            : max($currentPeakConnectionCount, $currentConnectionsCount);
102
103
                        // Then set it to the database.
104
                        $this->channelManager
105
                            ->getPublishClient()
106
                            ->hset(
107
                                $this->channelManager->getStatsRedisHash($appId, null),
108
                                'peak_connections_count', $peakConnectionsCount
109
                            );
110
                    });
111
            });
112
    }
113
114
    /**
115
     * Handle disconnections.
116
     *
117
     * @param  string|int  $appId
118
     * @return void
119
     */
120
    public function disconnection($appId)
121
    {
122
        // Decrement the current connections count by 1.
123
        $this->ensureAppIsInSet($appId)
124
            ->hincrby($this->channelManager->getStatsRedisHash($appId, null), 'current_connections_count', -1)
125
            ->then(function ($currentConnectionsCount) use ($appId) {
126
                // Get the peak connections count from Redis.
127
                $this->channelManager
128
                    ->getPublishClient()
129
                    ->hget($this->channelManager->getStatsRedisHash($appId, null), 'peak_connections_count')
130
                    ->then(function ($currentPeakConnectionCount) use ($currentConnectionsCount, $appId) {
131
                        // Extract the greatest number between the current peak connection count
132
                        // and the current connection number.
133
                        $peakConnectionsCount = is_null($currentPeakConnectionCount)
134
                            ? $currentConnectionsCount
135
                            : max($currentPeakConnectionCount, $currentConnectionsCount);
136
137
                        // Then set it to the database.
138
                        $this->channelManager
139
                            ->getPublishClient()
140
                            ->hset(
141
                                $this->channelManager->getStatsRedisHash($appId, null),
142
                                'peak_connections_count', $peakConnectionsCount
143
                            );
144
                    });
145
            });
146
    }
147
148
    /**
149
     * Save all the stored statistics.
150
     *
151
     * @return void
152
     */
153
    public function save()
154
    {
155
        $this->lock()->get(function () {
156
            $this->channelManager
157
                ->getPublishClient()
158
                ->smembers(static::$redisSetName)
159
                ->then(function ($members) {
160
                    foreach ($members as $appId) {
161
                        $this->channelManager
162
                            ->getPublishClient()
163
                            ->hgetall($this->channelManager->getStatsRedisHash($appId, null))
164
                            ->then(function ($list) use ($appId) {
165
                                if (! $list) {
166
                                    return;
167
                                }
168
169
                                $statistic = $this->arrayToStatisticInstance(
170
                                    $appId, Helpers::redisListToArray($list)
171
                                );
172
173
                                if ($statistic->shouldHaveTracesRemoved()) {
174
                                    return $this->resetAppTraces($appId);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->resetAppTraces($appId) targeting BeyondCode\LaravelWebSoc...ector::resetAppTraces() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
175
                                }
176
177
                                $this->createRecord($statistic, $appId);
178
179
                                $this->channelManager
180
                                    ->getGlobalConnectionsCount($appId)
181
                                    ->then(function ($currentConnectionsCount) use ($appId) {
182
                                        $currentConnectionsCount === 0 || is_null($currentConnectionsCount)
183
                                            ? $this->resetAppTraces($appId)
184
                                            : $this->resetStatistics($appId, $currentConnectionsCount);
185
                                    });
186
                            });
187
                    }
188
                });
189
        });
190
    }
191
192
    /**
193
     * Flush the stored statistics.
194
     *
195
     * @return void
196
     */
197
    public function flush()
198
    {
199
        $this->getStatistics()->then(function ($statistics) {
200
            foreach ($statistics as $appId => $statistic) {
201
                $this->resetAppTraces($appId);
202
            }
203
        });
204
    }
205
206
    /**
207
     * Get the saved statistics.
208
     *
209
     * @return PromiseInterface[array]
0 ignored issues
show
Documentation Bug introduced by
The doc comment PromiseInterface[array] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
210
     */
211
    public function getStatistics(): PromiseInterface
212
    {
213
        return $this->channelManager
214
            ->getPublishClient()
215
            ->smembers(static::$redisSetName)
216
            ->then(function ($members) {
217
                $appsWithStatistics = [];
218
219
                foreach ($members as $appId) {
220
                    $this->channelManager
221
                        ->getPublishClient()
222
                        ->hgetall($this->channelManager->getStatsRedisHash($appId, null))
223
                        ->then(function ($list) use ($appId, &$appsWithStatistics) {
224
                            $appsWithStatistics[$appId] = $this->arrayToStatisticInstance(
225
                                $appId, Helpers::redisListToArray($list)
226
                            );
227
                        });
228
                }
229
230
                return $appsWithStatistics;
231
            });
232
    }
233
234
    /**
235
     * Get the saved statistics for an app.
236
     *
237
     * @param  string|int  $appId
238
     * @return PromiseInterface[\BeyondCode\LaravelWebSockets\Statistics\Statistic|null]
0 ignored issues
show
Documentation Bug introduced by
The doc comment PromiseInterface[\Beyond...tistics\Statistic|null] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
239
     */
240
    public function getAppStatistics($appId): PromiseInterface
241
    {
242
        return $this->channelManager
243
            ->getPublishClient()
244
            ->hgetall($this->channelManager->getStatsRedisHash($appId, null))
245
            ->then(function ($list) use ($appId) {
246
                return $this->arrayToStatisticInstance(
247
                    $appId, Helpers::redisListToArray($list)
248
                );
249
            });
250
    }
251
252
    /**
253
     * Reset the statistics to a specific connection count.
254
     *
255
     * @param  string|int  $appId
256
     * @param  int  $currentConnectionCount
257
     * @return void
258
     */
259
    public function resetStatistics($appId, int $currentConnectionCount)
260
    {
261
        $this->channelManager
262
            ->getPublishClient()
263
            ->hset(
264
                $this->channelManager->getStatsRedisHash($appId, null),
265
                'current_connections_count', $currentConnectionCount
266
            );
267
268
        $this->channelManager
269
            ->getPublishClient()
270
            ->hset(
271
                $this->channelManager->getStatsRedisHash($appId, null),
272
                'peak_connections_count', max(0, $currentConnectionCount)
273
            );
274
275
        $this->channelManager
276
            ->getPublishClient()
277
            ->hset(
278
                $this->channelManager->getStatsRedisHash($appId, null),
279
                'websocket_messages_count', 0
280
            );
281
282
        $this->channelManager
283
            ->getPublishClient()
284
            ->hset(
285
                $this->channelManager->getStatsRedisHash($appId, null),
286
                'api_messages_count', 0
287
            );
288
    }
289
290
    /**
291
     * Remove all app traces from the database if no connections have been set
292
     * in the meanwhile since last save.
293
     *
294
     * @param  string|int  $appId
295
     * @return void
296
     */
297
    public function resetAppTraces($appId)
298
    {
299
        parent::resetAppTraces($appId);
300
301
        $this->channelManager
302
            ->getPublishClient()
303
            ->hdel(
304
                $this->channelManager->getStatsRedisHash($appId, null),
305
                'current_connections_count'
306
            );
307
308
        $this->channelManager
309
            ->getPublishClient()
310
            ->hdel(
311
                $this->channelManager->getStatsRedisHash($appId, null),
312
                'peak_connections_count'
313
            );
314
315
        $this->channelManager
316
            ->getPublishClient()
317
            ->hdel(
318
                $this->channelManager->getStatsRedisHash($appId, null),
319
                'websocket_messages_count'
320
            );
321
322
        $this->channelManager
323
            ->getPublishClient()
324
            ->hdel(
325
                $this->channelManager->getStatsRedisHash($appId, null),
326
                'api_messages_count'
327
            );
328
329
        $this->channelManager
330
            ->getPublishClient()
331
            ->srem(static::$redisSetName, $appId);
332
    }
333
334
    /**
335
     * Ensure the app id is stored in the Redis database.
336
     *
337
     * @param  string|int  $appId
338
     * @return \Clue\React\Redis\Client
339
     */
340
    protected function ensureAppIsInSet($appId)
341
    {
342
        $this->channelManager
343
            ->getPublishClient()
344
            ->sadd(static::$redisSetName, $appId);
345
346
        return $this->channelManager->getPublishClient();
347
    }
348
349
    /**
350
     * Get a new RedisLock instance to avoid race conditions.
351
     *
352
     * @return \Illuminate\Cache\CacheLock
353
     */
354
    protected function lock()
355
    {
356
        return new RedisLock($this->redis, static::$redisLockName, 0);
0 ignored issues
show
Bug introduced by
$this->redis of type Illuminate\Redis\RedisManager is incompatible with the type Illuminate\Redis\Connections\Connection expected by parameter $redis of Illuminate\Cache\RedisLock::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

356
        return new RedisLock(/** @scrutinizer ignore-type */ $this->redis, static::$redisLockName, 0);
Loading history...
357
    }
358
359
    /**
360
     * Transform a key-value pair to a Statistic instance.
361
     *
362
     * @param  string|int  $appId
363
     * @param  array  $stats
364
     * @return \BeyondCode\LaravelWebSockets\Statistics\Statistic
365
     */
366
    protected function arrayToStatisticInstance($appId, array $stats)
367
    {
368
        return Statistic::new($appId)
369
            ->setCurrentConnectionsCount($stats['current_connections_count'] ?? 0)
370
            ->setPeakConnectionsCount($stats['peak_connections_count'] ?? 0)
371
            ->setWebSocketMessagesCount($stats['websocket_messages_count'] ?? 0)
372
            ->setApiMessagesCount($stats['api_messages_count'] ?? 0);
373
    }
374
}
375