Completed
Pull Request — master (#13)
by Harry
02:22
created

StreamWriter::__destruct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 0
crap 2
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
    const ON_ERROR_ERROR     = 'error';
36
    const ON_ERROR_EXCEPTION = 'exception';
37
    const ON_ERROR_IGNORE    = 'ignore';
38
39
    /** @var resource|null */
40
    protected $socket;
41
    /** @var string */
42
    private $host;
43
    /** @var int */
44
    private $port;
45
    /** @var string */
46
    private $onError;
47
    /** @var float|null */
48
    private $timeout;
49
    /** @var string */
50
    private $instance;
51
    /** @var int */
52
    private $numFails = 0;
53
    /** @var float */
54
    private $waitTill = 0.0;
55
56
    /**
57
     * @param string     $instance
58
     * @param string     $host
59
     * @param int        $port
60
     * @param string     $onError What to do on connection error
61
     * @param float|null $timeout
62
     */
63 39
    public function __construct(
64
        $instance = 'writer',
65
        $host = '127.0.0.1',
66
        $port = 8125,
67
        $onError = self::ON_ERROR_EXCEPTION,
68
        $timeout = null
69
    ) {
70 39
        $this->instance = $instance;
71 39
        $this->host = $host;
72 39
        $this->port = $port;
73 39
        $this->onError = $onError;
74 39
        $this->timeout = $timeout;
75 39
    }
76
77 3
    public function __destruct()
78
    {
79 3
        if ($this->socket) {
80
            // the reason for this failing is that it is already closed, so ignore the result and not messing with
81
            // parent classes
82 2
            @fclose($this->socket);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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...
83 2
        }
84 3
    }
85
86
    /**
87
     * @param string $message
88
     *
89
     * @return bool
90
     */
91 39
    public function write($message)
92
    {
93 39
        $this->ensureConnection();
94 38
        if ($this->socket) {
95 35
            if (@fwrite($this->socket, $message) === false) {
96
                // attempt to re-send on socket resource failure
97 1
                $this->socket = $this->connect();
98 1
                if ($this->socket) {
99 1
                    return (@fwrite($this->socket, $message) !== false);
100
                }
101
            } else {
102 35
                return true;
103
            }
104
        }
105 3
        return false;
106
    }
107
108
    /**
109
     * Ensure that we are currently connected to the socket
110
     */
111 39
    protected function ensureConnection()
112
    {
113 39
        if ((!$this->socket) && ($this->canConnect())) {
114 38
            $this->socket = $this->connect();
115 37
        }
116 38
    }
117
118
    /**
119
     * @return bool
120
     */
121 39
    protected function canConnect()
122
    {
123 39
        return (microtime(true) > $this->waitTill);
124
    }
125
126
    /**
127
     * Attempt to connect to a stream
128
     *
129
     * @return null|resource
130
     */
131 39
    protected function connect()
132
    {
133 39
        $socket = @fsockopen('udp://' . $this->host, $this->port, $errno, $errstr, $this->timeout);
134 39
        if ($socket === false) {
135 4
            $this->waitTill = microtime(true) + (static::RETRY_INTERVAL * (pow(2, $this->numFails++)));
136
137 4
            switch ($this->onError) {
138 4
                case static::ON_ERROR_ERROR:
139 1
                    trigger_error(
140 1
                        sprintf('StatsD server connection failed (udp://%s:%d)', $this->host, $this->port),
141
                        E_USER_WARNING
142 1
                    );
143 1
                    break;
144 3
                case static::ON_ERROR_EXCEPTION:
145 1
                    throw new ConnectionException($this->instance, '(' . $errno . ') ' . $errstr);
146 3
            }
147 3
        } else {
148 35
            $this->numFails = 0;
149 35
            $this->waitTill = 0.0;
150
151 35
            $sec = (int) $this->timeout;
152 35
            $ms = (int) (($this->timeout - $sec) * 1000);
153 35
            stream_set_timeout($socket, $sec, $ms);
154
        }
155 38
        return $socket;
156
    }
157
}
158