Completed
Pull Request — master (#447)
by Alexandru
10:08
created

RedisStatisticsLogger::save()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 9.232
c 0
b 0
f 0
cc 4
nc 1
nop 0
1
<?php
2
3
namespace BeyondCode\LaravelWebSockets\Statistics\Logger;
4
5
use BeyondCode\LaravelWebSockets\Apps\App;
6
use BeyondCode\LaravelWebSockets\PubSub\ReplicationInterface;
7
use BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver;
8
use BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager;
9
use Illuminate\Cache\RedisLock;
10
use Illuminate\Support\Facades\Redis;
11
12
class RedisStatisticsLogger implements StatisticsLogger
13
{
14
    /**
15
     * The Channel manager.
16
     *
17
     * @var \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager
18
     */
19
    protected $channelManager;
20
21
    /**
22
     * The statistics driver instance.
23
     *
24
     * @var \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver
25
     */
26
    protected $driver;
27
28
    /**
29
     * The Redis manager instance.
30
     *
31
     * @var \Illuminate\Redis\RedisManager
32
     */
33
    protected $redis;
34
35
    /**
36
     * Initialize the logger.
37
     *
38
     * @param  \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManager  $channelManager
39
     * @param  \BeyondCode\LaravelWebSockets\Statistics\Drivers\StatisticsDriver  $driver
40
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
41
     */
42
    public function __construct(ChannelManager $channelManager, StatisticsDriver $driver)
43
    {
44
        $this->channelManager = $channelManager;
45
        $this->driver = $driver;
46
        $this->replicator = app(ReplicationInterface::class);
0 ignored issues
show
Bug introduced by
The property replicator does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
47
48
        $this->redis = Redis::connection(
0 ignored issues
show
Documentation Bug introduced by
It seems like \Illuminate\Support\Faca...onnection', 'default')) of type object<Illuminate\Redis\Connections\Connection> is incompatible with the declared type object<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...
49
            config('websockets.replication.redis.connection', 'default')
50
        );
51
    }
52
53
    /**
54
     * Handle the incoming websocket message.
55
     *
56
     * @param  mixed  $appId
57
     * @return void
58
     */
59
    public function webSocketMessage($appId)
60
    {
61
        $this->ensureAppIsSet($appId)
62
            ->__call('hincrby', [$this->getHash($appId), 'websocket_message_count', 1]);
63
    }
64
65
    /**
66
     * Handle the incoming API message.
67
     *
68
     * @param  mixed  $appId
69
     * @return void
70
     */
71
    public function apiMessage($appId)
72
    {
73
        $this->ensureAppIsSet($appId)
74
            ->__call('hincrby', [$this->getHash($appId), 'api_message_count', 1]);
75
    }
76
77
    /**
78
     * Handle the new conection.
79
     *
80
     * @param  mixed  $appId
81
     * @return void
82
     */
83
    public function connection($appId)
84
    {
85
        // Increment the current connections count by 1.
86
        $incremented = $this->ensureAppIsSet($appId)
87
            ->__call('hincrby', [$this->getHash($appId), 'current_connection_count', 1]);
88
89
        $incremented->then(function ($currentConnectionCount) use ($appId) {
90
            // Get the peak connections count from Redis.
91
            $peakConnectionCount = $this->replicator
92
                ->getPublishClient()
93
                ->__call('hget', [$this->getHash($appId), 'peak_connection_count']);
94
95
            $peakConnectionCount->then(function ($currentPeakConnectionCount) use ($currentConnectionCount, $appId) {
96
                // Extract the greatest number between the current peak connection count
97
                // and the current connection number.
98
99
                $peakConnectionCount = is_null($currentPeakConnectionCount)
100
                    ? $currentConnectionCount
101
                    : max($currentPeakConnectionCount, $currentConnectionCount);
102
103
                // Then set it to the database.
104
                $this->replicator
105
                    ->getPublishClient()
106
                    ->__call('hset', [$this->getHash($appId), 'peak_connection_count', $peakConnectionCount]);
107
            });
108
        });
109
    }
110
111
    /**
112
     * Handle disconnections.
113
     *
114
     * @param  mixed  $appId
115
     * @return void
116
     */
117
    public function disconnection($appId)
118
    {
119
        // Decrement the current connections count by 1.
120
        $decremented = $this->ensureAppIsSet($appId)
121
            ->__call('hincrby', [$this->getHash($appId), 'current_connection_count', -1]);
122
123
        $decremented->then(function ($currentConnectionCount) use ($appId) {
124
            // Get the peak connections count from Redis.
125
            $peakConnectionCount = $this->replicator
126
                ->getPublishClient()
127
                ->__call('hget', [$this->getHash($appId), 'peak_connection_count']);
128
129
            $peakConnectionCount->then(function ($currentPeakConnectionCount) use ($currentConnectionCount, $appId) {
130
                // Extract the greatest number between the current peak connection count
131
                // and the current connection number.
132
133
                $peakConnectionCount = is_null($currentPeakConnectionCount)
134
                    ? $currentConnectionCount
135
                    : max($currentPeakConnectionCount, $currentConnectionCount);
136
137
                // Then set it to the database.
138
                $this->replicator
139
                    ->getPublishClient()
140
                    ->__call('hset', [$this->getHash($appId), 'peak_connection_count', $peakConnectionCount]);
141
            });
142
        });
143
    }
144
145
    /**
146
     * Save all the stored statistics.
147
     *
148
     * @return void
149
     */
150
    public function save()
151
    {
152
        $this->lock()->get(function () {
153
            $setMembers = $this->replicator
154
                ->getPublishClient()
155
                ->__call('smembers', ['laravel-websockets:apps']);
156
157
            $setMembers->then(function ($members) {
158
                foreach ($members as $appId) {
159
                    $member = $this->replicator
160
                        ->getPublishClient()
161
                        ->__call('hgetall', [$this->getHash($appId)]);
162
163
                    $member->then(function ($statistic) use ($appId) {
164
                        if (! $statistic) {
165
                            return;
166
                        }
167
168
                        // Statistics come into a list where the keys are on even indexes
169
                        // and the values are on odd indexes. This way, we know which
170
                        // ones are keys and which ones are values and their get combined
171
                        // later to form the key => value array
172
173
                        [$keys, $values] = collect($statistic)->partition(function ($value, $key) {
0 ignored issues
show
Bug introduced by
The variable $keys does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $values does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
174
                            return $key % 2 === 0;
175
                        });
176
177
                        $statistic = array_combine($keys->all(), $values->all());
178
179
                        $this->createRecord($statistic, $appId);
180
181
                        $this->channelManager
182
                            ->getGlobalConnectionsCount($appId)
183
                            ->then(function ($currentConnectionCount) use ($appId) {
184
                                $currentConnectionCount === 0
185
                                    ? $this->resetAppTraces($appId)
186
                                    : $this->resetStatistics($appId, $currentConnectionCount);
187
                            });
188
                    });
189
                }
190
            });
191
        });
192
    }
193
194
    /**
195
     * Ensure the app id is stored in the Redis database.
196
     *
197
     * @param  mixed  $appId
198
     * @return \Illuminate\Redis\RedisManager
199
     */
200
    protected function ensureAppIsSet($appId)
201
    {
202
        $this->replicator
203
            ->getPublishClient()
204
            ->__call('sadd', ['laravel-websockets:apps', $appId]);
205
206
        return $this->replicator->getPublishClient();
207
    }
208
209
    /**
210
     * Reset the statistics to a specific connection count.
211
     *
212
     * @param  mixed  $appId
213
     * @param  int  $currentConnectionCount
214
     * @return void
215
     */
216
    public function resetStatistics($appId, int $currentConnectionCount)
217
    {
218
        $this->replicator
219
            ->getPublishClient()
220
            ->__call('hset', [$this->getHash($appId), 'current_connection_count', $currentConnectionCount]);
221
222
        $this->replicator
223
            ->getPublishClient()
224
            ->__call('hset', [$this->getHash($appId), 'peak_connection_count', $currentConnectionCount]);
225
226
        $this->replicator
227
            ->getPublishClient()
228
            ->__call('hset', [$this->getHash($appId), 'websocket_message_count', 0]);
229
230
        $this->replicator
231
            ->getPublishClient()
232
            ->__call('hset', [$this->getHash($appId), 'api_message_count', 0]);
233
    }
234
235
    /**
236
     * Remove all app traces from the database if no connections have been set
237
     * in the meanwhile since last save.
238
     *
239
     * @param  mixed  $appId
240
     * @return void
241
     */
242
    public function resetAppTraces($appId)
243
    {
244
        $this->replicator
245
            ->getPublishClient()
246
            ->__call('hdel', [$this->getHash($appId), 'current_connection_count']);
247
248
        $this->replicator
249
            ->getPublishClient()
250
            ->__call('hdel', [$this->getHash($appId), 'peak_connection_count']);
251
252
        $this->replicator
253
            ->getPublishClient()
254
            ->__call('hdel', [$this->getHash($appId), 'websocket_message_count']);
255
256
        $this->replicator
257
            ->getPublishClient()
258
            ->__call('hdel', [$this->getHash($appId), 'api_message_count']);
259
260
        $this->replicator
261
            ->getPublishClient()
262
            ->__call('srem', ['laravel-websockets:apps', $appId]);
263
    }
264
265
    /**
266
     * Get the Redis hash name for the app.
267
     *
268
     * @param  mixed  $appId
269
     * @return string
270
     */
271
    protected function getHash($appId): string
272
    {
273
        return "laravel-websockets:app:{$appId}";
274
    }
275
276
    /**
277
     * Get a new RedisLock instance to avoid race conditions.
278
     *
279
     * @return \Illuminate\Cache\CacheLock
280
     */
281
    protected function lock()
282
    {
283
        return new RedisLock($this->redis, 'laravel-websockets:lock', 0);
0 ignored issues
show
Documentation introduced by
$this->redis is of type object<Illuminate\Redis\RedisManager>, but the function expects a object<Illuminate\Redis\Connections\Connection>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
284
    }
285
286
    /**
287
     * Create a new record using the Statistic Driver.
288
     *
289
     * @param  array  $statistic
290
     * @param  mixed  $appId
291
     * @return void
292
     */
293
    protected function createRecord(array $statistic, $appId): void
294
    {
295
        $this->driver::create([
296
            'app_id' => $appId,
297
            'peak_connection_count' => $statistic['peak_connection_count'] ?? 0,
298
            'websocket_message_count' => $statistic['websocket_message_count'] ?? 0,
299
            'api_message_count' => $statistic['api_message_count'] ?? 0,
300
        ]);
301
    }
302
}
303