30f7c7724f986bc40073b172f351de55a1cf1265
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / newvfs / persistent / FSRecords.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vfs.newvfs.persistent;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.application.ApplicationNamesInfo;
20 import com.intellij.openapi.application.PathManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.util.Disposer;
23 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
24 import com.intellij.openapi.util.io.ByteSequence;
25 import com.intellij.openapi.util.io.FileAttributes;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.vfs.newvfs.FileAttribute;
28 import com.intellij.openapi.vfs.newvfs.impl.FileNameCache;
29 import com.intellij.util.ArrayUtil;
30 import com.intellij.util.BitUtil;
31 import com.intellij.util.CompressionUtil;
32 import com.intellij.util.SystemProperties;
33 import com.intellij.util.concurrency.AppExecutorUtil;
34 import com.intellij.util.containers.ConcurrentIntObjectMap;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.containers.IntArrayList;
37 import com.intellij.util.io.*;
38 import com.intellij.util.io.DataOutputStream;
39 import com.intellij.util.io.storage.*;
40 import gnu.trove.TIntArrayList;
41 import org.jetbrains.annotations.NotNull;
42 import org.jetbrains.annotations.Nullable;
43 import org.jetbrains.annotations.TestOnly;
44
45 import javax.swing.*;
46 import java.awt.*;
47 import java.io.*;
48 import java.nio.charset.Charset;
49 import java.security.MessageDigest;
50 import java.util.Arrays;
51 import java.util.concurrent.ConcurrentMap;
52 import java.util.concurrent.ExecutorService;
53 import java.util.concurrent.ScheduledFuture;
54 import java.util.concurrent.locks.ReentrantReadWriteLock;
55
56 /**
57  * @author max
58  */
59 @SuppressWarnings({"PointlessArithmeticExpression", "HardCodedStringLiteral"})
60 public class FSRecords {
61   private static final Logger LOG = Logger.getInstance("#com.intellij.vfs.persistent.FSRecords");
62
63   public static final boolean weHaveContentHashes = SystemProperties.getBooleanProperty("idea.share.contents", true);
64   private static final boolean lazyVfsDataCleaning = SystemProperties.getBooleanProperty("idea.lazy.vfs.data.cleaning", true);
65   static final boolean backgroundVfsFlush = SystemProperties.getBooleanProperty("idea.background.vfs.flush", true);
66   public static final boolean persistentAttributesList = SystemProperties.getBooleanProperty("idea.persistent.attr.list", true);
67   private static final boolean inlineAttributes = SystemProperties.getBooleanProperty("idea.inline.vfs.attributes", true);
68   static final boolean bulkAttrReadSupport = SystemProperties.getBooleanProperty("idea.bulk.attr.read", false);
69   static final boolean useSnappyForCompression = SystemProperties.getBooleanProperty("idea.use.snappy.for.vfs", false);
70   private static final boolean useSmallAttrTable = SystemProperties.getBooleanProperty("idea.use.small.attr.table.for.vfs", true);
71   static final String VFS_FILES_EXTENSION = System.getProperty("idea.vfs.files.extension", ".dat");
72
73   private static final int VERSION = 21 + (weHaveContentHashes ? 0x10:0) + (IOUtil.ourByteBuffersUseNativeByteOrder ? 0x37:0) +
74                                      (persistentAttributesList ? 31 : 0) + (bulkAttrReadSupport ? 0x27:0) + (inlineAttributes ? 0x31 : 0) +
75                                      (useSnappyForCompression ? 0x7f : 0) + (useSmallAttrTable ? 0x31 : 0) +
76                                      (PersistentHashMapValueStorage.COMPRESSION_ENABLED ? 21:0);
77
78   private static final int PARENT_OFFSET = 0;
79   private static final int PARENT_SIZE = 4;
80   private static final int NAME_OFFSET = PARENT_OFFSET + PARENT_SIZE;
81   private static final int NAME_SIZE = 4;
82   private static final int FLAGS_OFFSET = NAME_OFFSET + NAME_SIZE;
83   private static final int FLAGS_SIZE = 4;
84   private static final int ATTR_REF_OFFSET = FLAGS_OFFSET + FLAGS_SIZE;
85   private static final int ATTR_REF_SIZE = 4;
86   private static final int CONTENT_OFFSET = ATTR_REF_OFFSET + ATTR_REF_SIZE;
87   private static final int CONTENT_SIZE = 4;
88   private static final int TIMESTAMP_OFFSET = CONTENT_OFFSET + CONTENT_SIZE;
89   private static final int TIMESTAMP_SIZE = 8;
90   private static final int MOD_COUNT_OFFSET = TIMESTAMP_OFFSET + TIMESTAMP_SIZE;
91   private static final int MOD_COUNT_SIZE = 4;
92   private static final int LENGTH_OFFSET = MOD_COUNT_OFFSET + MOD_COUNT_SIZE;
93   private static final int LENGTH_SIZE = 8;
94
95   private static final int RECORD_SIZE = LENGTH_OFFSET + LENGTH_SIZE;
96
97   private static final byte[] ZEROES = new byte[RECORD_SIZE];
98
99   private static final int HEADER_VERSION_OFFSET = 0;
100   //private static final int HEADER_RESERVED_4BYTES_OFFSET = 4; // reserved
101   private static final int HEADER_GLOBAL_MOD_COUNT_OFFSET = 8;
102   private static final int HEADER_CONNECTION_STATUS_OFFSET = 12;
103   private static final int HEADER_TIMESTAMP_OFFSET = 16;
104   private static final int HEADER_SIZE = HEADER_TIMESTAMP_OFFSET + 8;
105
106   private static final int CONNECTED_MAGIC = 0x12ad34e4;
107   private static final int SAFELY_CLOSED_MAGIC = 0x1f2f3f4f;
108   private static final int CORRUPTED_MAGIC = 0xabcf7f7f;
109
110   private static final FileAttribute ourChildrenAttr = new FileAttribute("FsRecords.DIRECTORY_CHILDREN");
111
112   private static final ReentrantReadWriteLock.ReadLock r;
113   private static final ReentrantReadWriteLock.WriteLock w;
114
115   private static volatile int ourLocalModificationCount;
116   private static volatile boolean ourIsDisposed;
117
118   private static final int FREE_RECORD_FLAG = 0x100;
119   private static final int ALL_VALID_FLAGS = PersistentFS.ALL_VALID_FLAGS | FREE_RECORD_FLAG;
120
121   static {
122     //noinspection ConstantConditions
123     assert HEADER_SIZE <= RECORD_SIZE;
124
125     ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
126     r = lock.readLock();
127     w = lock.writeLock();
128   }
129
130   static void writeAttributesToRecord(int id, int parentId, @NotNull FileAttributes attributes, @NotNull String name) {
131     w.lock();
132     try {
133       setName(id, name);
134
135       setTimestamp(id, attributes.lastModified);
136       setLength(id, attributes.isDirectory() ? -1L : attributes.length);
137
138       setFlags(id, (attributes.isDirectory() ? PersistentFS.IS_DIRECTORY_FLAG : 0) |
139                              (attributes.isWritable() ? 0 : PersistentFS.IS_READ_ONLY) |
140                              (attributes.isSymLink() ? PersistentFS.IS_SYMLINK : 0) |
141                              (attributes.isSpecial() ? PersistentFS.IS_SPECIAL : 0) |
142                              (attributes.isHidden() ? PersistentFS.IS_HIDDEN : 0), true);
143       setParent(id, parentId);
144     }
145     catch (Throwable e) {
146       DbConnection.handleError(e);
147     }
148     finally {
149       w.unlock();
150     }
151   }
152
153   static void requestVfsRebuild(Throwable e) {
154     //noinspection ThrowableResultOfMethodCallIgnored
155     DbConnection.handleError(e);
156   }
157
158   static File basePath() {
159     return new File(DbConnection.getCachesDir());
160   }
161
162   public static class DbConnection {
163     private static boolean ourInitialized;
164     private static final ConcurrentMap<String, Integer> myAttributeIds = ContainerUtil.newConcurrentMap();
165
166     private static PersistentStringEnumerator myNames;
167     private static Storage myAttributes;
168     private static RefCountingStorage myContents;
169     private static ResizeableMappedFile myRecords;
170     private static PersistentBTreeEnumerator<byte[]> myContentHashesEnumerator;
171     private static final VfsDependentEnum<String> myAttributesList =
172       new VfsDependentEnum<>("attrib", EnumeratorStringDescriptor.INSTANCE, 1);
173     private static final TIntArrayList myFreeRecords = new TIntArrayList();
174
175     private static boolean myDirty;
176     private static ScheduledFuture<?> myFlushingFuture;
177     private static boolean myCorrupted;
178
179     private static final AttrPageAwareCapacityAllocationPolicy REASONABLY_SMALL = new AttrPageAwareCapacityAllocationPolicy();
180
181
182     public static void connect() {
183       w.lock();
184       try {
185         if (!ourInitialized) {
186           init();
187           setupFlushing();
188           ourInitialized = true;
189         }
190       }
191       finally {
192         w.unlock();
193       }
194     }
195
196     private static void scanFreeRecords() {
197       final int filelength = (int)myRecords.length();
198       LOG.assertTrue(filelength % RECORD_SIZE == 0, "invalid file size: " + filelength);
199
200       int count = filelength / RECORD_SIZE;
201       for (int n = 2; n < count; n++) {
202         if (BitUtil.isSet(getFlags(n), FREE_RECORD_FLAG)) {
203           myFreeRecords.add(n);
204         }
205       }
206     }
207
208     static int getFreeRecord() {
209       if (myFreeRecords.isEmpty()) return 0;
210       return myFreeRecords.remove(myFreeRecords.size() - 1);
211     }
212
213     private static void createBrokenMarkerFile(@Nullable Throwable reason) {
214       final File brokenMarker = getCorruptionMarkerFile();
215
216       try {
217         final ByteArrayOutputStream out = new ByteArrayOutputStream();
218         try (PrintStream stream = new PrintStream(out)) {
219           new Exception().printStackTrace(stream);
220           if (reason != null) {
221             stream.print("\nReason:\n");
222             reason.printStackTrace(stream);
223           }
224         }
225         LOG.info("Creating VFS corruption marker; Trace=\n" + out);
226
227         try (FileWriter writer = new FileWriter(brokenMarker)) {
228           writer.write("These files are corrupted and must be rebuilt from the scratch on next startup");
229         }
230       }
231       catch (IOException e) {
232         // No luck.
233       }
234     }
235
236     private static File getCorruptionMarkerFile() {
237       return new File(basePath(), "corruption.marker");
238     }
239
240     private static void init() {
241       final File basePath = basePath().getAbsoluteFile();
242       basePath.mkdirs();
243
244       final File namesFile = new File(basePath, "names" + VFS_FILES_EXTENSION);
245       final File attributesFile = new File(basePath, "attrib" + VFS_FILES_EXTENSION);
246       final File contentsFile = new File(basePath, "content" + VFS_FILES_EXTENSION);
247       final File contentsHashesFile = new File(basePath, "contentHashes" + VFS_FILES_EXTENSION);
248       final File recordsFile = new File(basePath, "records" + VFS_FILES_EXTENSION);
249
250       final File vfsDependentEnumBaseFile = VfsDependentEnum.getBaseFile();
251
252       if (!namesFile.exists()) {
253         invalidateIndex("'" + namesFile.getPath() + "' does not exist");
254       }
255
256       try {
257         if (getCorruptionMarkerFile().exists()) {
258           invalidateIndex("corruption marker found");
259           throw new IOException("Corruption marker file found");
260         }
261
262         PagedFileStorage.StorageLockContext storageLockContext = new PagedFileStorage.StorageLockContext(false);
263         myNames = new PersistentStringEnumerator(namesFile, storageLockContext);
264
265         myAttributes = new Storage(attributesFile.getPath(), REASONABLY_SMALL) {
266           @Override
267           protected AbstractRecordsTable createRecordsTable(PagePool pool, File recordsFile) throws IOException {
268             return inlineAttributes && useSmallAttrTable ? new CompactRecordsTable(recordsFile, pool, false) : super.createRecordsTable(pool, recordsFile);
269           }
270         };
271         myContents = new RefCountingStorage(contentsFile.getPath(), CapacityAllocationPolicy.FIVE_PERCENT_FOR_GROWTH, useSnappyForCompression) {
272           @NotNull
273           @Override
274           protected ExecutorService createExecutor() {
275             return AppExecutorUtil.createBoundedApplicationPoolExecutor("FSRecords pool",1);
276           }
277         }; // sources usually zipped with 4x ratio
278         myContentHashesEnumerator = weHaveContentHashes ? new ContentHashesUtil.HashEnumerator(contentsHashesFile, storageLockContext): null;
279         boolean aligned = PagedFileStorage.BUFFER_SIZE % RECORD_SIZE == 0;
280         assert aligned; // for performance
281         myRecords = new ResizeableMappedFile(recordsFile, 20 * 1024, storageLockContext,
282                                              PagedFileStorage.BUFFER_SIZE, aligned, IOUtil.ourByteBuffersUseNativeByteOrder);
283
284         if (myRecords.length() == 0) {
285           cleanRecord(0); // Clean header
286           cleanRecord(1); // Create root record
287           setCurrentVersion();
288         }
289
290         if (getVersion() != VERSION) {
291           throw new IOException("FS repository version mismatch");
292         }
293
294         if (myRecords.getInt(HEADER_CONNECTION_STATUS_OFFSET) != SAFELY_CLOSED_MAGIC) {
295           throw new IOException("FS repository wasn't safely shut down");
296         }
297         markDirty();
298         scanFreeRecords();
299       }
300       catch (Exception e) { // IOException, IllegalArgumentException
301         LOG.info("Filesystem storage is corrupted or does not exist. [Re]Building. Reason: " + e.getMessage());
302         try {
303           closeFiles();
304
305           boolean deleted = FileUtil.delete(getCorruptionMarkerFile());
306           deleted &= IOUtil.deleteAllFilesStartingWith(namesFile);
307           deleted &= AbstractStorage.deleteFiles(attributesFile.getPath());
308           deleted &= AbstractStorage.deleteFiles(contentsFile.getPath());
309           deleted &= IOUtil.deleteAllFilesStartingWith(contentsHashesFile);
310           deleted &= IOUtil.deleteAllFilesStartingWith(recordsFile);
311           deleted &= IOUtil.deleteAllFilesStartingWith(vfsDependentEnumBaseFile);
312
313           if (!deleted) {
314             throw new IOException("Cannot delete filesystem storage files");
315           }
316         }
317         catch (final IOException e1) {
318           final Runnable warnAndShutdown = () -> {
319             if (ApplicationManager.getApplication().isUnitTestMode()) {
320               //noinspection CallToPrintStackTrace
321               e1.printStackTrace();
322             }
323             else {
324               final String message = "Files in " + basePath.getPath() + " are locked.\n" +
325                                      ApplicationNamesInfo.getInstance().getProductName() + " will not be able to start up.";
326               if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
327                 JOptionPane.showMessageDialog(JOptionPane.getRootFrame(), message, "Fatal Error", JOptionPane.ERROR_MESSAGE);
328               }
329               else {
330                 //noinspection UseOfSystemOutOrSystemErr
331                 System.err.println(message);
332               }
333             }
334             Runtime.getRuntime().halt(1);
335           };
336
337           if (EventQueue.isDispatchThread()) {
338             warnAndShutdown.run();
339           }
340           else {
341             //noinspection SSBasedInspection
342             SwingUtilities.invokeLater(warnAndShutdown);
343           }
344
345           throw new RuntimeException("Can't rebuild filesystem storage ", e1);
346         }
347
348         init();
349       }
350     }
351
352     private static void invalidateIndex(String reason) {
353       LOG.info("Marking VFS as corrupted: " + reason);
354       final File indexRoot = PathManager.getIndexRoot();
355       if (indexRoot.exists()) {
356         final String[] children = indexRoot.list();
357         if (children != null && children.length > 0) {
358           // create index corruption marker only if index directory exists and is non-empty
359           // It is incorrect to consider non-existing indices "corrupted"
360           FileUtil.createIfDoesntExist(new File(PathManager.getIndexRoot(), "corruption.marker"));
361         }
362       }
363     }
364
365     private static String getCachesDir() {
366       String dir = System.getProperty("caches_dir");
367       return dir == null ? PathManager.getSystemPath() + "/caches/" : dir;
368     }
369
370     private static void markDirty() {
371       if (!myDirty) {
372         myDirty = true;
373         myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, CONNECTED_MAGIC);
374       }
375     }
376
377     private static void setupFlushing() {
378       if (!backgroundVfsFlush)
379         return;
380
381       myFlushingFuture = FlushingDaemon.everyFiveSeconds(new Runnable() {
382         private int lastModCount;
383
384         @Override
385         public void run() {
386           if (lastModCount == ourLocalModificationCount) {
387             flush();
388           }
389           lastModCount = ourLocalModificationCount;
390         }
391       });
392     }
393
394     public static void force() {
395       w.lock();
396       try {
397         doForce();
398       }
399       finally {
400         w.unlock();
401       }
402     }
403
404     private static void doForce() {
405       if (myNames != null) {
406         myNames.force();
407         myAttributes.force();
408         myContents.force();
409         if (myContentHashesEnumerator != null) myContentHashesEnumerator.force();
410         markClean();
411         myRecords.force();
412       }
413     }
414
415     private static void flush() {
416       if (!isDirty() || HeavyProcessLatch.INSTANCE.isRunning()) return;
417
418       r.lock();
419       try {
420         if (myFlushingFuture == null) {
421           return; // avoid NPE when close has already taken place
422         }
423         doForce();
424       }
425       finally {
426         r.unlock();
427       }
428     }
429
430     public static boolean isDirty() {
431       return myDirty || myNames.isDirty() || myAttributes.isDirty() || myContents.isDirty() || myRecords.isDirty() ||
432              myContentHashesEnumerator != null && myContentHashesEnumerator.isDirty();
433     }
434
435
436     private static int getVersion() {
437       final int recordsVersion = myRecords.getInt(HEADER_VERSION_OFFSET);
438       if (myAttributes.getVersion() != recordsVersion || myContents.getVersion() != recordsVersion) return -1;
439
440       return recordsVersion;
441     }
442
443     public static long getTimestamp() {
444       return myRecords.getLong(HEADER_TIMESTAMP_OFFSET);
445     }
446
447     private static void setCurrentVersion() {
448       myRecords.putInt(HEADER_VERSION_OFFSET, VERSION);
449       myRecords.putLong(HEADER_TIMESTAMP_OFFSET, System.currentTimeMillis());
450       myAttributes.setVersion(VERSION);
451       myContents.setVersion(VERSION);
452       myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, SAFELY_CLOSED_MAGIC);
453     }
454
455     static void cleanRecord(int id) {
456       myRecords.put(id * RECORD_SIZE, ZEROES, 0, RECORD_SIZE);
457     }
458
459     public static PersistentStringEnumerator getNames() {
460       return myNames;
461     }
462
463     private static void closeFiles() throws IOException {
464       if (myFlushingFuture != null) {
465         myFlushingFuture.cancel(false);
466         myFlushingFuture = null;
467       }
468
469       if (myNames != null) {
470         myNames.close();
471         myNames = null;
472       }
473
474       if (myAttributes != null) {
475         Disposer.dispose(myAttributes);
476         myAttributes = null;
477       }
478
479       if (myContents != null) {
480         Disposer.dispose(myContents);
481         myContents = null;
482       }
483
484       if (myContentHashesEnumerator != null) {
485         myContentHashesEnumerator.close();
486         myContentHashesEnumerator = null;
487       }
488
489       if (myRecords != null) {
490         markClean();
491         myRecords.close();
492         myRecords = null;
493       }
494       ourInitialized = false;
495     }
496
497     private static void markClean() {
498       if (myDirty) {
499         myDirty = false;
500         myRecords.putInt(HEADER_CONNECTION_STATUS_OFFSET, myCorrupted ? CORRUPTED_MAGIC : SAFELY_CLOSED_MAGIC);
501       }
502     }
503
504     private static final int RESERVED_ATTR_ID = bulkAttrReadSupport ? 1 : 0;
505     private static final int FIRST_ATTR_ID_OFFSET = bulkAttrReadSupport ? RESERVED_ATTR_ID : 0;
506
507     private static int getAttributeId(@NotNull String attId) throws IOException {
508       if (persistentAttributesList) {
509         return myAttributesList.getId(attId) + FIRST_ATTR_ID_OFFSET;
510       }
511       Integer integer = myAttributeIds.get(attId);
512       if (integer != null) return integer.intValue();
513       int enumeratedId = myNames.enumerate(attId);
514       integer = myAttributeIds.putIfAbsent(attId, enumeratedId);
515       return integer == null ? enumeratedId:  integer.intValue();
516     }
517
518     private static void handleError(@NotNull Throwable e) throws RuntimeException, Error {
519       if (!ourIsDisposed) {
520         // No need to forcibly mark VFS corrupted if it is already shut down
521         if (!myCorrupted && w.tryLock()) { // avoid deadlock if r lock is occupied by current thread
522           w.unlock();
523           createBrokenMarkerFile(e);
524           myCorrupted = true;
525           force();
526         }
527       }
528
529       if (e instanceof Error) throw (Error)e;
530       if (e instanceof RuntimeException) throw (RuntimeException)e;
531       throw new RuntimeException(e);
532     }
533
534     private static class AttrPageAwareCapacityAllocationPolicy extends CapacityAllocationPolicy {
535       boolean myAttrPageRequested;
536
537       @Override
538       public int calculateCapacity(int requiredLength) {   // 20% for growth
539         return Math.max(myAttrPageRequested ? 8:32, Math.min((int)(requiredLength * 1.2), (requiredLength / 1024 + 1) * 1024));
540       }
541     }
542   }
543
544   private FSRecords() {
545   }
546
547   public static void connect() {
548     DbConnection.connect();
549   }
550
551   public static long getCreationTimestamp() {
552     r.lock();
553     try {
554       return DbConnection.getTimestamp();
555     }
556     finally {
557       r.unlock();
558     }
559   }
560
561   private static ResizeableMappedFile getRecords() {
562     return DbConnection.myRecords;
563   }
564
565   private static PersistentBTreeEnumerator<byte[]> getContentHashesEnumerator() {
566     return DbConnection.myContentHashesEnumerator;
567   }
568
569   private static RefCountingStorage getContentStorage() {
570     return DbConnection.myContents;
571   }
572
573   private static Storage getAttributesStorage() {
574     return DbConnection.myAttributes;
575   }
576
577   public static PersistentStringEnumerator getNames() {
578     return DbConnection.getNames();
579   }
580
581   // todo: Address  / capacity store in records table, size store with payload
582   public static int createRecord() {
583     w.lock();
584     try {
585       DbConnection.markDirty();
586
587       final int free = DbConnection.getFreeRecord();
588       if (free == 0) {
589         final int fileLength = length();
590         LOG.assertTrue(fileLength % RECORD_SIZE == 0);
591         int newRecord = fileLength / RECORD_SIZE;
592         DbConnection.cleanRecord(newRecord);
593         assert fileLength + RECORD_SIZE == length();
594         return newRecord;
595       }
596       else {
597         if (lazyVfsDataCleaning) deleteContentAndAttributes(free);
598         DbConnection.cleanRecord(free);
599         return free;
600       }
601     }
602     catch (Throwable e) {
603       DbConnection.handleError(e);
604     }
605     finally {
606       w.unlock();
607     }
608     return -1;
609   }
610
611   private static int length() {
612     return (int)getRecords().length();
613   }
614   public static int getMaxId() {
615     r.lock();
616     try {
617       return length()/RECORD_SIZE;
618     }
619     finally {
620       r.unlock();
621     }
622   }
623
624   static void deleteRecordRecursively(int id) {
625     w.lock();
626     try {
627       incModCount(id);
628       if (lazyVfsDataCleaning) {
629         markAsDeletedRecursively(id);
630       } else {
631         doDeleteRecursively(id);
632       }
633     }
634     catch (Throwable e) {
635       DbConnection.handleError(e);
636     }
637     finally {
638       w.unlock();
639     }
640   }
641
642   private static void markAsDeletedRecursively(final int id) {
643     for (int subrecord : list(id)) {
644       markAsDeletedRecursively(subrecord);
645     }
646
647     markAsDeleted(id);
648   }
649
650   private static void markAsDeleted(final int id) {
651     w.lock();
652     try {
653       DbConnection.markDirty();
654       addToFreeRecordsList(id);
655     }
656     catch (Throwable e) {
657       DbConnection.handleError(e);
658     }
659     finally {
660       w.unlock();
661     }
662   }
663
664   private static void doDeleteRecursively(final int id) {
665     for (int subrecord : list(id)) {
666       doDeleteRecursively(subrecord);
667     }
668
669     deleteRecord(id);
670   }
671
672   private static void deleteRecord(final int id) {
673     w.lock();
674     try {
675       DbConnection.markDirty();
676       deleteContentAndAttributes(id);
677
678       DbConnection.cleanRecord(id);
679       addToFreeRecordsList(id);
680     }
681     catch (Throwable e) {
682       DbConnection.handleError(e);
683     }
684     finally {
685       w.unlock();
686     }
687   }
688
689   private static void deleteContentAndAttributes(int id) throws IOException {
690     int content_page = getContentRecordId(id);
691     if (content_page != 0) {
692       if (weHaveContentHashes) {
693         getContentStorage().releaseRecord(content_page, false);
694       } else {
695         getContentStorage().releaseRecord(content_page);
696       }
697     }
698
699     int att_page = getAttributeRecordId(id);
700     if (att_page != 0) {
701       final DataInputStream attStream = getAttributesStorage().readStream(att_page);
702       if (bulkAttrReadSupport) skipRecordHeader(attStream, DbConnection.RESERVED_ATTR_ID, id);
703
704       while (attStream.available() > 0) {
705         DataInputOutputUtil.readINT(attStream);// Attribute ID;
706         int attAddressOrSize = DataInputOutputUtil.readINT(attStream);
707
708         if (inlineAttributes) {
709           if(attAddressOrSize < MAX_SMALL_ATTR_SIZE) {
710             attStream.skipBytes(attAddressOrSize);
711             continue;
712           }
713           attAddressOrSize -= MAX_SMALL_ATTR_SIZE;
714         }
715         getAttributesStorage().deleteRecord(attAddressOrSize);
716       }
717       attStream.close();
718       getAttributesStorage().deleteRecord(att_page);
719     }
720   }
721
722   private static void addToFreeRecordsList(int id) {
723     // DbConnection.addFreeRecord(id); // important! Do not add fileId to free list until restart
724     setFlags(id, FREE_RECORD_FLAG, false);
725   }
726
727   static int[] listRoots() {
728     try {
729       r.lock();
730       try {
731         final DataInputStream input = readAttribute(1, ourChildrenAttr);
732         if (input == null) return ArrayUtil.EMPTY_INT_ARRAY;
733
734         try {
735           final int count = DataInputOutputUtil.readINT(input);
736           int[] result = ArrayUtil.newIntArray(count);
737           int prevId = 0;
738           for (int i = 0; i < count; i++) {
739             DataInputOutputUtil.readINT(input); // Name
740             prevId = result[i] = DataInputOutputUtil.readINT(input) + prevId; // Id
741           }
742           return result;
743         }
744         finally {
745           input.close();
746         }
747       }
748       finally {
749         r.unlock();
750       }
751     }
752     catch (Throwable e) {
753       DbConnection.handleError(e);
754       return null;
755     }
756   }
757
758   @TestOnly
759   public static void force() {
760     DbConnection.force();
761   }
762
763   @TestOnly
764   public static boolean isDirty() {
765     return DbConnection.isDirty();
766   }
767
768   private static void saveNameIdSequenceWithDeltas(int[] names, int[] ids, DataOutputStream output) throws IOException {
769     DataInputOutputUtil.writeINT(output, names.length);
770     int prevId = 0;
771     int prevNameId = 0;
772     for (int i = 0; i < names.length; i++) {
773       DataInputOutputUtil.writeINT(output, names[i] - prevNameId);
774       DataInputOutputUtil.writeINT(output, ids[i] - prevId);
775       prevId = ids[i];
776       prevNameId = names[i];
777     }
778   }
779
780   static int findRootRecord(@NotNull String rootUrl) {
781     w.lock();
782
783     try {
784       DbConnection.markDirty();
785       final int root = getNames().enumerate(rootUrl);
786
787       final DataInputStream input = readAttribute(1, ourChildrenAttr);
788       int[] names = ArrayUtil.EMPTY_INT_ARRAY;
789       int[] ids = ArrayUtil.EMPTY_INT_ARRAY;
790
791       if (input != null) {
792         try {
793           final int count = DataInputOutputUtil.readINT(input);
794           names = ArrayUtil.newIntArray(count);
795           ids = ArrayUtil.newIntArray(count);
796           int prevId = 0;
797           int prevNameId = 0;
798
799           for (int i = 0; i < count; i++) {
800             final int name = DataInputOutputUtil.readINT(input) + prevNameId;
801             final int id = DataInputOutputUtil.readINT(input) + prevId;
802             if (name == root) {
803               return id;
804             }
805
806             prevNameId = names[i] = name;
807             prevId = ids[i] = id;
808           }
809         }
810         finally {
811           input.close();
812         }
813       }
814
815       int id;
816       try (DataOutputStream output = writeAttribute(1, ourChildrenAttr)) {
817         id = createRecord();
818
819         int index = Arrays.binarySearch(ids, id);
820         ids = ArrayUtil.insert(ids, -index - 1, id);
821         names = ArrayUtil.insert(names, -index - 1, root);
822
823         saveNameIdSequenceWithDeltas(names, ids, output);
824       }
825
826       return id;
827     }
828     catch (Throwable e) {
829       DbConnection.handleError(e);
830     }
831     finally {
832       w.unlock();
833     }
834     return -1;
835   }
836
837   static void deleteRootRecord(int id) {
838     w.lock();
839
840     try {
841       DbConnection.markDirty();
842       final DataInputStream input = readAttribute(1, ourChildrenAttr);
843       assert input != null;
844       int[] names;
845       int[] ids;
846       try {
847         int count = DataInputOutputUtil.readINT(input);
848
849         names = ArrayUtil.newIntArray(count);
850         ids = ArrayUtil.newIntArray(count);
851         int prevId = 0;
852         int prevNameId = 0;
853         for (int i = 0; i < count; i++) {
854           names[i] = DataInputOutputUtil.readINT(input) + prevNameId;
855           ids[i] = DataInputOutputUtil.readINT(input) + prevId;
856           prevId = ids[i];
857           prevNameId = names[i];
858         }
859       }
860       finally {
861         input.close();
862       }
863
864       final int index = ArrayUtil.find(ids, id);
865       assert index >= 0;
866
867       names = ArrayUtil.remove(names, index);
868       ids = ArrayUtil.remove(ids, index);
869
870       try (DataOutputStream output = writeAttribute(1, ourChildrenAttr)) {
871         saveNameIdSequenceWithDeltas(names, ids, output);
872       }
873     }
874     catch (Throwable e) {
875       DbConnection.handleError(e);
876     }
877     finally {
878       w.unlock();
879     }
880   }
881
882   @NotNull
883   public static int[] list(int id) {
884     try {
885       r.lock();
886       try {
887         final DataInputStream input = readAttribute(id, ourChildrenAttr);
888         if (input == null) return ArrayUtil.EMPTY_INT_ARRAY;
889
890         final int count = DataInputOutputUtil.readINT(input);
891         final int[] result = ArrayUtil.newIntArray(count);
892         int prevId = id;
893         for (int i = 0; i < count; i++) {
894           prevId = result[i] = DataInputOutputUtil.readINT(input) + prevId;
895         }
896         input.close();
897         return result;
898       }
899       finally {
900         r.unlock();
901       }
902     }
903     catch (Throwable e) {
904       DbConnection.handleError(e);
905       return ArrayUtil.EMPTY_INT_ARRAY;
906     }
907   }
908
909   public static class NameId {
910     @NotNull
911     public static final NameId[] EMPTY_ARRAY = new NameId[0];
912     public final int id;
913     public final CharSequence name;
914     public final int nameId;
915
916     public NameId(int id, int nameId, @NotNull CharSequence name) {
917       this.id = id;
918       this.nameId = nameId;
919       this.name = name;
920     }
921
922     @Override
923     public String toString() {
924       return name + " (" + id + ")";
925     }
926   }
927
928   @NotNull
929   public static NameId[] listAll(int parentId) {
930     try {
931       r.lock();
932       try {
933         final DataInputStream input = readAttribute(parentId, ourChildrenAttr);
934         if (input == null) return NameId.EMPTY_ARRAY;
935
936         int count = DataInputOutputUtil.readINT(input);
937         NameId[] result = count == 0 ? NameId.EMPTY_ARRAY : new NameId[count];
938         int prevId = parentId;
939         for (int i = 0; i < count; i++) {
940           int id = DataInputOutputUtil.readINT(input) + prevId;
941           prevId = id;
942           int nameId = getNameId(id);
943           result[i] = new NameId(id, nameId, FileNameCache.getVFileName(nameId));
944         }
945         input.close();
946         return result;
947       }
948       finally {
949         r.unlock();
950       }
951     }
952     catch (Throwable e) {
953       DbConnection.handleError(e);
954       return NameId.EMPTY_ARRAY;
955     }
956   }
957
958   static boolean wereChildrenAccessed(int id) {
959     try {
960       r.lock();
961       try {
962         return findAttributePage(id, ourChildrenAttr, false) != 0;
963       } finally {
964         r.unlock();
965       }
966     }
967     catch (Throwable e) {
968       DbConnection.handleError(e);
969     }
970     return false;
971   }
972
973   public static void updateList(int id, @NotNull int[] childIds) {
974     Arrays.sort(childIds);
975     w.lock();
976     try {
977       DbConnection.markDirty();
978       try (DataOutputStream record = writeAttribute(id, ourChildrenAttr)) {
979         DataInputOutputUtil.writeINT(record, childIds.length);
980
981         int prevId = id;
982         for (int childId : childIds) {
983           assert childId > 0 : childId;
984           if (childId == id) {
985             LOG.error("Cyclic parent child relations");
986           }
987           else {
988             int delta = childId - prevId;
989             DataInputOutputUtil.writeINT(record, delta);
990             prevId = childId;
991           }
992         }
993       }
994     }
995     catch (Throwable e) {
996       DbConnection.handleError(e);
997     }
998     finally {
999       w.unlock();
1000     }
1001   }
1002
1003   private static void incModCount(int id) {
1004     DbConnection.markDirty();
1005     ourLocalModificationCount++;
1006     final int count = getModCount() + 1;
1007     getRecords().putInt(HEADER_GLOBAL_MOD_COUNT_OFFSET, count);
1008
1009     int parent = id;
1010     int depth = 10000;
1011     while (parent != 0) {
1012       setModCount(parent, count);
1013       parent = getParent(parent);
1014       if (depth -- == 0) {
1015         LOG.error("Cyclic parent child relation? file: " + getName(id));
1016         return;
1017       }
1018     }
1019   }
1020
1021   static int getLocalModCount() {
1022     return ourLocalModificationCount; // This is volatile, only modified under Application.runWriteAction() lock.
1023   }
1024
1025   public static int getModCount() {
1026     r.lock();
1027     try {
1028       return getRecords().getInt(HEADER_GLOBAL_MOD_COUNT_OFFSET);
1029     }
1030     finally {
1031       r.unlock();
1032     }
1033   }
1034
1035   public static int getParent(int id) {
1036     try {
1037       r.lock();
1038       try {
1039         final int parentId = getRecordInt(id, PARENT_OFFSET);
1040         if (parentId == id) {
1041           LOG.error("Cyclic parent child relations in the database. id = " + id);
1042           return 0;
1043         }
1044
1045         return parentId;
1046       }
1047       finally {
1048         r.unlock();
1049       }
1050     }
1051     catch (Throwable e) {
1052       DbConnection.handleError(e);
1053     }
1054     return -1;
1055   }
1056
1057   // returns id, parent(id), parent(parent(id)), ...  (already cached id or rootId)
1058   @NotNull
1059   public static TIntArrayList getParents(int id, @NotNull ConcurrentIntObjectMap<?> idCache) {
1060     TIntArrayList result = new TIntArrayList(10);
1061     r.lock();
1062     try {
1063       int parentId;
1064       do {
1065         result.add(id);
1066         if (idCache.containsKey(id)) {
1067           break;
1068         }
1069         parentId = getRecordInt(id, PARENT_OFFSET);
1070         if (parentId == id || result.size() % 128 == 0 && result.contains(parentId)) {
1071           LOG.error("Cyclic parent child relations in the database. id = " + parentId);
1072           return result;
1073         }
1074         id = parentId;
1075       } while (parentId != 0);
1076     }
1077     catch (Throwable e) {
1078       DbConnection.handleError(e);
1079     }
1080     finally {
1081       r.unlock();
1082     }
1083     return result;
1084   }
1085
1086   public static void setParent(int id, int parentId) {
1087     if (id == parentId) {
1088       LOG.error("Cyclic parent/child relations");
1089       return;
1090     }
1091
1092     w.lock();
1093     try {
1094       incModCount(id);
1095       putRecordInt(id, PARENT_OFFSET, parentId);
1096     }
1097     catch (Throwable e) {
1098       DbConnection.handleError(e);
1099     }
1100     finally {
1101       w.unlock();
1102     }
1103   }
1104
1105   public static int getNameId(int id) {
1106     try {
1107       r.lock();
1108       try {
1109         return getRecordInt(id, NAME_OFFSET);
1110       }
1111       finally {
1112         r.unlock();
1113       }
1114     }
1115     catch (Throwable e) {
1116       DbConnection.handleError(e);
1117     }
1118     return -1;
1119   }
1120
1121   public static int getNameId(String name) {
1122     try {
1123       r.lock();
1124       try {
1125         return getNames().enumerate(name);
1126       }
1127       finally {
1128         r.unlock();
1129       }
1130     }
1131     catch (Throwable e) {
1132       DbConnection.handleError(e);
1133     }
1134     return -1;
1135   }
1136
1137   public static String getName(int id) {
1138     return getNameSequence(id).toString();
1139   }
1140
1141   @NotNull
1142   public static CharSequence getNameSequence(int id) {
1143     try {
1144       r.lock();
1145       try {
1146         final int nameId = getRecordInt(id, NAME_OFFSET);
1147         return nameId == 0 ? "" : FileNameCache.getVFileName(nameId);
1148       }
1149       finally {
1150         r.unlock();
1151       }
1152     }
1153     catch (Throwable e) {
1154       DbConnection.handleError(e);
1155       return "";
1156     }
1157   }
1158
1159   public static String getNameByNameId(int nameId) {
1160     try {
1161       r.lock();
1162       try {
1163         return nameId != 0 ? getNames().valueOf(nameId) : "";
1164       }
1165       finally {
1166         r.unlock();
1167       }
1168     }
1169     catch (Throwable e) {
1170       DbConnection.handleError(e);
1171     }
1172     return null;
1173   }
1174
1175   public static void setName(int id, @NotNull String name) {
1176     w.lock();
1177     try {
1178       incModCount(id);
1179       int nameId = getNames().enumerate(name);
1180       putRecordInt(id, NAME_OFFSET, nameId);
1181     }
1182     catch (Throwable e) {
1183       DbConnection.handleError(e);
1184     }
1185     finally {
1186       w.unlock();
1187     }
1188   }
1189
1190   public static int getFlags(int id) {
1191     r.lock();
1192     try {
1193       return getRecordInt(id, FLAGS_OFFSET);
1194     }
1195     finally {
1196       r.unlock();
1197     }
1198   }
1199
1200   public static void setFlags(int id, int flags, final boolean markAsChange) {
1201     w.lock();
1202     try {
1203       if (markAsChange) {
1204         incModCount(id);
1205       }
1206       putRecordInt(id, FLAGS_OFFSET, flags);
1207     }
1208     catch (Throwable e) {
1209       DbConnection.handleError(e);
1210     }
1211     finally {
1212       w.unlock();
1213     }
1214   }
1215
1216   public static long getLength(int id) {
1217     r.lock();
1218     try {
1219       return getRecords().getLong(getOffset(id, LENGTH_OFFSET));
1220     }
1221     finally {
1222       r.unlock();
1223     }
1224   }
1225
1226   public static void setLength(int id, long len) {
1227     w.lock();
1228     try {
1229       incModCount(id);
1230       getRecords().putLong(getOffset(id, LENGTH_OFFSET), len);
1231     }
1232     catch (Throwable e) {
1233       DbConnection.handleError(e);
1234     }
1235     finally {
1236       w.unlock();
1237     }
1238   }
1239
1240   public static long getTimestamp(int id) {
1241     r.lock();
1242     try {
1243       return getRecords().getLong(getOffset(id, TIMESTAMP_OFFSET));
1244     }
1245     finally {
1246       r.unlock();
1247     }
1248   }
1249
1250   public static void setTimestamp(int id, long value) {
1251     w.lock();
1252     try {
1253       incModCount(id);
1254       getRecords().putLong(getOffset(id, TIMESTAMP_OFFSET), value);
1255     }
1256     catch (Throwable e) {
1257       DbConnection.handleError(e);
1258     }
1259     finally {
1260       w.unlock();
1261     }
1262   }
1263
1264   static int getModCount(int id) {
1265     r.lock();
1266     try {
1267       return getRecordInt(id, MOD_COUNT_OFFSET);
1268     }
1269     finally {
1270       r.unlock();
1271     }
1272   }
1273
1274   private static void setModCount(int id, int value) {
1275     putRecordInt(id, MOD_COUNT_OFFSET, value);
1276   }
1277
1278   private static int getContentRecordId(int fileId) {
1279     return getRecordInt(fileId, CONTENT_OFFSET);
1280   }
1281
1282   private static void setContentRecordId(int id, int value) {
1283     putRecordInt(id, CONTENT_OFFSET, value);
1284   }
1285
1286   private static int getAttributeRecordId(int id) {
1287     return getRecordInt(id, ATTR_REF_OFFSET);
1288   }
1289
1290   private static void setAttributeRecordId(int id, int value) {
1291     putRecordInt(id, ATTR_REF_OFFSET, value);
1292   }
1293
1294   private static int getRecordInt(int id, int offset) {
1295     return getRecords().getInt(getOffset(id, offset));
1296   }
1297
1298   private static void putRecordInt(int id, int offset, int value) {
1299     getRecords().putInt(getOffset(id, offset), value);
1300   }
1301
1302   private static int getOffset(int id, int offset) {
1303     return id * RECORD_SIZE + offset;
1304   }
1305
1306   @Nullable
1307   public static DataInputStream readContent(int fileId) {
1308     try {
1309       r.lock();
1310       int page;
1311       try {
1312         checkFileIsValid(fileId);
1313
1314         page = getContentRecordId(fileId);
1315         if (page == 0) return null;
1316       }
1317       finally {
1318         r.unlock();
1319       }
1320       return doReadContentById(page);
1321     }
1322     catch (Throwable e) {
1323       DbConnection.handleError(e);
1324     }
1325     return null;
1326   }
1327
1328   @Nullable
1329   static DataInputStream readContentById(int contentId) {
1330     try {
1331       return doReadContentById(contentId);
1332     }
1333     catch (Throwable e) {
1334       DbConnection.handleError(e);
1335     }
1336     return null;
1337   }
1338
1339   private static DataInputStream doReadContentById(int contentId) throws IOException {
1340     DataInputStream stream = getContentStorage().readStream(contentId);
1341     if (useSnappyForCompression) {
1342       byte[] bytes = CompressionUtil.readCompressed(stream);
1343       stream = new DataInputStream(new ByteArrayInputStream(bytes));
1344     }
1345
1346     return stream;
1347   }
1348
1349   @Nullable
1350   public static DataInputStream readAttributeWithLock(int fileId, FileAttribute att) {
1351     try {
1352       r.lock();
1353       try {
1354         DataInputStream stream = readAttribute(fileId, att);
1355         if (stream != null && att.isVersioned()) {
1356           try {
1357             int actualVersion = DataInputOutputUtil.readINT(stream);
1358             if (actualVersion != att.getVersion()) {
1359               stream.close();
1360               return null;
1361             }
1362           }
1363           catch (IOException e) {
1364             stream.close();
1365             return null;
1366           }
1367         }
1368         return stream;
1369       }
1370       finally {
1371         r.unlock();
1372       }
1373     }
1374     catch (Throwable e) {
1375       DbConnection.handleError(e);
1376     }
1377     return null;
1378   }
1379
1380   // should be called under r or w lock
1381   @Nullable
1382   private static DataInputStream readAttribute(int fileId, FileAttribute attribute) throws IOException {
1383     checkFileIsValid(fileId);
1384
1385     int recordId = getAttributeRecordId(fileId);
1386     if (recordId == 0) return null;
1387     int encodedAttrId = DbConnection.getAttributeId(attribute.getId());
1388
1389     Storage storage = getAttributesStorage();
1390
1391     int page = 0;
1392
1393     try (DataInputStream attrRefs = storage.readStream(recordId)) {
1394       if (bulkAttrReadSupport) skipRecordHeader(attrRefs, DbConnection.RESERVED_ATTR_ID, fileId);
1395
1396       while (attrRefs.available() > 0) {
1397         final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1398         final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1399
1400         if (attIdOnPage != encodedAttrId) {
1401           if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1402             attrRefs.skipBytes(attrAddressOrSize);
1403           }
1404         }
1405         else {
1406           if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1407             byte[] b = new byte[attrAddressOrSize];
1408             attrRefs.readFully(b);
1409             return new DataInputStream(new ByteArrayInputStream(b));
1410           }
1411           page = inlineAttributes ? attrAddressOrSize - MAX_SMALL_ATTR_SIZE : attrAddressOrSize;
1412           break;
1413         }
1414       }
1415     }
1416
1417     if (page == 0) {
1418       return null;
1419     }
1420     DataInputStream stream = getAttributesStorage().readStream(page);
1421     if (bulkAttrReadSupport) skipRecordHeader(stream, encodedAttrId, fileId);
1422     return stream;
1423   }
1424
1425   // Vfs small attrs: store inline:
1426   // file's AttrId -> [size, capacity] attr record (RESERVED_ATTR_ID fileId)? (attrId ((smallAttrSize smallAttrData) | (attr record)) )
1427   // other attr record: (AttrId, fileId) ? attrData
1428   private static final int MAX_SMALL_ATTR_SIZE = 64;
1429
1430   private static int findAttributePage(int fileId, FileAttribute attr, boolean toWrite) throws IOException {
1431     checkFileIsValid(fileId);
1432
1433     int recordId = getAttributeRecordId(fileId);
1434     int encodedAttrId = DbConnection.getAttributeId(attr.getId());
1435     boolean directoryRecord = false;
1436
1437     Storage storage = getAttributesStorage();
1438
1439     if (recordId == 0) {
1440       if (!toWrite) return 0;
1441
1442       recordId = storage.createNewRecord();
1443       setAttributeRecordId(fileId, recordId);
1444       directoryRecord = true;
1445     }
1446     else {
1447       try (DataInputStream attrRefs = storage.readStream(recordId)) {
1448         if (bulkAttrReadSupport) skipRecordHeader(attrRefs, DbConnection.RESERVED_ATTR_ID, fileId);
1449
1450         while (attrRefs.available() > 0) {
1451           final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1452           final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1453
1454           if (attIdOnPage == encodedAttrId) {
1455             if (inlineAttributes) {
1456               return attrAddressOrSize < MAX_SMALL_ATTR_SIZE ? -recordId : attrAddressOrSize - MAX_SMALL_ATTR_SIZE;
1457             }
1458             else {
1459               return attrAddressOrSize;
1460             }
1461           }
1462           else {
1463             if (inlineAttributes && attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1464               attrRefs.skipBytes(attrAddressOrSize);
1465             }
1466           }
1467         }
1468       }
1469     }
1470
1471     if (toWrite) {
1472       Storage.AppenderStream appender = storage.appendStream(recordId);
1473       if (bulkAttrReadSupport) {
1474         if (directoryRecord) {
1475           DataInputOutputUtil.writeINT(appender, DbConnection.RESERVED_ATTR_ID);
1476           DataInputOutputUtil.writeINT(appender, fileId);
1477         }
1478       }
1479
1480       DataInputOutputUtil.writeINT(appender, encodedAttrId);
1481       int attrAddress = storage.createNewRecord();
1482       DataInputOutputUtil.writeINT(appender, inlineAttributes ? attrAddress + MAX_SMALL_ATTR_SIZE : attrAddress);
1483       DbConnection.REASONABLY_SMALL.myAttrPageRequested = true;
1484       try {
1485         appender.close();
1486       } finally {
1487         DbConnection.REASONABLY_SMALL.myAttrPageRequested = false;
1488       }
1489       return attrAddress;
1490     }
1491
1492     return 0;
1493   }
1494
1495   private static void skipRecordHeader(DataInputStream refs, int expectedRecordTag, int expectedFileId) throws IOException {
1496     int attId = DataInputOutputUtil.readINT(refs);// attrId
1497     assert attId == expectedRecordTag || expectedRecordTag == 0;
1498     int fileId = DataInputOutputUtil.readINT(refs);// fileId
1499     assert expectedFileId == fileId || expectedFileId == 0;
1500   }
1501
1502   private static void writeRecordHeader(int recordTag, int fileId, DataOutputStream appender) throws IOException {
1503     DataInputOutputUtil.writeINT(appender, recordTag);
1504     DataInputOutputUtil.writeINT(appender, fileId);
1505   }
1506
1507   private static void checkFileIsValid(int fileId) {
1508     assert fileId > 0 : fileId;
1509     // TODO: This assertion is a bit timey, will remove when bug is caught.
1510     if (!lazyVfsDataCleaning) {
1511       assert !BitUtil.isSet(getFlags(fileId), FREE_RECORD_FLAG) : "Accessing attribute of a deleted page: " + fileId + ":" + getName(fileId);
1512     }
1513   }
1514
1515   static int acquireFileContent(int fileId) {
1516     w.lock();
1517     try {
1518       int record = getContentRecordId(fileId);
1519       if (record > 0) getContentStorage().acquireRecord(record);
1520       return record;
1521     }
1522     catch (Throwable e) {
1523       DbConnection.handleError(e);
1524     }
1525     finally {
1526       w.unlock();
1527     }
1528     return -1;
1529   }
1530
1531   static void releaseContent(int contentId) {
1532     w.lock();
1533     try {
1534       RefCountingStorage contentStorage = getContentStorage();
1535       if (weHaveContentHashes) {
1536         contentStorage.releaseRecord(contentId, false);
1537       } else {
1538         contentStorage.releaseRecord(contentId);
1539       }
1540     }
1541     catch (Throwable e) {
1542       DbConnection.handleError(e);
1543     }
1544     finally {
1545       w.unlock();
1546     }
1547   }
1548
1549   public static int getContentId(int fileId) {
1550     try {
1551       r.lock();
1552       try {
1553         return getContentRecordId(fileId);
1554       }
1555       finally {
1556         r.unlock();
1557       }
1558     }
1559     catch (Throwable e) {
1560       DbConnection.handleError(e);
1561     }
1562     return -1;
1563   }
1564
1565   @NotNull
1566   static DataOutputStream writeContent(int fileId, boolean readOnly) {
1567     return new ContentOutputStream(fileId, readOnly);
1568   }
1569
1570   private static final MessageDigest myDigest = ContentHashesUtil.createHashDigest();
1571
1572   static void writeContent(int fileId, ByteSequence bytes, boolean readOnly) {
1573     try {
1574       new ContentOutputStream(fileId, readOnly).writeBytes(bytes);
1575     }
1576     catch (Throwable e) {
1577       DbConnection.handleError(e);
1578     }
1579   }
1580
1581   static int storeUnlinkedContent(byte[] bytes) {
1582     w.lock();
1583     try {
1584       int recordId;
1585
1586       if (weHaveContentHashes) {
1587         recordId = findOrCreateContentRecord(bytes, 0, bytes.length);
1588         if (recordId > 0) return recordId;
1589         recordId = -recordId;
1590       } else {
1591         recordId = getContentStorage().acquireNewRecord();
1592       }
1593       AbstractStorage.StorageDataOutput output = getContentStorage().writeStream(recordId, true);
1594       output.write(bytes);
1595       output.close();
1596       return recordId;
1597     }
1598     catch (IOException e) {
1599       DbConnection.handleError(e);
1600     }
1601     finally {
1602       w.unlock();
1603     }
1604     return -1;
1605   }
1606
1607   @NotNull
1608   public static DataOutputStream writeAttribute(final int fileId, @NotNull FileAttribute att) {
1609     DataOutputStream stream = new AttributeOutputStream(fileId, att);
1610     if (att.isVersioned()) {
1611       try {
1612         DataInputOutputUtil.writeINT(stream, att.getVersion());
1613       }
1614       catch (IOException e) {
1615         throw new RuntimeException(e);
1616       }
1617     }
1618     return stream;
1619   }
1620
1621   private static class ContentOutputStream extends DataOutputStream {
1622     final int myFileId;
1623     final boolean myFixedSize;
1624
1625     private ContentOutputStream(final int fileId, boolean readOnly) {
1626       super(new BufferExposingByteArrayOutputStream());
1627       myFileId = fileId;
1628       myFixedSize = readOnly;
1629     }
1630
1631     @Override
1632     public void close() throws IOException {
1633       super.close();
1634
1635       try {
1636         final BufferExposingByteArrayOutputStream _out = (BufferExposingByteArrayOutputStream)out;
1637         writeBytes(new ByteSequence(_out.getInternalBuffer(), 0, _out.size()));
1638       }
1639       catch (Throwable e) {
1640         DbConnection.handleError(e);
1641       }
1642     }
1643
1644     public void writeBytes(ByteSequence bytes) throws IOException {
1645       RefCountingStorage contentStorage = getContentStorage();
1646       w.lock();
1647       try {
1648         incModCount(myFileId);
1649
1650         checkFileIsValid(myFileId);
1651
1652         int page;
1653         final boolean fixedSize;
1654         if (weHaveContentHashes) {
1655           page = findOrCreateContentRecord(bytes.getBytes(), bytes.getOffset(), bytes.getLength());
1656
1657           incModCount(myFileId);
1658           checkFileIsValid(myFileId);
1659
1660           setContentRecordId(myFileId, page > 0 ? page : -page);
1661
1662           if (page > 0) return;
1663           page = -page;
1664           fixedSize = true;
1665         } else {
1666           page = getContentRecordId(myFileId);
1667           if (page == 0 || contentStorage.getRefCount(page) > 1) {
1668             page = contentStorage.acquireNewRecord();
1669             setContentRecordId(myFileId, page);
1670           }
1671           fixedSize = myFixedSize;
1672         }
1673
1674         if (useSnappyForCompression) {
1675           BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream();
1676           DataOutputStream outputStream = new DataOutputStream(out);
1677           byte[] rawBytes = bytes.getBytes();
1678           if (bytes.getOffset() != 0) {
1679             rawBytes = new byte[bytes.getLength()];
1680             System.arraycopy(bytes.getBytes(), bytes.getOffset(), rawBytes, 0, bytes.getLength());
1681           }
1682           CompressionUtil.writeCompressed(outputStream, rawBytes, bytes.getLength());
1683           outputStream.close();
1684           bytes = new ByteSequence(out.getInternalBuffer(), 0, out.size());
1685         }
1686         contentStorage.writeBytes(page, bytes, fixedSize);
1687       }
1688       finally {
1689         w.unlock();
1690       }
1691     }
1692   }
1693
1694   private static final boolean DO_HARD_CONSISTENCY_CHECK = false;
1695   private static final boolean DUMP_STATISTICS = weHaveContentHashes;  // TODO: remove once not needed
1696   private static long totalContents;
1697   private static long totalReuses;
1698   private static long time;
1699   private static int contents;
1700   private static int reuses;
1701
1702   private static int findOrCreateContentRecord(byte[] bytes, int offset, int length) throws IOException {
1703     assert weHaveContentHashes;
1704
1705     long started = DUMP_STATISTICS ? System.nanoTime():0;
1706     myDigest.reset();
1707     myDigest.update(String.valueOf(length - offset).getBytes(Charset.defaultCharset()));
1708     myDigest.update("\0".getBytes(Charset.defaultCharset()));
1709     myDigest.update(bytes, offset, length);
1710     byte[] digest = myDigest.digest();
1711     long done = DUMP_STATISTICS ? System.nanoTime() - started : 0;
1712     time += done;
1713
1714     ++contents;
1715     totalContents += length;
1716
1717     if (DUMP_STATISTICS && (contents & 0x3FFF) == 0) {
1718       LOG.info("Contents:" + contents + " of " + totalContents + ", reuses:" + reuses + " of " + totalReuses + " for " + time / 1000000);
1719     }
1720     PersistentBTreeEnumerator<byte[]> hashesEnumerator = getContentHashesEnumerator();
1721     final int largestId = hashesEnumerator.getLargestId();
1722     int page = hashesEnumerator.enumerate(digest);
1723
1724     if (page <= largestId) {
1725       ++reuses;
1726       getContentStorage().acquireRecord(page);
1727       totalReuses += length;
1728
1729       if (DO_HARD_CONSISTENCY_CHECK) {
1730         DataInputStream stream = doReadContentById(page);
1731         int i = offset;
1732         for(int c = 0; c < length; ++c) {
1733           if (stream.available() == 0) {
1734             assert false;
1735           }
1736           if (bytes[i++] != stream.readByte()) {
1737             assert false;
1738           }
1739         }
1740         if (stream.available() > 0) {
1741           assert false;
1742         }
1743       }
1744       return page;
1745     } else {
1746       int newRecord = getContentStorage().acquireNewRecord();
1747       if (page != newRecord) {
1748         assert false:"Unexpected content storage modification";
1749       }
1750       if (DO_HARD_CONSISTENCY_CHECK) {
1751         if (hashesEnumerator.enumerate(digest) != page) {
1752           assert false;
1753         }
1754
1755         byte[] bytes1 = hashesEnumerator.valueOf(page);
1756         if (!Arrays.equals(digest, bytes1)) {
1757           assert false;
1758         }
1759       }
1760       return -page;
1761     }
1762   }
1763
1764   private static class AttributeOutputStream extends DataOutputStream {
1765     private final FileAttribute myAttribute;
1766     private final int myFileId;
1767
1768     private AttributeOutputStream(final int fileId, @NotNull FileAttribute attribute) {
1769       super(new BufferExposingByteArrayOutputStream());
1770       myFileId = fileId;
1771       myAttribute = attribute;
1772     }
1773
1774     @Override
1775     public void close() throws IOException {
1776       super.close();
1777
1778       try {
1779         final BufferExposingByteArrayOutputStream _out = (BufferExposingByteArrayOutputStream)out;
1780
1781         if (inlineAttributes && _out.size() < MAX_SMALL_ATTR_SIZE) {
1782           w.lock();
1783           try {
1784             rewriteDirectoryRecordWithAttrContent(_out);
1785             incModCount(myFileId);
1786           }
1787           finally {
1788             w.unlock();
1789           }
1790         }
1791         else {
1792           w.lock();
1793           try {
1794             incModCount(myFileId);
1795             int page = findAttributePage(myFileId, myAttribute, true);
1796             if (inlineAttributes && page < 0) {
1797               rewriteDirectoryRecordWithAttrContent(new BufferExposingByteArrayOutputStream());
1798               page = findAttributePage(myFileId, myAttribute, true);
1799             }
1800
1801             if (bulkAttrReadSupport) {
1802               BufferExposingByteArrayOutputStream stream = new BufferExposingByteArrayOutputStream();
1803               out = stream;
1804               writeRecordHeader(DbConnection.getAttributeId(myAttribute.getId()), myFileId, this);
1805               write(_out.getInternalBuffer(), 0, _out.size());
1806               getAttributesStorage().writeBytes(page, new ByteSequence(stream.getInternalBuffer(), 0, stream.size()), myAttribute.isFixedSize());
1807             }
1808             else {
1809               getAttributesStorage().writeBytes(page, new ByteSequence(_out.getInternalBuffer(), 0, _out.size()), myAttribute.isFixedSize());
1810             }
1811           }
1812           finally {
1813             w.unlock();
1814           }
1815         }
1816       }
1817       catch (Throwable e) {
1818         DbConnection.handleError(e);
1819       }
1820     }
1821
1822     void rewriteDirectoryRecordWithAttrContent(BufferExposingByteArrayOutputStream _out) throws IOException {
1823       int recordId = getAttributeRecordId(myFileId);
1824       assert inlineAttributes;
1825       int encodedAttrId = DbConnection.getAttributeId(myAttribute.getId());
1826
1827       Storage storage = getAttributesStorage();
1828       BufferExposingByteArrayOutputStream unchangedPreviousDirectoryStream = null;
1829       boolean directoryRecord = false;
1830
1831
1832       if (recordId == 0) {
1833         recordId = storage.createNewRecord();
1834         setAttributeRecordId(myFileId, recordId);
1835         directoryRecord = true;
1836       }
1837       else {
1838         DataInputStream attrRefs = storage.readStream(recordId);
1839
1840         DataOutputStream dataStream = null;
1841
1842         try {
1843           final int remainingAtStart = attrRefs.available();
1844           if (bulkAttrReadSupport) {
1845             unchangedPreviousDirectoryStream = new BufferExposingByteArrayOutputStream();
1846             dataStream = new DataOutputStream(unchangedPreviousDirectoryStream);
1847             int attId = DataInputOutputUtil.readINT(attrRefs);
1848             assert attId == DbConnection.RESERVED_ATTR_ID;
1849             int fileId = DataInputOutputUtil.readINT(attrRefs);
1850             assert myFileId == fileId;
1851
1852             writeRecordHeader(attId, fileId, dataStream);
1853           }
1854           while (attrRefs.available() > 0) {
1855             final int attIdOnPage = DataInputOutputUtil.readINT(attrRefs);
1856             final int attrAddressOrSize = DataInputOutputUtil.readINT(attrRefs);
1857
1858             if (attIdOnPage != encodedAttrId) {
1859               if (dataStream == null) {
1860                 unchangedPreviousDirectoryStream = new BufferExposingByteArrayOutputStream();
1861                 dataStream = new DataOutputStream(unchangedPreviousDirectoryStream);
1862               }
1863               DataInputOutputUtil.writeINT(dataStream, attIdOnPage);
1864               DataInputOutputUtil.writeINT(dataStream, attrAddressOrSize);
1865
1866               if (attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1867                 byte[] b = new byte[attrAddressOrSize];
1868                 attrRefs.readFully(b);
1869                 dataStream.write(b);
1870               }
1871             }
1872             else {
1873               if (attrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
1874                 if (_out.size() == attrAddressOrSize) {
1875                   // update inplace when new attr has the same size
1876                   int remaining = attrRefs.available();
1877                   storage.replaceBytes(recordId, remainingAtStart - remaining, new ByteSequence(_out.getInternalBuffer(), 0, _out.size()));
1878                   return;
1879                 }
1880                 attrRefs.skipBytes(attrAddressOrSize);
1881               }
1882             }
1883           }
1884         }
1885         finally {
1886           attrRefs.close();
1887           if (dataStream != null) dataStream.close();
1888         }
1889       }
1890
1891       AbstractStorage.StorageDataOutput directoryStream = storage.writeStream(recordId);
1892       if (directoryRecord) {
1893         if (bulkAttrReadSupport) writeRecordHeader(DbConnection.RESERVED_ATTR_ID, myFileId, directoryStream);
1894       }
1895       if(unchangedPreviousDirectoryStream != null) {
1896         directoryStream.write(unchangedPreviousDirectoryStream.getInternalBuffer(), 0, unchangedPreviousDirectoryStream.size());
1897       }
1898       if (_out.size() > 0) {
1899         DataInputOutputUtil.writeINT(directoryStream, encodedAttrId);
1900         DataInputOutputUtil.writeINT(directoryStream, _out.size());
1901         directoryStream.write(_out.getInternalBuffer(), 0, _out.size());
1902       }
1903
1904       directoryStream.close();
1905     }
1906   }
1907
1908   public static void dispose() {
1909     w.lock();
1910     try {
1911       DbConnection.force();
1912       DbConnection.closeFiles();
1913     }
1914     catch (Throwable e) {
1915       DbConnection.handleError(e);
1916     }
1917     finally {
1918       ourIsDisposed = true;
1919       w.unlock();
1920     }
1921   }
1922
1923   public static void invalidateCaches() {
1924     DbConnection.createBrokenMarkerFile(null);
1925   }
1926
1927   static void checkSanity() {
1928     long t = System.currentTimeMillis();
1929
1930     r.lock();
1931     try {
1932       final int fileLength = length();
1933       assert fileLength % RECORD_SIZE == 0;
1934       int recordCount = fileLength / RECORD_SIZE;
1935
1936       IntArrayList usedAttributeRecordIds = new IntArrayList();
1937       IntArrayList validAttributeIds = new IntArrayList();
1938       for (int id = 2; id < recordCount; id++) {
1939         int flags = getFlags(id);
1940         LOG.assertTrue((flags & ~ALL_VALID_FLAGS) == 0, "Invalid flags: 0x" + Integer.toHexString(flags) + ", id: " + id);
1941         if (BitUtil.isSet(flags, FREE_RECORD_FLAG)) {
1942           LOG.assertTrue(DbConnection.myFreeRecords.contains(id), "Record, marked free, not in free list: " + id);
1943         }
1944         else {
1945           LOG.assertTrue(!DbConnection.myFreeRecords.contains(id), "Record, not marked free, in free list: " + id);
1946           checkRecordSanity(id, recordCount, usedAttributeRecordIds, validAttributeIds);
1947         }
1948       }
1949     }
1950     finally {
1951       r.unlock();
1952     }
1953
1954     t = System.currentTimeMillis() - t;
1955     LOG.info("Sanity check took " + t + " ms");
1956   }
1957
1958   private static void checkRecordSanity(final int id, final int recordCount, final IntArrayList usedAttributeRecordIds,
1959                                         final IntArrayList validAttributeIds) {
1960     int parentId = getParent(id);
1961     assert parentId >= 0 && parentId < recordCount;
1962     if (parentId > 0 && getParent(parentId) > 0) {
1963       int parentFlags = getFlags(parentId);
1964       assert !BitUtil.isSet(parentFlags, FREE_RECORD_FLAG) : parentId + ": " + Integer.toHexString(parentFlags);
1965       assert BitUtil.isSet(parentFlags, PersistentFS.IS_DIRECTORY_FLAG) : parentId + ": " + Integer.toHexString(parentFlags);
1966     }
1967
1968     String name = getName(id);
1969     LOG.assertTrue(parentId == 0 || !name.isEmpty(), "File with empty name found under " + getName(parentId) + ", id=" + id);
1970
1971     checkContentsStorageSanity(id);
1972     checkAttributesStorageSanity(id, usedAttributeRecordIds, validAttributeIds);
1973
1974     long length = getLength(id);
1975     assert length >= -1 : "Invalid file length found for " + name + ": " + length;
1976   }
1977
1978   private static void checkContentsStorageSanity(int id) {
1979     int recordId = getContentRecordId(id);
1980     assert recordId >= 0;
1981     if (recordId > 0) {
1982       getContentStorage().checkSanity(recordId);
1983     }
1984   }
1985
1986   private static void checkAttributesStorageSanity(int id, IntArrayList usedAttributeRecordIds, IntArrayList validAttributeIds) {
1987     int attributeRecordId = getAttributeRecordId(id);
1988
1989     assert attributeRecordId >= 0;
1990     if (attributeRecordId > 0) {
1991       try {
1992         checkAttributesSanity(attributeRecordId, usedAttributeRecordIds, validAttributeIds);
1993       }
1994       catch (IOException ex) {
1995         DbConnection.handleError(ex);
1996       }
1997     }
1998   }
1999
2000   private static void checkAttributesSanity(final int attributeRecordId, final IntArrayList usedAttributeRecordIds,
2001                                             final IntArrayList validAttributeIds) throws IOException {
2002     assert !usedAttributeRecordIds.contains(attributeRecordId);
2003     usedAttributeRecordIds.add(attributeRecordId);
2004
2005     try (DataInputStream dataInputStream = getAttributesStorage().readStream(attributeRecordId)) {
2006       if (bulkAttrReadSupport) skipRecordHeader(dataInputStream, 0, 0);
2007
2008       while (dataInputStream.available() > 0) {
2009         int attId = DataInputOutputUtil.readINT(dataInputStream);
2010
2011         if (!validAttributeIds.contains(attId)) {
2012           assert persistentAttributesList || !getNames().valueOf(attId).isEmpty();
2013           validAttributeIds.add(attId);
2014         }
2015
2016         int attDataRecordIdOrSize = DataInputOutputUtil.readINT(dataInputStream);
2017
2018         if (inlineAttributes) {
2019           if (attDataRecordIdOrSize < MAX_SMALL_ATTR_SIZE) {
2020             dataInputStream.skipBytes(attDataRecordIdOrSize);
2021             continue;
2022           }
2023           else attDataRecordIdOrSize -= MAX_SMALL_ATTR_SIZE;
2024         }
2025         assert !usedAttributeRecordIds.contains(attDataRecordIdOrSize);
2026         usedAttributeRecordIds.add(attDataRecordIdOrSize);
2027
2028         getAttributesStorage().checkSanity(attDataRecordIdOrSize);
2029       }
2030     }
2031   }
2032
2033   public static void handleError(Throwable e) throws RuntimeException, Error {
2034     DbConnection.handleError(e);
2035   }
2036
2037   /*
2038   public interface BulkAttrReadCallback {
2039     boolean accepts(int fileId);
2040     boolean execute(int fileId, DataInputStream is);
2041   }
2042
2043   // custom DataInput implementation instead of DataInputStream (without extra allocations) (api change)
2044   // store each attr in separate file: pro: read only affected data, easy versioning
2045
2046   public static void readAttributeInBulk(FileAttribute attr, BulkAttrReadCallback callback) throws IOException {
2047     String attrId = attr.getId();
2048     int encodedAttrId = DbConnection.getAttributeId(attrId);
2049     synchronized (attrId) {
2050       Storage storage = getAttributesStorage();
2051       RecordIterator recordIterator = storage.recordIterator();
2052       while (recordIterator.hasNextRecordId()) {
2053         int recordId = recordIterator.nextRecordId();
2054         DataInputStream stream = storage.readStream(recordId);
2055
2056         int currentAttrId = DataInputOutputUtil.readINT(stream);
2057         int fileId = DataInputOutputUtil.readINT(stream);
2058         if (!callback.accepts(fileId)) continue;
2059
2060         if (currentAttrId == DbConnection.RESERVED_ATTR_ID) {
2061           if (!inlineAttributes) continue;
2062
2063           while(stream.available() > 0) {
2064             int directoryAttrId = DataInputOutputUtil.readINT(stream);
2065             int directoryAttrAddressOrSize = DataInputOutputUtil.readINT(stream);
2066
2067             if (directoryAttrId != encodedAttrId) {
2068               if (directoryAttrAddressOrSize < MAX_SMALL_ATTR_SIZE) stream.skipBytes(directoryAttrAddressOrSize);
2069             } else {
2070               if (directoryAttrAddressOrSize < MAX_SMALL_ATTR_SIZE) {
2071                 byte[] b = new byte[directoryAttrAddressOrSize];
2072                 stream.readFully(b);
2073                 DataInputStream inlineAttrStream = new DataInputStream(new ByteArrayInputStream(b));
2074                 int version = DataInputOutputUtil.readINT(inlineAttrStream);
2075                 if (version != attr.getVersion()) continue;
2076                 boolean result = callback.execute(fileId, inlineAttrStream); // todo
2077                 if (!result) break;
2078               }
2079             }
2080           }
2081         } else if (currentAttrId == encodedAttrId) {
2082           int version = DataInputOutputUtil.readINT(stream);
2083           if (version != attr.getVersion()) continue;
2084
2085           boolean result = callback.execute(fileId, stream); // todo
2086           if (!result) break;
2087         }
2088       }
2089     }
2090   }*/
2091 }