-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathblock.py
More file actions
230 lines (181 loc) · 7.21 KB
/
block.py
File metadata and controls
230 lines (181 loc) · 7.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from enum import Enum
import time
BLOCK_SIZE = 2 ** 14 # 16KB - Standard BitTorrent block size
class State(Enum):
FREE = 0
PENDING = 1
FULL = 2
def __str__(self):
return self.name
class Block:
def __init__(self, state: State = State.FREE, block_size: int = BLOCK_SIZE,
data: bytes = b'', last_seen: float = None):
"""
Represents a block of data in a torrent piece.
Args:
state: Current state of the block
block_size: Size of the block in bytes
data: The actual block data
last_seen: Timestamp when block was last accessed
"""
self.state: State = state
self.block_size: int = block_size
self.data: bytes = data
self.last_seen: float = last_seen if last_seen is not None else time.time()
# Validate inputs
if block_size <= 0:
raise ValueError(f"Block size must be positive, got {block_size}")
if block_size > 2 * BLOCK_SIZE: # Allow some flexibility but not too much
raise ValueError(f"Block size too large: {block_size}")
if data and len(data) > block_size:
raise ValueError(f"Data size {len(data)} exceeds block size {block_size}")
def __str__(self):
"""Human-readable string representation"""
state_emoji = {
State.FREE: "🟢",
State.PENDING: "🟡",
State.FULL: "🔵"
}
data_info = f"{len(self.data)}/{self.block_size} bytes" if self.data else "empty"
return f"{state_emoji.get(self.state, '⚫')} {self.state.name} - {data_info} - seen {time.time() - self.last_seen:.1f}s ago"
def __repr__(self):
"""Detailed representation for debugging"""
return (f"Block(state={self.state}, size={self.block_size}, "
f"data_length={len(self.data)}, last_seen={self.last_seen})")
def set_data(self, data: bytes) -> bool:
"""
Set block data and mark as FULL if data matches block size.
Args:
data: The data to set for this block
Returns:
bool: True if data was set successfully, False otherwise
"""
if not data:
return False
if len(data) > self.block_size:
# Truncate data that's too large (shouldn't happen in normal operation)
self.data = data[:self.block_size]
else:
self.data = data
# Only mark as FULL if we have complete data
if len(self.data) == self.block_size:
self.state = State.FULL
else:
# For partial data (like last block in piece), still mark as FULL
# since we won't get more data for this block
self.state = State.FULL
self.last_seen = time.time()
return True
def mark_pending(self):
"""Mark this block as pending download"""
self.state = State.PENDING
self.last_seen = time.time()
def mark_free(self):
"""Reset block to free state (for retries)"""
self.state = State.FREE
self.data = b''
self.last_seen = time.time()
def is_stale(self, timeout_seconds: float = 30.0) -> bool:
"""
Check if this block has been pending for too long.
Args:
timeout_seconds: How long to wait before considering stale
Returns:
bool: True if block is stale and should be reset
"""
if self.state != State.PENDING:
return False
return (time.time() - self.last_seen) > timeout_seconds
def is_complete(self) -> bool:
"""Check if block has complete data"""
return self.state == State.FULL and len(self.data) == self.block_size
def get_remaining_size(self) -> int:
"""Get remaining bytes needed to complete this block"""
if self.state == State.FULL:
return 0
return self.block_size - len(self.data)
def validate(self) -> bool:
"""Validate block state and data consistency"""
if self.state == State.FREE:
return len(self.data) == 0
elif self.state == State.PENDING:
return len(self.data) <= self.block_size
elif self.state == State.FULL:
return 0 < len(self.data) <= self.block_size
return False
def to_dict(self) -> dict:
"""Convert block to dictionary for serialization/debugging"""
return {
'state': self.state.name,
'block_size': self.block_size,
'data_length': len(self.data),
'last_seen': self.last_seen,
'is_stale': self.is_stale(),
'is_complete': self.is_complete(),
'remaining_size': self.get_remaining_size()
}
# Utility functions for block management
def create_blocks_for_piece(piece_size: int, piece_index: int) -> list['Block']:
"""
Create all blocks needed for a piece.
Args:
piece_size: Total size of the piece in bytes
piece_index: Index of the piece (for debugging)
Returns:
list[Block]: List of blocks for the piece
"""
if piece_size <= 0:
raise ValueError(f"Invalid piece size: {piece_size}")
num_full_blocks = piece_size // BLOCK_SIZE
last_block_size = piece_size % BLOCK_SIZE
blocks = []
# Create full-sized blocks
for _ in range(num_full_blocks):
blocks.append(Block(block_size=BLOCK_SIZE))
# Create last block if needed
if last_block_size > 0:
blocks.append(Block(block_size=last_block_size))
return blocks
def calculate_block_range(piece_size: int) -> tuple[int, int]:
"""
Calculate the range of blocks needed for a piece.
Args:
piece_size: Size of the piece in bytes
Returns:
tuple: (number_of_full_blocks, last_block_size)
"""
num_full_blocks = piece_size // BLOCK_SIZE
last_block_size = piece_size % BLOCK_SIZE
return num_full_blocks, last_block_size
# Test function for debugging
def test_block_functionality():
"""Test basic block functionality"""
print("🧪 Testing Block functionality...")
# Test basic block
block = Block()
assert block.state == State.FREE
assert block.block_size == BLOCK_SIZE
assert len(block.data) == 0
print("✅ Basic block creation works")
# Test data setting
test_data = b"x" * BLOCK_SIZE
block.set_data(test_data)
assert block.state == State.FULL
assert block.data == test_data
print("✅ Data setting works")
# Test partial data (last block scenario)
small_block = Block(block_size=100)
small_data = b"x" * 50
small_block.set_data(small_data)
assert block.state == State.FULL # Should still be marked FULL
print("✅ Partial data handling works")
# Test stale detection
stale_block = Block(state=State.PENDING)
assert not stale_block.is_stale(timeout_seconds=1.0)
import time
time.sleep(1.1)
assert stale_block.is_stale(timeout_seconds=1.0)
print("✅ Stale detection works")
print("🎉 All block tests passed!")
if __name__ == "__main__":
test_block_functionality()