1#!/usr/bin/env python
2#
3# Public Domain 2014-2018 MongoDB, Inc.
4# Public Domain 2008-2014 WiredTiger, Inc.
5#
6# This is free and unencumbered software released into the public domain.
7#
8# Anyone is free to copy, modify, publish, use, compile, sell, or
9# distribute this software, either in source code form or as a compiled
10# binary, for any purpose, commercial or non-commercial, and by any
11# means.
12#
13# In jurisdictions that recognize copyright laws, the author or authors
14# of this software dedicate any and all copyright interest in the
15# software to the public domain. We make this dedication for the benefit
16# of the public at large and to the detriment of our heirs and
17# successors. We intend this dedication to be an overt act of
18# relinquishment in perpetuity of all present and future rights to this
19# software under copyright law.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27# OTHER DEALINGS IN THE SOFTWARE.
28#
29# test_txn05.py
30# Transactions: commits and rollbacks
31#
32
33import fnmatch, os, shutil, time
34from suite_subprocess import suite_subprocess
35from wtscenario import make_scenarios
36import wttest
37
38class test_txn05(wttest.WiredTigerTestCase, suite_subprocess):
39    logmax = "100K"
40    tablename = 'test_txn05'
41    uri = 'table:' + tablename
42    archive_list = ['true', 'false']
43    sync_list = [
44        '(method=dsync,enabled)',
45        '(method=fsync,enabled)',
46        '(method=none,enabled)',
47        '(enabled=false)'
48    ]
49
50    types = [
51        ('row', dict(tabletype='row',
52                    create_params = 'key_format=i,value_format=i')),
53        ('var', dict(tabletype='var',
54                    create_params = 'key_format=r,value_format=i')),
55        ('fix', dict(tabletype='fix',
56                    create_params = 'key_format=r,value_format=8t')),
57    ]
58    op1s = [
59        ('trunc-all', dict(op1=('all', 0))),
60        ('trunc-both', dict(op1=('both', 2))),
61        ('trunc-start', dict(op1=('start', 2))),
62        ('trunc-stop', dict(op1=('stop', 2))),
63    ]
64    txn1s = [('t1c', dict(txn1='commit')), ('t1r', dict(txn1='rollback'))]
65
66    scenarios = make_scenarios(types, op1s, txn1s)
67
68    def conn_config(self):
69        # Cycle through the different transaction_sync values in a
70        # deterministic manner.
71        txn_sync = self.sync_list[
72            self.scenario_number % len(self.sync_list)]
73        # Set archive false on the home directory.
74        return 'log=(archive=false,enabled,file_max=%s),' % self.logmax + \
75            'transaction_sync="%s",' % txn_sync
76
77    # Check that a cursor (optionally started in a new transaction), sees the
78    # expected values.
79    def check(self, session, txn_config, expected):
80        if txn_config:
81            session.begin_transaction(txn_config)
82        c = session.open_cursor(self.uri, None)
83        actual = dict((k, v) for k, v in c if v != 0)
84        # Search for the expected items as well as iterating
85        for k, v in expected.iteritems():
86            self.assertEqual(c[k], v)
87        c.close()
88        if txn_config:
89            session.commit_transaction()
90        self.assertEqual(actual, expected)
91
92    # Check the state of the system with respect to the current cursor and
93    # different isolation levels.
94    def check_all(self, current, committed):
95        # Transactions see their own changes.
96        # Read-uncommitted transactions see all changes.
97        # Snapshot and read-committed transactions should not see changes.
98        self.check(self.session, None, current)
99        self.check(self.session2, "isolation=snapshot", committed)
100        self.check(self.session2, "isolation=read-committed", committed)
101        self.check(self.session2, "isolation=read-uncommitted", current)
102
103        # Opening a clone of the database home directory should run
104        # recovery and see the committed results.  Flush the log because
105        # the backup may not get all the log records if we are running
106        # without a sync option.  Use sync=off to force a write to the OS.
107        self.session.log_flush('sync=off')
108        self.backup(self.backup_dir)
109        backup_conn_params = 'log=(enabled,file_max=%s)' % self.logmax
110        backup_conn = self.wiredtiger_open(self.backup_dir, backup_conn_params)
111        try:
112            self.check(backup_conn.open_session(), None, committed)
113        finally:
114            backup_conn.close()
115
116    def check_log(self, committed):
117        self.backup(self.backup_dir)
118        #
119        # Open and close the backup connection a few times to force
120        # repeated recovery and log archiving even if later recoveries
121        # are essentially no-ops. Confirm that the backup contains
122        # the committed operations after recovery.
123        #
124        # Cycle through the different archive values in a
125        # deterministic manner.
126        self.archive = self.archive_list[
127            self.scenario_number % len(self.archive_list)]
128        backup_conn_params = \
129            'log=(enabled,file_max=%s,archive=%s)' % (self.logmax, self.archive)
130        orig_logs = fnmatch.filter(os.listdir(self.backup_dir), "*gerLog*")
131        endcount = 2
132        count = 0
133        while count < endcount:
134            backup_conn = self.wiredtiger_open(self.backup_dir,
135                                               backup_conn_params)
136            try:
137                 session = backup_conn.open_session()
138            finally:
139                self.check(session, None, committed)
140                # Sleep long enough so that the archive thread is guaranteed
141                # to run before we close the connection.
142                time.sleep(1.0)
143                if count == 0:
144                    first_logs = \
145                        fnmatch.filter(os.listdir(self.backup_dir), "*gerLog*")
146                backup_conn.close()
147            count += 1
148        #
149        # Check logs after repeated openings. The first log should
150        # have been archived if configured. Subsequent openings would not
151        # archive because no checkpoint is written due to no modifications.
152        #
153        cur_logs = fnmatch.filter(os.listdir(self.backup_dir), "*gerLog*")
154        for o in orig_logs:
155            # Creating the backup was effectively an unclean shutdown so
156            # even after sleeping, we should never archive log files
157            # because a checkpoint has not run.  Later opens and runs of
158            # recovery will detect a clean shutdown and allow archiving.
159            self.assertEqual(True, o in first_logs)
160            if self.archive == 'true':
161                self.assertEqual(False, o in cur_logs)
162            else:
163                self.assertEqual(True, o in cur_logs)
164        #
165        # Run printlog and make sure it exits with zero status.
166        #
167        self.runWt(['-h', self.backup_dir, 'printlog'], outfilename='printlog.out')
168
169    def test_ops(self):
170        self.backup_dir = os.path.join(self.home, "WT_BACKUP")
171        self.session2 = self.conn.open_session()
172        # print "Creating %s with config '%s'" % (self.uri, self.create_params)
173        self.session.create(self.uri, self.create_params)
174        # Set up the table with entries for 1-5.
175        # We then truncate starting or ending in various places.
176        c = self.session.open_cursor(self.uri, None)
177        current = {1:1, 2:1, 3:1, 4:1, 5:1}
178        for k in current:
179            c[k] = 1
180        committed = current.copy()
181
182        ops = (self.op1, )
183        txns = (self.txn1, )
184        for i, ot in enumerate(zip(ops, txns)):
185            self.session.begin_transaction()
186            ok, txn = ot
187            # print '%d: %s(%d)[%s]' % (i, ok[0], ok[1], txn)
188            op, k = ok
189
190            # print '%d: %s(%d)[%s]' % (i, ok[0], ok[1], txn)
191            if op == 'stop':
192                c.set_key(k)
193                self.session.truncate(None, None, c, None)
194                kstart = 1
195                kstop = k
196            elif op == 'start':
197                c.set_key(k)
198                self.session.truncate(None, c, None, None)
199                kstart = k
200                kstop = len(current)
201            elif op == 'both':
202                c2 = self.session.open_cursor(self.uri, None)
203                # For both, the key given is the start key.  Add 2
204                # for the stop key.
205                kstart = k
206                kstop = k + 2
207                c.set_key(kstart)
208                c2.set_key(kstop)
209                self.session.truncate(None, c, c2, None)
210                c2.close()
211            elif op == 'all':
212                c2 = self.session.open_cursor(self.uri, None)
213                kstart = 1
214                kstop = len(current)
215                c.set_key(kstart)
216                c2.set_key(kstop)
217                self.session.truncate(None, c, c2, None)
218                c2.close()
219
220            while (kstart <= kstop):
221                del current[kstart]
222                kstart += 1
223
224            # print current
225            # Check the state after each operation.
226            self.check_all(current, committed)
227
228            if txn == 'commit':
229                committed = current.copy()
230                self.session.commit_transaction()
231            elif txn == 'rollback':
232                current = committed.copy()
233                self.session.rollback_transaction()
234
235            # Check the state after each commit/rollback.
236            self.check_all(current, committed)
237
238        # Check the log state after the entire op completes
239        # and run recovery.
240        if self.scenario_number % (len(test_txn05.scenarios) / 100 + 1) == 0:
241            self.check_log(committed)
242
243if __name__ == '__main__':
244    wttest.run()
245