Manager::attemptNextPeer()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 14
nc 1
nop 1
dl 0
loc 22
ccs 5
cts 5
cp 1
crap 1
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BitWasp\Bitcoin\Networking\Peer;
6
7
use BitWasp\Bitcoin\Networking\Settings\NetworkSettings;
8
use BitWasp\Bitcoin\Networking\Structure\NetworkAddress;
9
use BitWasp\Bitcoin\Networking\Structure\NetworkAddressInterface;
10
use Evenement\EventEmitter;
11
use React\Promise\Deferred;
12
use React\Promise\RejectedPromise;
13
use React\Promise\Timer\TimeoutException;
14
15
class Manager extends EventEmitter
16
{
17
    /**
18
     * @var Connector
19
     */
20
    private $connector;
21
22
    /**
23
     * @var Peer[]
24
     */
25
    private $outPeers = [];
26
27
    /**
28
     * @var Peer[]
29
     */
30
    private $inPeers = [];
31
32
    /**
33
     * @var int
34
     */
35
    private $nOutPeers = 0;
36
37
    /**
38
     * @var int
39
     */
40 6
    private $nInPeers = 0;
41
42 6
    /**
43 6
     * @var NetworkSettings
44
     */
45
    private $settings;
46
47
    /**
48
     * Manager constructor.
49
     * @param Connector $connector
50
     * @param NetworkSettings $settings
51 3
     */
52
    public function __construct(Connector $connector, NetworkSettings $settings)
53 3
    {
54
        $this->connector = $connector;
55 3
        $this->settings = $settings;
56 3
    }
57 3
58
    /**
59 3
     * Store the newly connected peer, and trigger a new connection if they go away.
60 3
     *
61 3
     * @param Peer $peer
62
     * @return Peer
63
     */
64
    public function registerOutboundPeer(Peer $peer): Peer
65
    {
66
        $next = $this->nOutPeers++;
67 3
        $peer->on('close', function ($peer) use ($next) {
68
            $this->emit('disconnect', [$peer]);
69 3
            unset($this->outPeers[$next]);
70 3
        });
71
72
        $this->outPeers[$next] = $peer;
73 3
        $this->emit('outbound', [$peer]);
74 3
        return $peer;
75 3
    }
76
77
    /**
78
     * @param Peer $peer
79
     */
80
    public function registerInboundPeer(Peer $peer)
81 3
    {
82
        $next = $this->nInPeers++;
83
        $this->inPeers[$next] = $peer;
84 3
        $peer->on('close', function () use ($next) {
85 3
            unset($this->inPeers[$next]);
86
        });
87 3
        $this->emit('inbound', [$peer]);
88
    }
89
90
    /**
91
     * @param Listener $listener
92
     * @return $this
93
     */
94
    public function registerListener(Listener $listener)
95 3
    {
96
        $listener->on('connection', function (Peer $peer) {
97 3
            $this->registerInboundPeer($peer);
98
        });
99
100
        return $this;
101
    }
102
103
    /**
104 3
     * @param NetworkAddress $address
105
     * @return \React\Promise\PromiseInterface
106 3
     * @throws \Exception
107
     */
108
    public function connect(NetworkAddressInterface $address)
109
    {
110 3
        return $this->connector->connect($address);
111 2
    }
112
113
    /**
114
     * @param Locator $locator
115
     * @return \React\Promise\Promise|\React\Promise\PromiseInterface
116
     */
117
    public function getAnotherPeer(Locator $locator)
118
    {
119
        $deferred = new Deferred();
120 3
121 3
        // Otherwise, rely on the Locator.
122
        try {
123 3
            $deferred->resolve($locator->popAddress());
124
        } catch (\Exception $e) {
125 3
            $locator->queryDnsSeeds()->then(
126 3
                function () use ($deferred, $locator) {
127 1
                    $deferred->resolve($locator->popAddress());
128 2
                },
129 1
                function ($error): RejectedPromise {
130 2
                    return new RejectedPromise($error);
131 3
                }
132 1
            );
133 3
        }
134
135
        return $deferred->promise();
136
    }
137
138
    /**
139
     * @param Locator $locator
140
     * @return \React\Promise\Promise|\React\Promise\PromiseInterface
141 3
     */
142
    public function attemptNextPeer(Locator $locator)
143 3
    {
144 3
        $attempt = new Deferred();
145 3
146 2
        $this
147
            ->getAnotherPeer($locator)
148 3
            ->then(function (NetworkAddress $address) use ($attempt) {
149
                return $this->connect($address)->then(
150
                    function (Peer $peer) use ($attempt) {
151
                        $this->registerOutboundPeer($peer);
152
                        $attempt->resolve($peer);
153
                        return $peer;
154
                    },
155
                    function (\Exception $error) use ($attempt) {
156
                        $attempt->reject($error);
157
                    }
158
                );
159
            }, function ($error) use ($attempt) {
160
                $attempt->reject($error);
161
            });
162
163
        return $attempt->promise();
164
    }
165
166
    /**
167
     * @param Locator $locator
168
     * @param int $retries
169
     * @return \React\Promise\PromiseInterface
170
     */
171
    public function connectNextPeer(Locator $locator, int $retries = null)
172
    {
173
        if ($retries === null) {
174
            $retries = $this->settings->getMaxConnectRetries();
175
        }
176
177
        if (!(is_integer($retries) && $retries >= 0)) {
178
            throw new \InvalidArgumentException("Invalid retry count, must be an integer greater than zero");
179
        }
180
181
        $errorBack = function ($error) use ($locator, $retries) {
182
            $allowContinue = false;
183
            if ($error instanceof \RuntimeException) {
184
                if ($error->getMessage() === "Connection refused") {
185
                    $allowContinue = true;
186
                }
187
            }
188
189
            if ($error instanceof TimeoutException) {
190
                $allowContinue = true;
191
            }
192
193
            if (!$allowContinue) {
194
                throw $error;
195
            }
196
197
            if (0 >= $retries) {
198
                throw new \RuntimeException("Connection to peers failed: too many attempts");
199
            }
200
201
            return $this->connectNextPeer($locator, $retries - 1);
202
        };
203
204
        return $this
205
            ->attemptNextPeer($locator)
206
            ->then(null, $errorBack);
207
    }
208
209
    /**
210
     * @param Locator $locator
211
     * @param int $n
212
     * @return \React\Promise\Promise
213
     */
214
    public function connectToPeers(Locator $locator, int $n)
215
    {
216
        $peers = [];
217
        for ($i = 0; $i < $n; $i++) {
218
            $peers[$i] = $this->connectNextPeer($locator);
219
        }
220
221
        return \React\Promise\all($peers);
222
    }
223
}
224