Passed
Push — master ( 64c6db...94fe0c )
by Harry
01:43
created

StreamWriter::write()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 14
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 20
rs 9.4888
ccs 13
cts 14
cp 0.9286
crap 5.009
1
<?php
2
/**
3
 * This file is part of graze/dog-statsd
4
 *
5
 * Copyright (c) 2017 Nature Delivered Ltd. <https://www.graze.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license https://github.com/graze/dog-statsd/blob/master/LICENSE.md
11
 * @link    https://github.com/graze/dog-statsd
12
 */
13
14
namespace Graze\DogStatsD\Stream;
15
16
use Graze\DogStatsD\Exception\ConnectionException;
17
18
/**
19
 * StreamWriter will attempt to write a message to a udp socket.
20
 *
21
 * If the connection fails, it will never try and reconnect to prevent application blocking
22
 */
23
class StreamWriter implements WriterInterface
24
{
25
    /**
26
     * Seconds to wait (as a base) for exponential back-off on connection
27
     *
28
     * minDelay = RETRY_INTERVAL * (2 ^ num_failed_attempts)
29
     *
30
     * e.g.
31
     * 0, 0.1 0.2 0.4 0.8 1.6 3.2 6.4 12.8 25.6 51.2 102.4 etc...
32
     */
33
    const RETRY_INTERVAL = 0.1;
34
35
    /**
36
     * Maximum length of a string to send
37
     */
38
    const MAX_SEND_LENGTH = 1024;
39
40
    const ON_ERROR_ERROR     = 'error';
41
    const ON_ERROR_EXCEPTION = 'exception';
42
    const ON_ERROR_IGNORE    = 'ignore';
43
44
    /** @var resource|null */
45
    protected $socket;
46
    /** @var string */
47
    private $host;
48
    /** @var int */
49
    private $port;
50
    /** @var string */
51
    private $onError;
52
    /** @var float|null */
53
    private $timeout;
54
    /** @var string */
55
    private $instance;
56
    /** @var int */
57
    private $numFails = 0;
58
    /** @var float */
59
    private $waitTill = 0.0;
60
61
    /**
62
     * @param string     $instance
63
     * @param string     $host
64
     * @param int        $port
65
     * @param string     $onError What to do on connection error
66
     * @param float|null $timeout
67
     */
68 47
    public function __construct(
69
        $instance = 'writer',
70
        $host = '127.0.0.1',
71
        $port = 8125,
72
        $onError = self::ON_ERROR_EXCEPTION,
73
        $timeout = null
74
    ) {
75 47
        $this->instance = $instance;
76 47
        $this->host = $host;
77 47
        $this->port = $port;
78 47
        $this->onError = $onError;
79 47
        $this->timeout = $timeout;
80 47
    }
81
82 7
    public function __destruct()
83
    {
84 7
        if ($this->socket) {
85
            // the reason for this failing is that it is already closed, so ignore the result and not messing with
86
            // parent classes
87 6
            @fclose($this->socket);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

87
            /** @scrutinizer ignore-unhandled */ @fclose($this->socket);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
88
        }
89 7
    }
90
91
    /**
92
     * @param string $message
93
     *
94
     * @return bool
95
     */
96 47
    public function write($message)
97
    {
98 47
        $this->ensureConnection();
99 46
        if ($this->socket) {
100 43
            $totalLength = strlen($message);
101 43
            $retries = 1;
102 43
            for ($written = 0; $written < $totalLength; $written += $response) {
103 43
                $response = @fwrite($this->socket, substr($message, $written), static::MAX_SEND_LENGTH);
104 43
                if ($response === false) {
105 1
                    if ($retries-- > 0) {
106 1
                        $this->socket = $this->connect();
107 1
                        $response = 0;
108
                    } else {
109
                        return false;
110
                    }
111
                }
112
            }
113 43
            return ($written === $totalLength);
114
        }
115 3
        return false;
116
    }
117
118
    /**
119
     * Ensure that we are currently connected to the socket
120
     */
121 47
    protected function ensureConnection()
122
    {
123 47
        if ((!$this->socket) && ($this->canConnect())) {
124 46
            $this->socket = $this->connect();
125
        }
126 46
    }
127
128
    /**
129
     * @return bool
130
     */
131 47
    protected function canConnect()
132
    {
133 47
        return (microtime(true) > $this->waitTill);
134
    }
135
136
    /**
137
     * Attempt to connect to a stream
138
     *
139
     * @return null|resource
140
     */
141 47
    protected function connect()
142
    {
143 47
        $socket = @fsockopen('udp://' . $this->host, $this->port, $errno, $errstr, $this->timeout);
144 47
        if ($socket === false) {
145 4
            $this->waitTill = microtime(true) + (static::RETRY_INTERVAL * (pow(2, $this->numFails++)));
146
147 4
            switch ($this->onError) {
148 4
                case static::ON_ERROR_ERROR:
149 1
                    trigger_error(
150 1
                        sprintf('StatsD server connection failed (udp://%s:%d)', $this->host, $this->port),
151 1
                        E_USER_WARNING
152
                    );
153 1
                    break;
154 3
                case static::ON_ERROR_EXCEPTION:
155 4
                    throw new ConnectionException($this->instance, '(' . $errno . ') ' . $errstr);
156
            }
157
        } else {
158 43
            $this->numFails = 0;
159 43
            $this->waitTill = 0.0;
160
161 43
            $sec = (int) $this->timeout;
162 43
            $ms = (int) (($this->timeout - $sec) * 1000);
163 43
            stream_set_timeout($socket, $sec, $ms);
164
        }
165 46
        return $socket;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $socket could also return false which is incompatible with the documented return type null|resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
166
    }
167
}
168