Skip to content

Commit 5b4e1eb

Browse files
feat(orc-452): shuffle ipfs providers on startup
1 parent fc77ac5 commit 5b4e1eb

File tree

2 files changed

+47
-7
lines changed

2 files changed

+47
-7
lines changed

src/web3py/extensions/ipfs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import random
23
from functools import wraps
34
from typing import Iterable
45
from web3 import Web3
@@ -27,11 +28,16 @@ class IPFS(Module):
2728
def __init__(self, w3: Web3, providers: Iterable[IPFSProvider], *, retries: int = 3) -> None:
2829
super().__init__(w3)
2930
self.retries = retries
30-
self.providers = list(providers)
31+
3132
self.current_provider_index: int = 0
3233
self.last_working_provider_index: int = 0
3334
self.current_frame: FrameNumber | None = None
3435

36+
self.providers = list(providers)
37+
# Randomize provider order to reduce probability that
38+
# all oracles use the same provider simultaneously in one frame
39+
random.shuffle(self.providers)
40+
3541
assert self.providers
3642

3743
for p in self.providers:

tests/web3py/test_ipfs.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,23 @@ def test_init__invalid_provider_type__raises_assertion_error(self, mock_w3):
5959
with pytest.raises(AssertionError):
6060
IPFS(mock_w3, ["not a provider"])
6161

62-
def test_provider_selection__different_frames__rotates_providers(self, mock_w3, mock_provider1, mock_provider2):
62+
def test_providers_order__shuffled_once_at_init__order_remain_the_same_during_operations(
63+
self, mock_w3, mock_provider1, mock_provider2
64+
):
65+
ipfs = IPFS(mock_w3, [mock_provider1, mock_provider2])
66+
initial_providers_order = ipfs.providers[:]
67+
68+
ipfs.fetch(HARDCODED_FETCH_CID, FrameNumber(0))
69+
ipfs.publish(HARDCODED_PUBLISH_CONTENT, FrameNumber(1), "test")
70+
ipfs.fetch(HARDCODED_FETCH_CID, FrameNumber(2))
71+
72+
assert ipfs.providers == initial_providers_order
73+
74+
@patch('random.shuffle')
75+
def test_provider_selection__different_frames__rotates_providers(
76+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
77+
):
78+
mock_shuffle.return_value = None
6379
ipfs = IPFS(mock_w3, [mock_provider1, mock_provider2])
6480

6581
ipfs.fetch(HARDCODED_FETCH_CID, FrameNumber(0))
@@ -74,7 +90,11 @@ def test_provider_selection__different_frames__rotates_providers(self, mock_w3,
7490
ipfs.fetch(HARDCODED_FETCH_CID, FrameNumber(3))
7591
assert ipfs.provider == mock_provider2
7692

77-
def test_provider_selection__same_frame__keeps_same_provider(self, mock_w3, mock_provider1, mock_provider2):
93+
@patch('random.shuffle')
94+
def test_provider_selection__same_frame__keeps_same_provider(
95+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
96+
):
97+
mock_shuffle.return_value = None
7898
ipfs = IPFS(mock_w3, [mock_provider1, mock_provider2])
7999

80100
ipfs.fetch(HARDCODED_FETCH_CID, FrameNumber(1))
@@ -114,7 +134,11 @@ def test_fetch__all_retries_fail__raises_no_more_providers_error(self, mock_w3,
114134
assert isinstance(excinfo.value.__cause__, MaxRetryError)
115135
assert provider.fetch.call_count == 3
116136

117-
def test_fetch__first_provider_fails__falls_back_to_second_provider(self, mock_w3, mock_provider1, mock_provider2):
137+
@patch('random.shuffle')
138+
def test_fetch__first_provider_fails__falls_back_to_second_provider(
139+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
140+
):
141+
mock_shuffle.return_value = None
118142
provider1 = mock_provider1
119143
provider1.fetch = MagicMock(side_effect=Exception("fail"))
120144
provider2 = mock_provider2
@@ -127,7 +151,11 @@ def test_fetch__first_provider_fails__falls_back_to_second_provider(self, mock_w
127151
assert provider1.fetch.call_count == 1
128152
assert provider2.fetch.call_count == 1
129153

130-
def test_fetch__all_providers_fail__raises_no_more_providers_error(self, mock_w3, mock_provider1, mock_provider2):
154+
@patch('random.shuffle')
155+
def test_fetch__all_providers_fail__raises_no_more_providers_error(
156+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
157+
):
158+
mock_shuffle.return_value = None
131159
provider1 = mock_provider1
132160
provider1.fetch = MagicMock(side_effect=Exception("fail1"))
133161
provider2 = mock_provider2
@@ -159,9 +187,11 @@ def test_publish__all_retries_fail__raises_no_more_providers_error(self, mock_w3
159187
assert isinstance(excinfo.value.__cause__, MaxRetryError)
160188
assert provider.publish.call_count == 3
161189

190+
@patch('random.shuffle')
162191
def test_publish__first_provider_fails__falls_back_to_second_provider(
163-
self, mock_w3, mock_provider1, mock_provider2
192+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
164193
):
194+
mock_shuffle.return_value = None
165195
provider1 = mock_provider1
166196
provider1.publish = MagicMock(side_effect=Exception("fail"))
167197
provider2 = mock_provider2
@@ -174,7 +204,11 @@ def test_publish__first_provider_fails__falls_back_to_second_provider(
174204
assert provider1.publish.call_count == 1
175205
assert provider2.publish.call_count == 1
176206

177-
def test_publish__all_providers_fail__raises_no_more_providers_error(self, mock_w3, mock_provider1, mock_provider2):
207+
@patch('random.shuffle')
208+
def test_publish__all_providers_fail__raises_no_more_providers_error(
209+
self, mock_shuffle, mock_w3, mock_provider1, mock_provider2
210+
):
211+
mock_shuffle.return_value = None
178212
provider1 = mock_provider1
179213
provider1.publish = MagicMock(side_effect=Exception("fail1"))
180214
provider2 = mock_provider2

0 commit comments

Comments
 (0)