|
15 | 15 | """Utilities for testing pymongo
|
16 | 16 | """
|
17 | 17 |
|
| 18 | +import gc |
18 | 19 | import os
|
19 | 20 | import struct
|
20 | 21 | import sys
|
21 | 22 | import threading
|
| 23 | +import time |
22 | 24 |
|
23 | 25 | from nose.plugins.skip import SkipTest
|
24 | 26 | from pymongo import MongoClient, MongoReplicaSetClient
|
25 |
| -from pymongo.errors import AutoReconnect |
| 27 | +from pymongo.errors import AutoReconnect, ConnectionFailure, OperationFailure |
26 | 28 | from pymongo.pool import NO_REQUEST, NO_SOCKET_YET, SocketInfo
|
27 | 29 | from test import host, port, version
|
28 | 30 |
|
@@ -586,6 +588,130 @@ def test_max_bson_size(self):
|
586 | 588 | c.max_message_size)
|
587 | 589 |
|
588 | 590 |
|
| 591 | +def collect_until(fn): |
| 592 | + start = time.time() |
| 593 | + while not fn(): |
| 594 | + if (time.time() - start) > 5: |
| 595 | + raise AssertionError("timed out") |
| 596 | + |
| 597 | + gc.collect() |
| 598 | + |
| 599 | + |
| 600 | +class _TestExhaustCursorMixin(object): |
| 601 | + """Test that clients properly handle errors from exhaust cursors. |
| 602 | +
|
| 603 | + Inherit from this class and from unittest.TestCase, and override |
| 604 | + _get_client(self, **kwargs). |
| 605 | + """ |
| 606 | + def test_exhaust_query_server_error(self): |
| 607 | + # When doing an exhaust query, the socket stays checked out on success |
| 608 | + # but must be checked in on error to avoid semaphore leaks. |
| 609 | + client = self._get_client(max_pool_size=1) |
| 610 | + if is_mongos(client): |
| 611 | + raise SkipTest("Can't use exhaust cursors with mongos") |
| 612 | + |
| 613 | + collection = client.pymongo_test.test |
| 614 | + pool = get_pool(client) |
| 615 | + |
| 616 | + sock_info = one(pool.sockets) |
| 617 | + cursor = collection.find({'$bad_query_operator': 1}, exhaust=True) |
| 618 | + self.assertRaises(OperationFailure, cursor.next) |
| 619 | + del cursor |
| 620 | + collect_until(lambda: sock_info in pool.sockets) |
| 621 | + self.assertFalse(sock_info.closed) |
| 622 | + |
| 623 | + # The semaphore was decremented despite the error. |
| 624 | + self.assertTrue(pool._socket_semaphore.acquire(blocking=False)) |
| 625 | + |
| 626 | + def test_exhaust_getmore_server_error(self): |
| 627 | + # When doing a getmore on an exhaust cursor, the socket stays checked |
| 628 | + # out on success but must be checked in on error to avoid semaphore |
| 629 | + # leaks. |
| 630 | + client = self._get_client(max_pool_size=1) |
| 631 | + if is_mongos(client): |
| 632 | + raise SkipTest("Can't use exhaust cursors with mongos") |
| 633 | + |
| 634 | + # A separate client that doesn't affect the test client's pool. |
| 635 | + client2 = self._get_client() |
| 636 | + |
| 637 | + collection = client.pymongo_test.test |
| 638 | + collection.remove() |
| 639 | + |
| 640 | + # Enough data to ensure it streams down for a few milliseconds. |
| 641 | + long_str = 'a' * (256 * 1024) |
| 642 | + collection.insert([{'a': long_str} for _ in range(1000)]) |
| 643 | + |
| 644 | + pool = get_pool(client) |
| 645 | + pool._check_interval_seconds = None # Never check. |
| 646 | + sock_info = one(pool.sockets) |
| 647 | + |
| 648 | + cursor = collection.find(exhaust=True) |
| 649 | + |
| 650 | + # Initial query succeeds. |
| 651 | + cursor.next() |
| 652 | + |
| 653 | + # Cause a server error on getmore. |
| 654 | + client2.pymongo_test.test.drop() |
| 655 | + self.assertRaises(OperationFailure, list, cursor) |
| 656 | + del cursor |
| 657 | + collect_until(lambda: sock_info.closed) |
| 658 | + self.assertFalse(sock_info in pool.sockets) |
| 659 | + |
| 660 | + # The semaphore was decremented despite the error. |
| 661 | + self.assertTrue(pool._socket_semaphore.acquire(blocking=False)) |
| 662 | + |
| 663 | + def test_exhaust_query_network_error(self): |
| 664 | + # When doing an exhaust query, the socket stays checked out on success |
| 665 | + # but must be checked in on error to avoid semaphore leaks. |
| 666 | + client = self._get_client(max_pool_size=1) |
| 667 | + if is_mongos(client): |
| 668 | + raise SkipTest("Can't use exhaust cursors with mongos") |
| 669 | + |
| 670 | + collection = client.pymongo_test.test |
| 671 | + pool = get_pool(client) |
| 672 | + pool._check_interval_seconds = None # Never check. |
| 673 | + |
| 674 | + # Cause a network error. |
| 675 | + sock_info = one(pool.sockets) |
| 676 | + sock_info.sock.close() |
| 677 | + cursor = collection.find(exhaust=True) |
| 678 | + self.assertRaises(ConnectionFailure, cursor.next) |
| 679 | + self.assertTrue(sock_info.closed) |
| 680 | + |
| 681 | + # The semaphore was decremented despite the error. |
| 682 | + self.assertTrue(pool._socket_semaphore.acquire(blocking=False)) |
| 683 | + |
| 684 | + def test_exhaust_getmore_network_error(self): |
| 685 | + # When doing a getmore on an exhaust cursor, the socket stays checked |
| 686 | + # out on success but must be checked in on error to avoid semaphore |
| 687 | + # leaks. |
| 688 | + client = self._get_client(max_pool_size=1) |
| 689 | + if is_mongos(client): |
| 690 | + raise SkipTest("Can't use exhaust cursors with mongos") |
| 691 | + |
| 692 | + collection = client.pymongo_test.test |
| 693 | + collection.remove() |
| 694 | + collection.insert([{} for _ in range(200)]) # More than one batch. |
| 695 | + pool = get_pool(client) |
| 696 | + pool._check_interval_seconds = None # Never check. |
| 697 | + |
| 698 | + cursor = collection.find(exhaust=True) |
| 699 | + |
| 700 | + # Initial query succeeds. |
| 701 | + cursor.next() |
| 702 | + |
| 703 | + # Cause a network error. |
| 704 | + sock_info = cursor._Cursor__exhaust_mgr.sock |
| 705 | + sock_info.sock.close() |
| 706 | + |
| 707 | + # A getmore fails. |
| 708 | + self.assertRaises(ConnectionFailure, list, cursor) |
| 709 | + self.assertTrue(sock_info.closed) |
| 710 | + |
| 711 | + # The semaphore was decremented despite the error. |
| 712 | + self.assertTrue(pool._socket_semaphore.acquire(blocking=False)) |
| 713 | + |
| 714 | + |
589 | 715 | # Backport of WarningMessage from python 2.6, with fixed syntax for python 2.4.
|
590 | 716 | class WarningMessage(object):
|
591 | 717 |
|
|
0 commit comments