1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.store; |
7 | |
8 | import java.io.IOException; |
9 | import java.io.OutputStreamWriter; |
10 | import java.io.PrintWriter; |
11 | import java.sql.SQLException; |
12 | import java.util.HashSet; |
13 | import java.util.Properties; |
14 | |
15 | import org.h2.api.ErrorCode; |
16 | import org.h2.engine.ConnectionInfo; |
17 | import org.h2.engine.Constants; |
18 | import org.h2.engine.Database; |
19 | import org.h2.engine.Session; |
20 | import org.h2.message.DbException; |
21 | import org.h2.store.fs.FilePathRec; |
22 | import org.h2.store.fs.FileUtils; |
23 | import org.h2.store.fs.Recorder; |
24 | import org.h2.tools.Recover; |
25 | import org.h2.util.IOUtils; |
26 | import org.h2.util.New; |
27 | import org.h2.util.StringUtils; |
28 | import org.h2.util.Utils; |
29 | |
30 | /** |
31 | * A tool that simulates a crash while writing to the database, and then |
32 | * verifies the database doesn't get corrupt. |
33 | */ |
34 | public class RecoverTester implements Recorder { |
35 | |
36 | private static RecoverTester instance; |
37 | |
38 | private String testDatabase = "memFS:reopen"; |
39 | private int writeCount = Utils.getProperty("h2.recoverTestOffset", 0); |
40 | private int testEvery = Utils.getProperty("h2.recoverTest", 64); |
41 | private final long maxFileSize = Utils.getProperty( |
42 | "h2.recoverTestMaxFileSize", Integer.MAX_VALUE) * 1024L * 1024; |
43 | private int verifyCount; |
44 | private final HashSet<String> knownErrors = New.hashSet(); |
45 | private volatile boolean testing; |
46 | |
47 | /** |
48 | * Initialize the recover test. |
49 | * |
50 | * @param recoverTest the value of the recover test parameter |
51 | */ |
52 | public static synchronized void init(String recoverTest) { |
53 | RecoverTester tester = RecoverTester.getInstance(); |
54 | if (StringUtils.isNumber(recoverTest)) { |
55 | tester.setTestEvery(Integer.parseInt(recoverTest)); |
56 | } |
57 | FilePathRec.setRecorder(tester); |
58 | } |
59 | |
60 | public static synchronized RecoverTester getInstance() { |
61 | if (instance == null) { |
62 | instance = new RecoverTester(); |
63 | } |
64 | return instance; |
65 | } |
66 | |
67 | @Override |
68 | public void log(int op, String fileName, byte[] data, long x) { |
69 | if (op != Recorder.WRITE && op != Recorder.TRUNCATE) { |
70 | return; |
71 | } |
72 | if (!fileName.endsWith(Constants.SUFFIX_PAGE_FILE) && |
73 | !fileName.endsWith(Constants.SUFFIX_MV_FILE)) { |
74 | return; |
75 | } |
76 | writeCount++; |
77 | if ((writeCount % testEvery) != 0) { |
78 | return; |
79 | } |
80 | if (FileUtils.size(fileName) > maxFileSize) { |
81 | // System.out.println(fileName + " " + IOUtils.length(fileName)); |
82 | return; |
83 | } |
84 | if (testing) { |
85 | // avoid deadlocks |
86 | return; |
87 | } |
88 | testing = true; |
89 | PrintWriter out = null; |
90 | try { |
91 | out = new PrintWriter( |
92 | new OutputStreamWriter( |
93 | FileUtils.newOutputStream(fileName + ".log", true))); |
94 | testDatabase(fileName, out); |
95 | } catch (IOException e) { |
96 | throw DbException.convertIOException(e, null); |
97 | } finally { |
98 | IOUtils.closeSilently(out); |
99 | testing = false; |
100 | } |
101 | } |
102 | |
103 | private synchronized void testDatabase(String fileName, PrintWriter out) { |
104 | out.println("+ write #" + writeCount + " verify #" + verifyCount); |
105 | try { |
106 | IOUtils.copyFiles(fileName, testDatabase + Constants.SUFFIX_PAGE_FILE); |
107 | String mvFileName = fileName.substring(0, fileName.length() - |
108 | Constants.SUFFIX_PAGE_FILE.length()) + |
109 | Constants.SUFFIX_MV_FILE; |
110 | if (FileUtils.exists(mvFileName)) { |
111 | IOUtils.copyFiles(mvFileName, testDatabase + Constants.SUFFIX_MV_FILE); |
112 | } |
113 | verifyCount++; |
114 | // avoid using the Engine class to avoid deadlocks |
115 | Properties p = new Properties(); |
116 | p.setProperty("user", ""); |
117 | p.setProperty("password", ""); |
118 | ConnectionInfo ci = new ConnectionInfo("jdbc:h2:" + testDatabase + |
119 | ";FILE_LOCK=NO;TRACE_LEVEL_FILE=0", p); |
120 | Database database = new Database(ci, null); |
121 | // close the database |
122 | Session sysSession = database.getSystemSession(); |
123 | sysSession.prepare("script to '" + testDatabase + ".sql'").query(0); |
124 | sysSession.prepare("shutdown immediately").update(); |
125 | database.removeSession(null); |
126 | // everything OK - return |
127 | return; |
128 | } catch (DbException e) { |
129 | SQLException e2 = DbException.toSQLException(e); |
130 | int errorCode = e2.getErrorCode(); |
131 | if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) { |
132 | return; |
133 | } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) { |
134 | return; |
135 | } |
136 | e.printStackTrace(System.out); |
137 | } catch (Exception e) { |
138 | // failed |
139 | int errorCode = 0; |
140 | if (e instanceof SQLException) { |
141 | errorCode = ((SQLException) e).getErrorCode(); |
142 | } |
143 | if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) { |
144 | return; |
145 | } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) { |
146 | return; |
147 | } |
148 | e.printStackTrace(System.out); |
149 | } |
150 | out.println("begin ------------------------------ " + writeCount); |
151 | try { |
152 | Recover.execute(fileName.substring(0, fileName.lastIndexOf('/')), null); |
153 | } catch (SQLException e) { |
154 | // ignore |
155 | } |
156 | testDatabase += "X"; |
157 | try { |
158 | IOUtils.copyFiles(fileName, testDatabase + Constants.SUFFIX_PAGE_FILE); |
159 | // avoid using the Engine class to avoid deadlocks |
160 | Properties p = new Properties(); |
161 | ConnectionInfo ci = new ConnectionInfo("jdbc:h2:" + |
162 | testDatabase + ";FILE_LOCK=NO", p); |
163 | Database database = new Database(ci, null); |
164 | // close the database |
165 | database.removeSession(null); |
166 | } catch (Exception e) { |
167 | int errorCode = 0; |
168 | if (e instanceof DbException) { |
169 | e = ((DbException) e).getSQLException(); |
170 | errorCode = ((SQLException) e).getErrorCode(); |
171 | } |
172 | if (errorCode == ErrorCode.WRONG_USER_OR_PASSWORD) { |
173 | return; |
174 | } else if (errorCode == ErrorCode.FILE_ENCRYPTION_ERROR_1) { |
175 | return; |
176 | } |
177 | StringBuilder buff = new StringBuilder(); |
178 | StackTraceElement[] list = e.getStackTrace(); |
179 | for (int i = 0; i < 10 && i < list.length; i++) { |
180 | buff.append(list[i].toString()).append('\n'); |
181 | } |
182 | String s = buff.toString(); |
183 | if (!knownErrors.contains(s)) { |
184 | out.println(writeCount + " code: " + errorCode + " " + e.toString()); |
185 | e.printStackTrace(System.out); |
186 | knownErrors.add(s); |
187 | } else { |
188 | out.println(writeCount + " code: " + errorCode); |
189 | } |
190 | } |
191 | } |
192 | |
193 | public void setTestEvery(int testEvery) { |
194 | this.testEvery = testEvery; |
195 | } |
196 | |
197 | } |