Skip to content

Commit b62d20b

Browse files
borrrdenpasin
andauthored
Merge release/4.1 into master (#502)
* Fix the tests to pass with the addition of crash logging (#493) * CBL-8402 : Fix data race on SerialExecutor.currentThread (#495) Make currentThread an AtomicLong so isInsideExecutor() reads a consistent, visible value across pool threads. Clear it with compareAndSet in the finally block so a task that has already claimed the thread for the next run is not cleared. * CBL-8389 : Fix build and static analysis issues in kt-serialization API changes (#498) * Replace var with explicit FLDict type in Document.setContent (common is compiled at Java 8) * Suppress PMD SingularField and SpotBugs URF_UNREAD_FIELD on Document.extraBackingStore (keep-alive field) * Add null check on FLValue.fromData() result in Document.setContent (NP_NULL_ON_SOME_PATH) * Move serialization tests from test/java to test/kotlin so that only the android-ktx test builds compile them : - FleeceSerializationTest.kt moved as-is - ResultTest.kt renamed to ResultSerializationTest.kt - Serialization test and TestModel extracted from CollectionTest.kt into new CollectionSerializationTest.kt * Fix incoming BLE L2CAP socket double-free on connection teardown (#499) btAttached() (incoming) now calls c4socket_retain, matching btOpen(), to balance the c4socket_release in NativeC4Socket_closed. Without it the incoming socket was double-freed on teardown (SIGABRT, invalid refCount -6666666). --------- Co-authored-by: Pasin Suriyentrakorn <pasin@couchbase.com>
1 parent 6a8b38d commit b62d20b

8 files changed

Lines changed: 110 additions & 63 deletions

File tree

common/main/cpp/native_c4btsocketfactory.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ static void btOpen(C4Socket* socket,
7979
jint envState = attachJVM(&env, "btOpen");
8080
if (envState != JNI_OK && envState != JNI_EDETACHED) return;
8181

82+
// Balanced by the c4socket_release in NativeC4Socket_closed.
8283
c4socket_retain(socket);
8384

8485
// addr->hostname carries the CBL peer-ID / BT MAC address as a C4Slice.
@@ -178,6 +179,9 @@ static void btAttached(C4Socket* socket) {
178179

179180
jstring jPeerID = toJString(env, ctx->peerID);
180181

182+
// Balanced by the c4socket_release in NativeC4Socket_closed. Mirrors btOpen.
183+
c4socket_retain(socket);
184+
181185
env->CallStaticVoidMethod(
182186
cls_C4BTSocketFactory, m_attached,
183187
(jlong) socket,

common/main/java/com/couchbase/lite/Document.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ static Document getDocumentWithRevisions(@NonNull Collection collection, @NonNul
136136

137137
// Set by setData(FLSliceResult,boolean) to keep the Fleece backing store from being GC'd.
138138
// (This is kind of a hack, and it's only used ephemerally by Kotlin serialization.)
139+
@SuppressFBWarnings("URF_UNREAD_FIELD")
140+
@SuppressWarnings("PMD.SingularField")
139141
@GuardedBy("lock")
140142
@Nullable
141143
private FLSliceResult extraBackingStore;
@@ -632,9 +634,10 @@ private void setC4Document(@Nullable C4Document c4doc, boolean mutable) {
632634
// for use by CollectionExtensions.kt
633635
void setContent(@NonNull FLSliceResult fleeceData, boolean mutable) {
634636
synchronized (lock) {
635-
var data = FLValue.fromData(fleeceData).asFLDict();
637+
final FLValue body = FLValue.fromData(fleeceData);
638+
if (body == null) { throw new CouchbaseLiteError("Failed parsing fleece data"); }
636639
extraBackingStore = fleeceData;
637-
setContentLocked(data, mutable);
640+
setContentLocked(body.asFLDict(), mutable);
638641
}
639642
}
640643

common/main/java/com/couchbase/lite/internal/exec/SerialExecutor.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.concurrent.RejectedExecutionException;
2727
import java.util.concurrent.ThreadPoolExecutor;
2828
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.atomic.AtomicLong;
2930

3031
import com.couchbase.lite.LogDomain;
3132
import com.couchbase.lite.internal.logging.Log;
@@ -55,7 +56,9 @@ class SerialExecutor implements ExecutionService.CloseableExecutor {
5556
@Nullable
5657
private CountDownLatch stopLatch;
5758

58-
private long currentThread = -1;
59+
// Id of the pool thread currently running a task for this executor
60+
@NonNull
61+
private final AtomicLong currentThread = new AtomicLong(-1);
5962

6063
SerialExecutor(@NonNull ThreadPoolExecutor executor) {
6164
Preconditions.assertNotNull(executor, "executor");
@@ -120,7 +123,7 @@ public boolean stop(long timeout, @NonNull TimeUnit unit) {
120123

121124
@Override
122125
public boolean isInsideExecutor() {
123-
return Thread.currentThread().getId() == currentThread;
126+
return Thread.currentThread().getId() == currentThread.get();
124127
}
125128

126129
@NonNull
@@ -164,9 +167,13 @@ private void executeTask(@Nullable InstrumentedTask prevTask) {
164167
if (nextTask == null) { return; }
165168

166169
try { executor.execute(() -> {
167-
currentThread = Thread.currentThread().getId();
170+
final long tid = Thread.currentThread().getId();
171+
currentThread.set(tid);
168172
try { nextTask.run(); }
169-
finally { currentThread = -1; }
173+
finally {
174+
// Clear only if unchanged: the next task may already have claimed it.
175+
currentThread.compareAndSet(tid, -1);
176+
}
170177
}); }
171178
catch (RuntimeException e) {
172179
Log.w(LogDomain.DATABASE, "Catastrophic executor failure (Serial Executor)!", e);

common/test/java/com/couchbase/lite/CollectionTest.kt

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
package com.couchbase.lite
1818

1919
import com.couchbase.lite.internal.utils.SlowTest
20-
import kotlinx.serialization.Serializable
21-
import kotlinx.serialization.Transient
2220
import org.junit.Assert
2321
import org.junit.Test
2422

@@ -1215,52 +1213,4 @@ class CollectionTest : BaseDbTest() {
12151213

12161214
Assert.assertEquals(scope, testDatabase.getScope(Scope.DEFAULT_NAME))
12171215
}
1218-
1219-
//---------------------------------------------
1220-
// Model-based (serialization) API
1221-
//---------------------------------------------
1222-
1223-
@Test
1224-
fun saveNewDocInCollectionFromModel() {
1225-
val id = getUniqueName("test_doc")
1226-
1227-
// Create a new model and save it:
1228-
val model = TestModel("Nigel", 12)
1229-
testCollection.save(model, id)
1230-
Assert.assertEquals(testCollection, model.documentMeta?.collection)
1231-
Assert.assertEquals(id, model.documentMeta?.id)
1232-
1233-
// Read it back:
1234-
Assert.assertEquals(1, testCollection.count)
1235-
val gotModel = testCollection.getDocumentAs<TestModel>(id)!!
1236-
Assert.assertEquals(model, gotModel)
1237-
1238-
// Modify and save again:
1239-
gotModel.favorites = listOf("XTC", "Elvis Costello")
1240-
Assert.assertTrue(testCollection.save(gotModel))
1241-
Assert.assertNotEquals(model.documentMeta?.revisionID, gotModel.documentMeta?.revisionID)
1242-
1243-
// Get it as a regular Document and verify the contents:
1244-
val doc = testCollection.getDocument(id)!!
1245-
Assert.assertEquals(gotModel.documentMeta?.revisionID, doc.revisionID)
1246-
Assert.assertEquals("Nigel", doc.getString("name"))
1247-
Assert.assertEquals(12, doc.getInt("age"))
1248-
val faves = doc.getArray("favorites")!!
1249-
Assert.assertEquals(2, faves.count())
1250-
Assert.assertEquals("XTC", faves.getString(0))
1251-
Assert.assertEquals("Elvis Costello", faves.getString(1))
1252-
1253-
// Delete the model. `model` is out of date, so it will fail, but `gotModel` is OK:
1254-
Assert.assertFalse(testCollection.delete(model, ConcurrencyControl.FAIL_ON_CONFLICT))
1255-
Assert.assertTrue(testCollection.delete(gotModel, ConcurrencyControl.FAIL_ON_CONFLICT))
1256-
}
1257-
}
1258-
1259-
1260-
// Simple Model class for tests
1261-
@Serializable
1262-
data class TestModel(var name: String,
1263-
var age: Int,
1264-
var favorites: List<String>? = null): DocumentModel {
1265-
@Transient override var documentMeta: DocumentMeta? = null
12661216
}

common/test/java/com/couchbase/lite/logging/LogTest.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ class LogTest : BaseDbTest() {
292292

293293
val rex = Regex("cbl_(debug|verbose|info|warning|error)_\\d+\\.cbllog")
294294
for (file in files) {
295-
Assert.assertTrue(file.name.matches(rex))
295+
Assert.assertTrue(file.name.matches(rex) || file.name.contains("crash"))
296296
}
297297
}
298298
}
@@ -347,8 +347,9 @@ class LogTest : BaseDbTest() {
347347
) {
348348
// This should create two files for each of the 5 levels except verbose (debug, info, warning, error):
349349
// 1k of logs plus .5k headers. There should be only one file at the verbose level (just the headers)
350+
// and the crash log file
350351
write1KBToLog()
351-
Assert.assertEquals((4 * 2) + 1, logFiles.size)
352+
Assert.assertEquals((4 * 2) + 2, logFiles.size)
352353
}
353354
}
354355

@@ -364,11 +365,11 @@ class LogTest : BaseDbTest() {
364365
) {
365366
// This should create several files for each of the 5 levels except verbose (debug, info, warning, error):
366367
// 1k of logs plus .5k headers. Although lots of files are created they should be trimmed to only 3
367-
// at each level. There should be only one file at the verbose level (just the headers)
368+
// at each level. There should be only one file at the verbose level (just the headers) and the crash log file
368369
repeat(21) {
369370
write1KBToLog()
370371
}
371-
Assert.assertEquals((4 * 3) + 1, logFiles.size)
372+
Assert.assertEquals((4 * 3) + 2, logFiles.size)
372373
}
373374
}
374375

@@ -403,6 +404,10 @@ class LogTest : BaseDbTest() {
403404
Assert.assertNotNull(tempDir!!.listFiles())
404405
for (log in logFiles!!) {
405406
val fn = log.name.lowercase(Locale.getDefault())
407+
if (fn.contains("crash")) {
408+
continue;
409+
}
410+
406411
if (fn.startsWith("cbl_debug_") || fn.startsWith("cbl_verbose_")) {
407412
Assert.assertFalse(getLogContents(log).contains(uuidString))
408413
} else {
@@ -420,6 +425,10 @@ class LogTest : BaseDbTest() {
420425
) {
421426
write1KBToLog()
422427
for (log in logFiles) {
428+
if (log.name.contains("crash")) {
429+
continue;
430+
}
431+
423432
var logLine: String
424433
BufferedReader(FileReader(log)).use {
425434
logLine = it.readLine()
@@ -477,7 +486,7 @@ class LogTest : BaseDbTest() {
477486
Log.e(LogDomain.DATABASE, message, error)
478487

479488
for (log in logFiles) {
480-
if (!log.name.contains("verbose")) {
489+
if (!log.name.contains("verbose") && !log.name.contains("crash")) {
481490
Assert.assertTrue(getLogContents(log).contains(uuid))
482491
}
483492
}
@@ -498,7 +507,7 @@ class LogTest : BaseDbTest() {
498507
Log.e(LogDomain.DATABASE, message, error, uuid2)
499508

500509
for (log in logFiles) {
501-
if (!log.name.contains("verbose")) {
510+
if (!log.name.contains("verbose") && !log.name.contains("crash")) {
502511
val content = getLogContents(log)
503512
Assert.assertTrue(content.contains(uuid1))
504513
Assert.assertTrue(content.contains(uuid2))
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// Copyright (c) 2026 Couchbase, Inc.
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+
17+
package com.couchbase.lite
18+
19+
import kotlinx.serialization.Serializable
20+
import kotlinx.serialization.Transient
21+
import org.junit.Assert
22+
import org.junit.Test
23+
24+
25+
/** Tests serialization-based Collection accessors implemented in CollectionExtensions.kt. */
26+
class CollectionSerializationTest : BaseDbTest() {
27+
//---------------------------------------------
28+
// Model-based (serialization) API
29+
//---------------------------------------------
30+
31+
@Test
32+
fun saveNewDocInCollectionFromModel() {
33+
val id = getUniqueName("test_doc")
34+
35+
// Create a new model and save it:
36+
val model = TestModel("Nigel", 12)
37+
testCollection.save(model, id)
38+
Assert.assertEquals(testCollection, model.documentMeta?.collection)
39+
Assert.assertEquals(id, model.documentMeta?.id)
40+
41+
// Read it back:
42+
Assert.assertEquals(1, testCollection.count)
43+
val gotModel = testCollection.getDocumentAs<TestModel>(id)!!
44+
Assert.assertEquals(model, gotModel)
45+
46+
// Modify and save again:
47+
gotModel.favorites = listOf("XTC", "Elvis Costello")
48+
Assert.assertTrue(testCollection.save(gotModel))
49+
Assert.assertNotEquals(model.documentMeta?.revisionID, gotModel.documentMeta?.revisionID)
50+
51+
// Get it as a regular Document and verify the contents:
52+
val doc = testCollection.getDocument(id)!!
53+
Assert.assertEquals(gotModel.documentMeta?.revisionID, doc.revisionID)
54+
Assert.assertEquals("Nigel", doc.getString("name"))
55+
Assert.assertEquals(12, doc.getInt("age"))
56+
val faves = doc.getArray("favorites")!!
57+
Assert.assertEquals(2, faves.count())
58+
Assert.assertEquals("XTC", faves.getString(0))
59+
Assert.assertEquals("Elvis Costello", faves.getString(1))
60+
61+
// Delete the model. `model` is out of date, so it will fail, but `gotModel` is OK:
62+
Assert.assertFalse(testCollection.delete(model, ConcurrencyControl.FAIL_ON_CONFLICT))
63+
Assert.assertTrue(testCollection.delete(gotModel, ConcurrencyControl.FAIL_ON_CONFLICT))
64+
}
65+
}
66+
67+
68+
// Simple Model class for tests
69+
@Serializable
70+
data class TestModel(var name: String,
71+
var age: Int,
72+
var favorites: List<String>? = null): DocumentModel {
73+
@Transient override var documentMeta: DocumentMeta? = null
74+
}

common/test/java/com/couchbase/lite/ResultTest.kt renamed to common/test/kotlin/com/couchbase/lite/ResultSerializationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,4 @@ class ResultSerializationTest : BaseQueryTest() {
6767

6868
assertFalse(iter.hasNext())
6969
}
70-
}
70+
}

common/test/java/com/couchbase/lite/internal/fleece/FleeceSerializationTest.kt renamed to common/test/kotlin/com/couchbase/lite/internal/fleece/FleeceSerializationTest.kt

File renamed without changes.

0 commit comments

Comments
 (0)