mirror of https://github.com/apache/cloudstack.git
398 lines
14 KiB
Python
398 lines
14 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Multi-operation sequences, parallel reads across multiple transfer objects,
|
|
cross-backend scenarios, and edge cases.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import unittest
|
|
import urllib.error
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
from .test_base import (
|
|
IMAGE_SIZE,
|
|
ImageServerTestCase,
|
|
http_get,
|
|
http_patch,
|
|
http_post,
|
|
http_put,
|
|
make_file_transfer,
|
|
make_nbd_transfer,
|
|
randbytes,
|
|
shutdown_image_server,
|
|
test_timeout,
|
|
)
|
|
|
|
log = logging.getLogger(__name__)
|
|
FUTURES_TIMEOUT = 60 # seconds for as_completed to collect all results
|
|
|
|
|
|
def _fetch(url, headers=None):
|
|
"""GET *url* and return the body bytes, properly closing the response."""
|
|
resp = http_get(url, headers=headers)
|
|
try:
|
|
return resp.read()
|
|
finally:
|
|
resp.close()
|
|
|
|
|
|
class TestParallelReadsFileBackend(ImageServerTestCase):
|
|
"""Multiple concurrent GET requests to multiple file-backed transfers."""
|
|
|
|
@test_timeout(120)
|
|
def test_parallel_reads_single_file_transfer(self):
|
|
data = randbytes(500, IMAGE_SIZE)
|
|
tid, url, path, cleanup = make_file_transfer(data=data)
|
|
try:
|
|
results = {}
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {}
|
|
for i in range(8):
|
|
start = i * (IMAGE_SIZE // 8)
|
|
end = start + (IMAGE_SIZE // 8) - 1
|
|
f = pool.submit(
|
|
_fetch, url, headers={"Range": f"bytes={start}-{end}"}
|
|
)
|
|
futures[f] = (start, end)
|
|
|
|
for f in as_completed(futures, timeout=FUTURES_TIMEOUT):
|
|
start, end = futures[f]
|
|
results[(start, end)] = f.result()
|
|
|
|
for (start, end), chunk in sorted(results.items()):
|
|
self.assertEqual(chunk, data[start:end + 1], f"Mismatch at {start}-{end}")
|
|
finally:
|
|
cleanup()
|
|
|
|
@test_timeout(120)
|
|
def test_parallel_reads_multiple_file_transfers(self):
|
|
"""Parallel reads across 4 different file-backed transfer objects."""
|
|
transfers = []
|
|
try:
|
|
for i in range(4):
|
|
data = randbytes(600 + i, IMAGE_SIZE)
|
|
tid, url, path, cleanup = make_file_transfer(data=data)
|
|
transfers.append((tid, url, data, cleanup))
|
|
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {}
|
|
for idx, (tid, url, data, _) in enumerate(transfers):
|
|
for j in range(2):
|
|
f = pool.submit(_fetch, url)
|
|
futures[f] = (idx, data)
|
|
|
|
for f in as_completed(futures, timeout=FUTURES_TIMEOUT):
|
|
idx, expected_data = futures[f]
|
|
got = f.result()
|
|
self.assertEqual(got, expected_data, f"Transfer {idx} mismatch")
|
|
finally:
|
|
for _, _, _, cleanup in transfers:
|
|
cleanup()
|
|
|
|
|
|
class TestParallelReadsNbdBackend(ImageServerTestCase):
|
|
"""Multiple concurrent GET requests to multiple NBD-backed transfers."""
|
|
|
|
@test_timeout(120)
|
|
def test_parallel_reads_single_nbd_transfer(self):
|
|
data = randbytes(700, IMAGE_SIZE)
|
|
tid, url, nbd_server, cleanup = make_nbd_transfer()
|
|
try:
|
|
log.info("Writing %d bytes to NBD transfer %s", IMAGE_SIZE, tid)
|
|
http_put(url, data)
|
|
log.info("NBD write done, starting 8 parallel range reads")
|
|
|
|
results = {}
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {}
|
|
for i in range(8):
|
|
start = i * (IMAGE_SIZE // 8)
|
|
end = start + (IMAGE_SIZE // 8) - 1
|
|
f = pool.submit(
|
|
_fetch, url, headers={"Range": f"bytes={start}-{end}"}
|
|
)
|
|
futures[f] = (start, end)
|
|
|
|
completed = 0
|
|
for f in as_completed(futures, timeout=FUTURES_TIMEOUT):
|
|
start, end = futures[f]
|
|
results[(start, end)] = f.result()
|
|
completed += 1
|
|
log.info("NBD range read %d/8 done: bytes=%d-%d", completed, start, end)
|
|
|
|
for (start, end), chunk in sorted(results.items()):
|
|
self.assertEqual(chunk, data[start:end + 1], f"Mismatch at {start}-{end}")
|
|
finally:
|
|
cleanup()
|
|
|
|
@test_timeout(120)
|
|
def test_parallel_reads_multiple_nbd_transfers(self):
|
|
"""Parallel reads across 4 different NBD-backed transfer objects."""
|
|
transfers = []
|
|
try:
|
|
for i in range(4):
|
|
data = randbytes(800 + i, IMAGE_SIZE)
|
|
log.info("Setting up NBD transfer %d", i)
|
|
tid, url, nbd_server, cleanup = make_nbd_transfer()
|
|
log.info("Writing data to NBD transfer %d (tid=%s)", i, tid)
|
|
http_put(url, data)
|
|
transfers.append((tid, url, data, cleanup))
|
|
log.info("NBD transfer %d ready", i)
|
|
|
|
log.info("Starting parallel reads across %d NBD transfers", len(transfers))
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {}
|
|
for idx, (tid, url, data, _) in enumerate(transfers):
|
|
for j in range(2):
|
|
f = pool.submit(_fetch, url)
|
|
futures[f] = (idx, data)
|
|
|
|
completed = 0
|
|
for f in as_completed(futures, timeout=FUTURES_TIMEOUT):
|
|
idx, expected_data = futures[f]
|
|
got = f.result()
|
|
completed += 1
|
|
log.info("Read %d/%d done: NBD transfer idx=%d, %d bytes",
|
|
completed, len(futures), idx, len(got))
|
|
self.assertEqual(got, expected_data, f"NBD transfer {idx} mismatch")
|
|
finally:
|
|
for _, _, _, cleanup in transfers:
|
|
cleanup()
|
|
|
|
|
|
class TestParallelReadsMixedBackends(ImageServerTestCase):
|
|
"""Parallel reads across a mix of file and NBD transfers simultaneously."""
|
|
|
|
@test_timeout(120)
|
|
def test_parallel_reads_file_and_nbd_mixed(self):
|
|
transfers = []
|
|
try:
|
|
for i in range(2):
|
|
log.info("Setting up file transfer %d", i)
|
|
data = randbytes(900 + i, IMAGE_SIZE)
|
|
tid, url, path, cleanup = make_file_transfer(data=data)
|
|
transfers.append(("file", tid, url, data, cleanup))
|
|
log.info("File transfer %d ready: tid=%s", i, tid)
|
|
|
|
for i in range(2):
|
|
log.info("Setting up NBD transfer %d", i)
|
|
data = randbytes(950 + i, IMAGE_SIZE)
|
|
tid, url, nbd_server, cleanup = make_nbd_transfer()
|
|
log.info("NBD transfer %d registered: tid=%s, writing data...", i, tid)
|
|
http_put(url, data)
|
|
transfers.append(("nbd", tid, url, data, cleanup))
|
|
log.info("NBD transfer %d ready", i)
|
|
|
|
log.info("Starting parallel reads across %d transfers (2 file + 2 nbd)",
|
|
len(transfers))
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures = {}
|
|
for idx, (backend_type, tid, url, data, _) in enumerate(transfers):
|
|
for j in range(2):
|
|
f = pool.submit(_fetch, url)
|
|
futures[f] = (idx, backend_type, data)
|
|
|
|
completed = 0
|
|
for f in as_completed(futures, timeout=FUTURES_TIMEOUT):
|
|
idx, backend_type, expected = futures[f]
|
|
got = f.result()
|
|
completed += 1
|
|
log.info("Read %d/%d done: %s transfer idx=%d, %d bytes",
|
|
completed, len(futures), backend_type, idx, len(got))
|
|
self.assertEqual(got, expected, f"{backend_type} transfer {idx} mismatch")
|
|
|
|
log.info("All parallel mixed reads completed successfully")
|
|
except TimeoutError:
|
|
log.error("TIMEOUT in mixed parallel reads — dumping server logs")
|
|
self.dump_server_logs()
|
|
raise
|
|
finally:
|
|
for _, _, _, _, cleanup in transfers:
|
|
cleanup()
|
|
|
|
|
|
class TestWriteThenReadNbd(ImageServerTestCase):
|
|
"""Multi-step write sequences on NBD backend."""
|
|
|
|
def setUp(self):
|
|
self._tid, self._url, self._nbd, self._cleanup = make_nbd_transfer()
|
|
|
|
def tearDown(self):
|
|
self._cleanup()
|
|
|
|
def test_partial_writes_then_full_read(self):
|
|
http_put(self._url, b"\x00" * IMAGE_SIZE)
|
|
|
|
chunk_size = IMAGE_SIZE // 4
|
|
for i in range(4):
|
|
offset = i * chunk_size
|
|
end = offset + chunk_size - 1
|
|
data = bytes([i & 0xFF]) * chunk_size
|
|
http_patch(self._url, data, headers={
|
|
"Range": f"bytes={offset}-{end}",
|
|
"Content-Type": "application/octet-stream",
|
|
"Content-Length": str(chunk_size),
|
|
})
|
|
|
|
resp = http_get(self._url)
|
|
full = resp.read()
|
|
for i in range(4):
|
|
offset = i * chunk_size
|
|
self.assertEqual(full[offset:offset + chunk_size], bytes([i & 0xFF]) * chunk_size)
|
|
|
|
def test_zero_then_extents(self):
|
|
http_put(self._url, randbytes(1000, IMAGE_SIZE))
|
|
|
|
payload = json.dumps({"op": "zero", "size": IMAGE_SIZE // 2, "offset": 0}).encode()
|
|
http_patch(self._url, payload, headers={
|
|
"Content-Type": "application/json",
|
|
"Content-Length": str(len(payload)),
|
|
})
|
|
|
|
resp = http_get(f"{self._url}/extents")
|
|
extents = json.loads(resp.read())
|
|
total = sum(e["length"] for e in extents)
|
|
self.assertEqual(total, IMAGE_SIZE)
|
|
|
|
def test_write_flush_read(self):
|
|
data = randbytes(1001, IMAGE_SIZE)
|
|
resp = http_put(f"{self._url}?flush=y", data)
|
|
body = json.loads(resp.read())
|
|
self.assertTrue(body["flushed"])
|
|
|
|
resp2 = http_get(self._url)
|
|
self.assertEqual(resp2.read(), data)
|
|
|
|
|
|
class TestWriteThenReadFile(ImageServerTestCase):
|
|
def setUp(self):
|
|
self._tid, self._url, self._path, self._cleanup = make_file_transfer()
|
|
|
|
def tearDown(self):
|
|
self._cleanup()
|
|
|
|
def test_put_then_get_roundtrip(self):
|
|
data = randbytes(1100, IMAGE_SIZE)
|
|
http_put(self._url, data)
|
|
resp = http_get(self._url)
|
|
self.assertEqual(resp.read(), data)
|
|
|
|
|
|
class TestRegisterUseUnregisterUse(ImageServerTestCase):
|
|
def test_unregistered_transfer_returns_404(self):
|
|
data = randbytes(1200, IMAGE_SIZE)
|
|
tid, url, path, cleanup = make_file_transfer(data=data)
|
|
|
|
resp = http_get(url)
|
|
self.assertEqual(resp.read(), data)
|
|
|
|
cleanup()
|
|
|
|
with self.assertRaises(urllib.error.HTTPError) as ctx:
|
|
http_get(url)
|
|
self.assertEqual(ctx.exception.code, 404)
|
|
|
|
|
|
class TestMultipleTransfersSimultaneous(ImageServerTestCase):
|
|
@test_timeout(120)
|
|
def test_operate_on_file_and_nbd_concurrently(self):
|
|
file_data = randbytes(1300, IMAGE_SIZE)
|
|
nbd_data = randbytes(1301, IMAGE_SIZE)
|
|
|
|
ftid, furl, fpath, fcleanup = make_file_transfer(data=file_data)
|
|
ntid, nurl, nbd_server, ncleanup = make_nbd_transfer()
|
|
|
|
try:
|
|
log.info("Writing data to NBD transfer %s", ntid)
|
|
http_put(nurl, nbd_data)
|
|
|
|
log.info("Starting concurrent file + NBD reads")
|
|
with ThreadPoolExecutor(max_workers=4) as pool:
|
|
f_file = pool.submit(_fetch, furl)
|
|
f_nbd = pool.submit(_fetch, nurl)
|
|
|
|
self.assertEqual(f_file.result(timeout=FUTURES_TIMEOUT), file_data)
|
|
self.assertEqual(f_nbd.result(timeout=FUTURES_TIMEOUT), nbd_data)
|
|
log.info("Concurrent reads completed successfully")
|
|
finally:
|
|
fcleanup()
|
|
ncleanup()
|
|
|
|
|
|
class TestLargeChunkedTransfer(ImageServerTestCase):
|
|
def test_put_larger_than_chunk_size_file(self):
|
|
"""Upload data that spans multiple CHUNK_SIZE boundaries."""
|
|
tid, url, path, cleanup = make_file_transfer()
|
|
try:
|
|
data = randbytes(1400, IMAGE_SIZE)
|
|
http_put(url, data)
|
|
resp = http_get(url)
|
|
self.assertEqual(resp.read(), data)
|
|
finally:
|
|
cleanup()
|
|
|
|
def test_nbd_put_larger_than_chunk_size(self):
|
|
tid, url, nbd_server, cleanup = make_nbd_transfer()
|
|
try:
|
|
data = randbytes(1401, IMAGE_SIZE)
|
|
http_put(url, data)
|
|
resp = http_get(url)
|
|
self.assertEqual(resp.read(), data)
|
|
finally:
|
|
cleanup()
|
|
|
|
|
|
class TestEdgeCases(ImageServerTestCase):
|
|
def test_get_not_found_path(self):
|
|
with self.assertRaises(urllib.error.HTTPError) as ctx:
|
|
http_get(f"{self.base_url}/not/images/path")
|
|
self.assertEqual(ctx.exception.code, 404)
|
|
|
|
def test_post_unknown_tail(self):
|
|
tid, url, path, cleanup = make_file_transfer()
|
|
try:
|
|
with self.assertRaises(urllib.error.HTTPError) as ctx:
|
|
http_post(f"{url}/unknown")
|
|
self.assertEqual(ctx.exception.code, 404)
|
|
finally:
|
|
cleanup()
|
|
|
|
def test_get_extents_then_flush_nbd(self):
|
|
tid, url, nbd_server, cleanup = make_nbd_transfer()
|
|
try:
|
|
http_put(url, randbytes(1500, IMAGE_SIZE))
|
|
|
|
resp = http_get(f"{url}/extents")
|
|
self.assertEqual(resp.status, 200)
|
|
resp.read()
|
|
|
|
resp2 = http_post(f"{url}/flush")
|
|
body = json.loads(resp2.read())
|
|
self.assertTrue(body["ok"])
|
|
finally:
|
|
cleanup()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
unittest.main()
|
|
finally:
|
|
shutdown_image_server()
|