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.engine; |
7 | |
8 | import java.util.ArrayList; |
9 | import java.util.HashMap; |
10 | import org.h2.message.DbException; |
11 | import org.h2.store.Data; |
12 | import org.h2.store.FileStore; |
13 | import org.h2.table.Table; |
14 | import org.h2.util.New; |
15 | |
16 | /** |
17 | * Each session keeps a undo log if rollback is required. |
18 | */ |
19 | public class UndoLog { |
20 | |
21 | private final Database database; |
22 | private final ArrayList<Long> storedEntriesPos = New.arrayList(); |
23 | private final ArrayList<UndoLogRecord> records = New.arrayList(); |
24 | private FileStore file; |
25 | private Data rowBuff; |
26 | private int memoryUndo; |
27 | private int storedEntries; |
28 | private HashMap<Integer, Table> tables; |
29 | private final boolean largeTransactions; |
30 | |
31 | /** |
32 | * Create a new undo log for the given session. |
33 | * |
34 | * @param session the session |
35 | */ |
36 | UndoLog(Session session) { |
37 | this.database = session.getDatabase(); |
38 | largeTransactions = database.getSettings().largeTransactions; |
39 | } |
40 | |
41 | /** |
42 | * Get the number of active rows in this undo log. |
43 | * |
44 | * @return the number of rows |
45 | */ |
46 | int size() { |
47 | if (largeTransactions) { |
48 | return storedEntries + records.size(); |
49 | } |
50 | if (SysProperties.CHECK && memoryUndo > records.size()) { |
51 | DbException.throwInternalError(); |
52 | } |
53 | return records.size(); |
54 | } |
55 | |
56 | /** |
57 | * Clear the undo log. This method is called after the transaction is |
58 | * committed. |
59 | */ |
60 | void clear() { |
61 | records.clear(); |
62 | storedEntries = 0; |
63 | storedEntriesPos.clear(); |
64 | memoryUndo = 0; |
65 | if (file != null) { |
66 | file.closeAndDeleteSilently(); |
67 | file = null; |
68 | rowBuff = null; |
69 | } |
70 | } |
71 | |
72 | /** |
73 | * Get the last record and remove it from the list of operations. |
74 | * |
75 | * @return the last record |
76 | */ |
77 | public UndoLogRecord getLast() { |
78 | int i = records.size() - 1; |
79 | if (largeTransactions) { |
80 | if (i < 0 && storedEntries > 0) { |
81 | int last = storedEntriesPos.size() - 1; |
82 | long pos = storedEntriesPos.get(last); |
83 | storedEntriesPos.remove(last); |
84 | long end = file.length(); |
85 | int bufferLength = (int) (end - pos); |
86 | Data buff = Data.create(database, bufferLength); |
87 | file.seek(pos); |
88 | file.readFully(buff.getBytes(), 0, bufferLength); |
89 | while (buff.length() < bufferLength) { |
90 | UndoLogRecord e = UndoLogRecord.loadFromBuffer(buff, this); |
91 | records.add(e); |
92 | memoryUndo++; |
93 | } |
94 | storedEntries -= records.size(); |
95 | file.setLength(pos); |
96 | file.seek(pos); |
97 | } |
98 | i = records.size() - 1; |
99 | } |
100 | UndoLogRecord entry = records.get(i); |
101 | if (entry.isStored()) { |
102 | int start = Math.max(0, i - database.getMaxMemoryUndo() / 2); |
103 | UndoLogRecord first = null; |
104 | for (int j = start; j <= i; j++) { |
105 | UndoLogRecord e = records.get(j); |
106 | if (e.isStored()) { |
107 | e.load(rowBuff, file, this); |
108 | memoryUndo++; |
109 | if (first == null) { |
110 | first = e; |
111 | } |
112 | } |
113 | } |
114 | for (int k = 0; k < i; k++) { |
115 | UndoLogRecord e = records.get(k); |
116 | e.invalidatePos(); |
117 | } |
118 | seek(first.getFilePos()); |
119 | } |
120 | return entry; |
121 | } |
122 | |
123 | /** |
124 | * Go to the right position in the file. |
125 | * |
126 | * @param filePos the position in the file |
127 | */ |
128 | void seek(long filePos) { |
129 | file.seek(filePos * Constants.FILE_BLOCK_SIZE); |
130 | } |
131 | |
132 | /** |
133 | * Remove the last record from the list of operations. |
134 | * |
135 | * @param trimToSize if the undo array should shrink to conserve memory |
136 | */ |
137 | void removeLast(boolean trimToSize) { |
138 | int i = records.size() - 1; |
139 | UndoLogRecord r = records.remove(i); |
140 | if (!r.isStored()) { |
141 | memoryUndo--; |
142 | } |
143 | if (trimToSize && i > 1024 && (i & 1023) == 0) { |
144 | records.trimToSize(); |
145 | } |
146 | } |
147 | |
148 | /** |
149 | * Append an undo log entry to the log. |
150 | * |
151 | * @param entry the entry |
152 | */ |
153 | void add(UndoLogRecord entry) { |
154 | records.add(entry); |
155 | if (largeTransactions) { |
156 | memoryUndo++; |
157 | if (memoryUndo > database.getMaxMemoryUndo() && |
158 | database.isPersistent() && |
159 | !database.isMultiVersion()) { |
160 | if (file == null) { |
161 | String fileName = database.createTempFile(); |
162 | file = database.openFile(fileName, "rw", false); |
163 | file.setCheckedWriting(false); |
164 | file.setLength(FileStore.HEADER_LENGTH); |
165 | } |
166 | Data buff = Data.create(database, Constants.DEFAULT_PAGE_SIZE); |
167 | for (int i = 0; i < records.size(); i++) { |
168 | UndoLogRecord r = records.get(i); |
169 | buff.checkCapacity(Constants.DEFAULT_PAGE_SIZE); |
170 | r.append(buff, this); |
171 | if (i == records.size() - 1 || buff.length() > Constants.UNDO_BLOCK_SIZE) { |
172 | storedEntriesPos.add(file.getFilePointer()); |
173 | file.write(buff.getBytes(), 0, buff.length()); |
174 | buff.reset(); |
175 | } |
176 | } |
177 | storedEntries += records.size(); |
178 | memoryUndo = 0; |
179 | records.clear(); |
180 | file.autoDelete(); |
181 | return; |
182 | } |
183 | } else { |
184 | if (!entry.isStored()) { |
185 | memoryUndo++; |
186 | } |
187 | if (memoryUndo > database.getMaxMemoryUndo() && |
188 | database.isPersistent() && |
189 | !database.isMultiVersion()) { |
190 | if (file == null) { |
191 | String fileName = database.createTempFile(); |
192 | file = database.openFile(fileName, "rw", false); |
193 | file.setCheckedWriting(false); |
194 | file.seek(FileStore.HEADER_LENGTH); |
195 | rowBuff = Data.create(database, Constants.DEFAULT_PAGE_SIZE); |
196 | Data buff = rowBuff; |
197 | for (int i = 0; i < records.size(); i++) { |
198 | UndoLogRecord r = records.get(i); |
199 | saveIfPossible(r, buff); |
200 | } |
201 | } else { |
202 | saveIfPossible(entry, rowBuff); |
203 | } |
204 | file.autoDelete(); |
205 | } |
206 | } |
207 | } |
208 | |
209 | private void saveIfPossible(UndoLogRecord r, Data buff) { |
210 | if (!r.isStored() && r.canStore()) { |
211 | r.save(buff, file, this); |
212 | memoryUndo--; |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Get the table id for this undo log. If the table is not registered yet, |
218 | * this is done as well. |
219 | * |
220 | * @param table the table |
221 | * @return the id |
222 | */ |
223 | int getTableId(Table table) { |
224 | int id = table.getId(); |
225 | if (tables == null) { |
226 | tables = New.hashMap(); |
227 | } |
228 | // need to overwrite the old entry, because the old object |
229 | // might be deleted in the meantime |
230 | tables.put(id, table); |
231 | return id; |
232 | } |
233 | |
234 | /** |
235 | * Get the table for this id. The table must be registered for this undo log |
236 | * first by calling getTableId. |
237 | * |
238 | * @param id the table id |
239 | * @return the table object |
240 | */ |
241 | Table getTable(int id) { |
242 | return tables.get(id); |
243 | } |
244 | |
245 | } |