• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bernardladenthin / BitcoinAddressFinder / #315

23 May 2025 03:19PM UTC coverage: 66.005% (-1.9%) from 67.925%
#315

push

bernardladenthin
Refactor OpenCL resource handling and improve consistency across API

- Introduced AutoCloseable for OpenCL resources (OpenClTask, OpenCLContext)
- Replaced manual release() methods with try-with-resources using close()
- Added isClosed() for defensive resource lifecycle checks
- Refactored buffer handling to separate host and device pointers
- Renamed createSecrets() param for clarity (overallWorkSize instead of batchSizeInBits)
- Updated all tests and affected logic to match new API and lifecycle
- Enhanced OpenCLGridResult to avoid unnecessary buffer duplication
- Improved benchmark documentation in README with CPU column
- Added optional LMDB in-memory caching in configuration

11 of 118 new or added lines in 10 files covered. (9.32%)

3 existing lines in 2 files now uncovered.

1231 of 1865 relevant lines covered (66.01%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.94
/src/main/java/net/ladenthin/bitcoinaddressfinder/persistence/lmdb/LMDBPersistence.java
1
// @formatter:off
2
/**
3
 * Copyright 2020 Bernard Ladenthin bernard.ladenthin@gmail.com
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *    http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
// @formatter:on
19
package net.ladenthin.bitcoinaddressfinder.persistence.lmdb;
20

21
import net.ladenthin.bitcoinaddressfinder.persistence.Persistence;
22
import net.ladenthin.bitcoinaddressfinder.persistence.PersistenceUtils;
23
import org.lmdbjava.CursorIterable;
24
import org.lmdbjava.Dbi;
25
import org.lmdbjava.Env;
26
import org.lmdbjava.EnvFlags;
27
import org.lmdbjava.KeyRange;
28
import org.lmdbjava.Txn;
29

30
import java.io.File;
31
import java.io.FileWriter;
32
import java.io.IOException;
33
import java.nio.ByteBuffer;
34
import java.util.HashSet;
35
import java.util.List;
36
import java.util.Map;
37
import java.util.Set;
38
import java.util.concurrent.atomic.AtomicBoolean;
39
import net.ladenthin.bitcoinaddressfinder.ByteBufferUtility;
40
import net.ladenthin.bitcoinaddressfinder.ByteConversion;
41
import net.ladenthin.bitcoinaddressfinder.KeyUtility;
42
import net.ladenthin.bitcoinaddressfinder.SeparatorFormat;
43
import net.ladenthin.bitcoinaddressfinder.configuration.CAddressFileOutputFormat;
44
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationReadOnly;
45
import net.ladenthin.bitcoinaddressfinder.configuration.CLMDBConfigurationWrite;
46
import org.apache.commons.codec.binary.Hex;
47
import org.bitcoinj.base.Coin;
48
import org.bitcoinj.base.LegacyAddress;
49
import org.lmdbjava.BufferProxy;
50
import org.lmdbjava.ByteBufferProxy;
51

52
import static org.lmdbjava.DbiFlags.MDB_CREATE;
53
import static org.lmdbjava.Env.create;
54
import org.lmdbjava.EnvInfo;
55
import org.slf4j.Logger;
56
import org.slf4j.LoggerFactory;
57

58
public class LMDBPersistence implements Persistence {
59

60
    private static final String DB_NAME_HASH160_TO_COINT = "hash160toCoin";
61
    private static final int DB_COUNT = 1;
62
    
63
    private final Logger logger = LoggerFactory.getLogger(LMDBPersistence.class);
1✔
64

65
    private final PersistenceUtils persistenceUtils;
66
    private final CLMDBConfigurationWrite lmdbConfigurationWrite;
67
    private final CLMDBConfigurationReadOnly lmdbConfigurationReadOnly;
68
    private final KeyUtility keyUtility;
69
    private Env<ByteBuffer> env;
70
    private Dbi<ByteBuffer> lmdb_h160ToAmount;
71
    private long increasedCounter = 0;
1✔
72
    private long increasedSum = 0;
1✔
73
    private Set<ByteBuffer> addressCache = null;
1✔
74

75

76
    public LMDBPersistence(CLMDBConfigurationWrite lmdbConfigurationWrite, PersistenceUtils persistenceUtils) {
1✔
77
        this.lmdbConfigurationReadOnly = null;
1✔
78
        this.lmdbConfigurationWrite = lmdbConfigurationWrite;
1✔
79
        this.persistenceUtils = persistenceUtils;
1✔
80
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
81
    }
1✔
82

83
    public LMDBPersistence(CLMDBConfigurationReadOnly lmdbConfigurationReadOnly, PersistenceUtils persistenceUtils) {
1✔
84
        this.lmdbConfigurationReadOnly = lmdbConfigurationReadOnly;
1✔
85
        lmdbConfigurationWrite = null;
1✔
86
        this.persistenceUtils = persistenceUtils;
1✔
87
        this.keyUtility = new KeyUtility(persistenceUtils.network, new ByteBufferUtility(true));
1✔
88
    }
1✔
89
    
90
    @Override
91
    public void init() {
92
        if (lmdbConfigurationWrite != null) {
1✔
93
            initWritable();
1✔
94
        } else if (lmdbConfigurationReadOnly != null) {
1✔
95
            initReadOnly();
1✔
96
        } else {
97
            throw new IllegalArgumentException("Neither write nor read-only configuration provided.");
×
98
        }
99
        
100
        
101
        logStatsIfConfigured(true);
1✔
102
    }
1✔
103
    
104
    public void loadAllAddressesToCache() {
NEW
105
        logger.info("##### BEGIN: loadAllAddressesToCache #####");
×
NEW
106
        Set<ByteBuffer> cache = new HashSet<>();
×
NEW
107
        try (Txn<ByteBuffer> txn = env.txnRead()) {
×
NEW
108
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
×
NEW
109
                for (CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
×
NEW
110
                    ByteBuffer key = ByteBuffer.allocate(kv.key().remaining());
×
NEW
111
                    key.put(kv.key()).flip();
×
NEW
112
                    cache.add(key);
×
NEW
113
                }
×
114
            }
115
        }
NEW
116
        addressCache = cache;
×
NEW
117
        logger.info("Loaded {} addresses into in-memory cache.", addressCache.size());
×
NEW
118
        logger.info("##### END: loadAllAddressesToCache #####");
×
NEW
119
    }
×
120

121
    public void unloadAddressCache() {
NEW
122
        addressCache = null;
×
NEW
123
    }
×
124
    
125
    private void initReadOnly() {
126
        BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationReadOnly.useProxyOptimal);
1✔
127
        env = create(bufferProxy).setMaxDbs(DB_COUNT).open(new File(lmdbConfigurationReadOnly.lmdbDirectory), EnvFlags.MDB_RDONLY_ENV, EnvFlags.MDB_NOLOCK);
1✔
128
        lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT);
1✔
129
        
130
        if (lmdbConfigurationReadOnly.loadToMemoryCacheOnInit) {
1✔
NEW
131
            loadAllAddressesToCache();
×
132
        }
133
    }
1✔
134

135
    private void initWritable() {
136
        // -Xmx10G -XX:MaxDirectMemorySize=5G
137
        // We always need an Env. An Env owns a physical on-disk storage file. One
138
        // Env can store many different databases (ie sorted maps).
139
        File lmdbDirectory = new File(lmdbConfigurationWrite.lmdbDirectory);
1✔
140
        lmdbDirectory.mkdirs();
1✔
141
        
142
        BufferProxy<ByteBuffer> bufferProxy = getBufferProxyByUseProxyOptimal(lmdbConfigurationWrite.useProxyOptimal);
1✔
143
        
144
        env = create(bufferProxy)
1✔
145
                // LMDB also needs to know how large our DB might be. Over-estimating is OK.
146
                .setMapSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.initialMapSizeInMiB))
1✔
147
                // LMDB also needs to know how many DBs (Dbi) we want to store in this Env.
148
                .setMaxDbs(DB_COUNT)
1✔
149
                // Now let's open the Env. The same path can be concurrently opened and
150
                // used in different processes, but do not open the same path twice in
151
                // the same process at the same time.
152
                
153
                //https://github.com/kentnl/CHI-Driver-LMDB
154
                .open(lmdbDirectory, EnvFlags.MDB_NOSYNC, EnvFlags.MDB_NOMETASYNC, EnvFlags.MDB_WRITEMAP, EnvFlags.MDB_MAPASYNC);
1✔
155
        // We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The
156
        // MDB_CREATE flag causes the DB to be created if it doesn't already exist.
157
        lmdb_h160ToAmount = env.openDbi(DB_NAME_HASH160_TO_COINT, MDB_CREATE);
1✔
158
    }
1✔
159

160
    /**
161
     * https://github.com/lmdbjava/lmdbjava/wiki/Buffers
162
     *
163
     * @param useProxyOptimal
164
     * @return
165
     */
166
    private BufferProxy<ByteBuffer> getBufferProxyByUseProxyOptimal(boolean useProxyOptimal) {
167
        if (useProxyOptimal) {
1✔
168
            return ByteBufferProxy.PROXY_OPTIMAL;
1✔
169
        } else {
170
            return ByteBufferProxy.PROXY_SAFE;
×
171
        }
172
    }
173
    
174
    private void logStatsIfConfigured(boolean onInit) {
175
        if (isLoggingEnabled(lmdbConfigurationWrite, onInit) || isLoggingEnabled(lmdbConfigurationReadOnly, onInit)) {
1✔
176
            logStats();
1✔
177
        }
178
    }
1✔
179

180
    private boolean isLoggingEnabled(CLMDBConfigurationReadOnly config, boolean onInit) {
181
        return config != null && (onInit ? config.logStatsOnInit : config.logStatsOnClose);
1✔
182
    }
183

184
    @Override
185
    public void close() {
186
        logStatsIfConfigured(false);
1✔
187
        lmdb_h160ToAmount.close();
1✔
188
        env.close();
1✔
189
    }
1✔
190
    
191
    @Override
192
    public boolean isClosed() {
193
        return env.isClosed();
1✔
194
    }
195

196
    @Override
197
    public Coin getAmount(ByteBuffer hash160) {
198
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
199
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
200
            return getCoinFromByteBuffer(byteBuffer);
1✔
201
        }
202
    }
203
    
204
    private Coin getCoinFromByteBuffer(ByteBuffer byteBuffer) {
205
        if (byteBuffer == null || byteBuffer.capacity() == 0) {
1✔
206
            return Coin.ZERO;
1✔
207
        }
208
        return Coin.valueOf(byteBuffer.getLong());
1✔
209
    }
210

211
    @Override
212
    public boolean containsAddress(ByteBuffer hash160) {
213
        /*
214
        if (sortedAddressCache != null) {
215
            byte[] key = new byte[hash160.remaining()];
216
            hash160.get(key);
217
            hash160.rewind(); // falls der Buffer erneut verwendet wird
218

219
            return Arrays.binarySearch(sortedAddressCache, key, Arrays::compare) >= 0;
220
        }
221
        */
222
        
223
        if (lmdbConfigurationReadOnly.disableAddressLookup) {
1✔
NEW
224
            return false;
×
225
        }
226
        
227
        if (addressCache != null) {
1✔
NEW
228
            return addressCache.contains(hash160);
×
229
        }
230
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
231
            ByteBuffer byteBuffer = lmdb_h160ToAmount.get(txn, hash160);
1✔
232
            return byteBuffer != null;
1✔
233
        }
234
    }
235

236
    @Override
237
    public void writeAllAmountsToAddressFile(File file, CAddressFileOutputFormat addressFileOutputFormat, AtomicBoolean shouldRun) throws IOException {
238
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
239
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
240
                try (FileWriter writer = new FileWriter(file)) {
1✔
241
                    for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
242
                        if (!shouldRun.get()) {
1✔
243
                            return;
×
244
                        }
245
                        ByteBuffer addressAsByteBuffer = kv.key();
1✔
246
                        if(logger.isTraceEnabled()) {
1✔
247
                            String hexFromByteBuffer = new ByteBufferUtility(false).getHexFromByteBuffer(addressAsByteBuffer);
×
248
                            logger.trace("Process address: " + hexFromByteBuffer);
×
249
                        }
250
                        LegacyAddress address = keyUtility.byteBufferToAddress(addressAsByteBuffer);
1✔
251
                        final String line;
252
                        switch(addressFileOutputFormat) {
1✔
253
                            case HexHash:
254
                                line = Hex.encodeHexString(address.getHash()) + System.lineSeparator();
1✔
255
                                break;
1✔
256
                            case FixedWidthBase58BitcoinAddress:
257
                                line = String.format("%-34s", address.toBase58()) + System.lineSeparator();
1✔
258
                                break;
1✔
259
                            case DynamicWidthBase58BitcoinAddressWithAmount:
260
                                ByteBuffer value = kv.val();
1✔
261
                                Coin coin = getCoinFromByteBuffer(value);
1✔
262
                                line = address.toBase58() + SeparatorFormat.COMMA.getSymbol() + coin.getValue() + System.lineSeparator();
1✔
263
                                break;
1✔
264
                            default:
265
                                throw new IllegalArgumentException("Unknown addressFileOutputFormat: " + addressFileOutputFormat);
×
266
                        }
267
                        writer.write(line);
1✔
268
                    }
1✔
269
                }
×
270
            }
×
271
        }
×
272
    }
1✔
273

274
    @Override
275
    public void putAllAmounts(Map<ByteBuffer, Coin> amounts) throws IOException {
276
        for (Map.Entry<ByteBuffer, Coin> entry : amounts.entrySet()) {
×
277
            ByteBuffer hash160 = entry.getKey();
×
278
            Coin coin = entry.getValue();
×
279
            putNewAmount(hash160, coin);
×
280
        }
×
281
    }
×
282

283
    @Override
284
    public void changeAmount(ByteBuffer hash160, Coin amountToChange) {
285
        Coin valueInDB = getAmount(hash160);
×
286
        Coin toWrite = valueInDB.add(amountToChange);
×
287
        putNewAmount(hash160, toWrite);
×
288
    }
×
289

290
    @Override
291
    public void putNewAmount(ByteBuffer hash160, Coin amount) {
292
        putNewAmountWithAutoIncrease(hash160, amount);
1✔
293
    }
1✔
294
    
295
    /**
296
     * If an {@link org.lmdbjava.Env.MapFullException} was thrown during a put. The map might be increased if configured.
297
     * The increase value needs to be high enough. Otherwise the next put fails nevertheless.
298
     */
299
    private void putNewAmountWithAutoIncrease(ByteBuffer hash160, Coin amount) {
300
        try {
301
            putNewAmountUnsafe(hash160, amount);
1✔
302
        } catch (org.lmdbjava.Env.MapFullException e) {
1✔
303
            if (lmdbConfigurationWrite.increaseMapAutomatically == true) {
1✔
304
                increaseDatabaseSize(new ByteConversion().mibToBytes(lmdbConfigurationWrite.increaseSizeInMiB));
1✔
305
                /**
306
                 * It is possible that the exception will be thrown again, in this case increaseSizeInMiB should be changed and it's a configuration issue.
307
                 * See {@link CLMDBConfigurationWrite#increaseSizeInMiB}.
308
                 */
309
                putNewAmountUnsafe(hash160, amount);
1✔
310
            } else {
311
                throw e;
1✔
312
            }
313
        }
1✔
314
    }
1✔
315
    
316
    private void putNewAmountUnsafe(ByteBuffer hash160, Coin amount) {
317
        try (Txn<ByteBuffer> txn = env.txnWrite()) {
1✔
318
            if (lmdbConfigurationWrite.deleteEmptyAddresses && amount.isZero()) {
1✔
319
                lmdb_h160ToAmount.delete(txn, hash160);
×
320
            } else {
321
                long amountAsLong = amount.longValue();
1✔
322
                if (lmdbConfigurationWrite.useStaticAmount) {
1✔
323
                    amountAsLong = lmdbConfigurationWrite.staticAmount;
1✔
324
                }
325
                lmdb_h160ToAmount.put(txn, hash160, persistenceUtils.longToByteBufferDirect(amountAsLong));
1✔
326
            }
327
            txn.commit();
1✔
328
        }
329
    }
1✔
330

331
    @Override
332
    public Coin getAllAmountsFromAddresses(List<ByteBuffer> hash160s) {
333
        Coin allAmounts = Coin.ZERO;
×
334
        for (ByteBuffer hash160 : hash160s) {
×
335
            allAmounts = allAmounts.add(getAmount(hash160));
×
336
        }
×
337
        return allAmounts;
×
338
    }
339

340
    @Override
341
    public long count() {
342
        long count = 0;
1✔
343
        try (Txn<ByteBuffer> txn = env.txnRead()) {
1✔
344
            try (CursorIterable<ByteBuffer> iterable = lmdb_h160ToAmount.iterate(txn, KeyRange.all())) {
1✔
345
                for (final CursorIterable.KeyVal<ByteBuffer> kv : iterable) {
1✔
346
                    count++;
1✔
347
                }
1✔
348
            }
349
        }
350
        return count;
1✔
351
    }
352

353
    @Override
354
    public long getDatabaseSize() {
355
        EnvInfo info = env.info();
1✔
356
        return info.mapSize;
1✔
357
    }
358

359
    @Override
360
    public void increaseDatabaseSize(long toIncrease) {
361
        increasedCounter++;
1✔
362
        increasedSum += toIncrease;
1✔
363
        long newSize = getDatabaseSize() + toIncrease;
1✔
364
        env.setMapSize(newSize);
1✔
365
    }
1✔
366

367
    @Override
368
    public long getIncreasedCounter() {
369
        return increasedCounter;
1✔
370
    }
371

372
    @Override
373
    public long getIncreasedSum() {
374
        return increasedSum;
1✔
375
    }
376
    
377
    @Override
378
    public void logStats() {
379
        logger.info("##### BEGIN: LMDB stats #####");
1✔
380
        logger.info("... this may take a lot of time ...");
1✔
381
        logger.info("DatabaseSize: " + new ByteConversion().bytesToMib(getDatabaseSize()) + " MiB");
1✔
382
        logger.info("IncreasedCounter: " + getIncreasedCounter());
1✔
383
        logger.info("IncreasedSum: " + new ByteConversion().bytesToMib(getIncreasedSum()) + " MiB");
1✔
384
        logger.info("Stat: " + env.stat());
1✔
385
        // Attention: slow!
386
        long count = count();
1✔
387
        logger.info("LMDB contains " + count + " unique entries.");
1✔
388
        logger.info("##### END: LMDB stats #####");
1✔
389
    }
1✔
390
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc