Skip to content

Commit 3f9d15e

Browse files
authored
Merge pull request #6825 from streetcomplete/new-update-on-conflict-logic
fixes #6779, fixes #6419, fixes #6771, supercedes #5073
1 parent 41aa28f commit 3f9d15e

8 files changed

Lines changed: 63 additions & 43 deletions

File tree

app/src/androidUnitTest/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataControllerTest.kt

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -127,33 +127,17 @@ class MapDataControllerTest {
127127
}
128128

129129
@Test fun deleteOlderThan() {
130-
val nodeKeys = listOf(
130+
val elementKeys = listOf(
131131
ElementKey(NODE, 1L),
132132
ElementKey(NODE, 2L),
133-
ElementKey(NODE, 3L),
134-
)
135-
val filteredNodeKeys = listOf(
136-
ElementKey(NODE, 1L),
137-
ElementKey(NODE, 3L),
138-
)
139-
val wayKeys = listOf(
140-
ElementKey(ElementType.WAY, 1L),
141-
)
142-
val relationKeys = listOf(
143-
ElementKey(ElementType.RELATION, 1L),
144133
)
145-
val elementKeys = relationKeys + wayKeys + filteredNodeKeys
146-
on(nodeDB.getIdsOlderThan(123L)).thenReturn(nodeKeys.map { it.id })
147-
on(wayDB.getIdsOlderThan(123L)).thenReturn(wayKeys.map { it.id })
148-
on(relationDB.getIdsOlderThan(123L)).thenReturn(relationKeys.map { it.id })
149-
on(wayDB.filterNodeIdsWithoutWays(nodeKeys.map { it.id })).thenReturn(filteredNodeKeys.map { it.id })
134+
on(elementDB.getIdsOlderThan(123L)).thenReturn(elementKeys)
150135
val listener = mock<MapDataController.Listener>()
151136

152137
controller.addListener(listener)
153138
controller.deleteOlderThan(123L)
154139

155-
verify(elementDB).deleteAll(wayKeys + relationKeys)
156-
verify(elementDB).deleteAll(filteredNodeKeys)
140+
verify(elementDB).deleteAll(elementKeys)
157141
verify(geometryDB).deleteAll(elementKeys)
158142
verify(createdElementsController).deleteAll(elementKeys)
159143

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/Cleaner.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ class Cleaner(
3434
val time = nowAsEpochMilliseconds()
3535

3636
val oldDataTimestamp = nowAsEpochMilliseconds() - ApplicationConstants.DELETE_OLD_DATA_AFTER
37-
noteController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
38-
mapDataController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
37+
while (true) {
38+
val deleted = noteController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
39+
if (deleted < MAX_DELETE_ELEMENTS) break
40+
}
41+
while (true) {
42+
val deleted = mapDataController.deleteOlderThan(oldDataTimestamp, MAX_DELETE_ELEMENTS)
43+
if (deleted < MAX_DELETE_ELEMENTS) break
44+
}
3945
downloadedTilesController.deleteOlderThan(oldDataTimestamp)
4046
// do this after cleaning map data and notes, because some metadata rely on map data
4147
questTypeRegistry.forEach { it.deleteMetadataOlderThan(oldDataTimestamp) }

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditUploader.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import de.westnordost.streetcomplete.data.osm.mapdata.MapDataApiClient
1111
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataChanges
1212
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataController
1313
import de.westnordost.streetcomplete.data.osm.mapdata.MapDataUpdates
14+
import de.westnordost.streetcomplete.data.osm.mapdata.Node
1415
import de.westnordost.streetcomplete.data.osm.mapdata.RemoteMapDataRepository
16+
import de.westnordost.streetcomplete.data.osm.mapdata.Way
1517

1618
class ElementEditUploader(
1719
private val changesetManager: OpenChangesetsManager,
@@ -64,7 +66,7 @@ class ElementEditUploader(
6466
// remote in an incompatible way. So, we don't catch the exception but exit
6567
val remoteChanges = edit.action.createUpdates(RemoteMapDataRepository(mapDataApi), getIdProvider())
6668

67-
return try {
69+
val updates = try {
6870
uploadChanges(edit, remoteChanges, false)
6971
}
7072
// probably changeset was closed -> try again once with new changeset
@@ -75,6 +77,14 @@ class ElementEditUploader(
7577
catch (e: ChangesetTooLargeException) {
7678
uploadChanges(edit, remoteChanges, true)
7779
}
80+
81+
// We need to backfill any updates of ways which include nodes that we don't have in the
82+
// local database already with these nodes from remote. Otherwise, when new nodes have been
83+
// added to the updated way, we'd end up with incomplete ways.
84+
// We only need to do that in this method because when just uploading local changes without
85+
// conflict, there can't be any new elements referenced by the updated way except those we
86+
// added ourselves and thus already know.
87+
return backfillMapDataUpdates(updates)
7888
}
7989

8090
private suspend fun uploadChanges(
@@ -89,4 +99,31 @@ class ElementEditUploader(
8999
}
90100
return mapDataApi.uploadChanges(changesetId, changes, ApplicationConstants::ignoreRelation)
91101
}
102+
103+
/** Ensures that all nodes of all updated ways in [updates] are either already present in the
104+
* local database or otherwise then also included in the result. */
105+
private suspend fun backfillMapDataUpdates(updates: MapDataUpdates): MapDataUpdates {
106+
val nodeIdsOfUpdatedWays = updates.updated
107+
.filterIsInstance<Way>()
108+
.flatMapTo(HashSet()) { it.nodeIds }
109+
110+
val idsOfUpdatedNodes =
111+
updates.updated.filterIsInstance<Node>().mapTo(HashSet()) { it.id } +
112+
updates.idUpdates.map { it.newElementId }
113+
114+
val nodeIdsThatMustBePresentInLocalData = nodeIdsOfUpdatedWays - idsOfUpdatedNodes
115+
116+
val presentNodeIds = mapDataController
117+
.getNodes(nodeIdsThatMustBePresentInLocalData)
118+
.mapTo(HashSet()) { it.id }
119+
120+
val nodesThatMustBeFetchedFromRemote = nodeIdsThatMustBePresentInLocalData - presentNodeIds
121+
122+
return if (nodesThatMustBeFetchedFromRemote.isNotEmpty()) {
123+
val nodes = nodesThatMustBeFetchedFromRemote.mapNotNull { mapDataApi.getNode(it) }
124+
updates.copy(updated = updates.updated + nodes)
125+
} else {
126+
updates
127+
}
128+
}
92129
}

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/edits/upload/ElementEditsUploader.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineName
2121
import kotlinx.coroutines.CoroutineScope
2222
import kotlinx.coroutines.Dispatchers
2323
import kotlinx.coroutines.IO
24+
import kotlinx.coroutines.NonCancellable
2425
import kotlinx.coroutines.SupervisorJob
2526
import kotlinx.coroutines.sync.Mutex
2627
import kotlinx.coroutines.sync.withLock
@@ -46,7 +47,7 @@ class ElementEditsUploader(
4647
/* the sync of local change -> API and its response should not be cancellable because
4748
* otherwise an inconsistency in the data would occur. E.g. no "star" for an uploaded
4849
* change, a change could be uploaded twice etc */
49-
withContext(scope.coroutineContext) { uploadEdit(edit, getIdProvider) }
50+
withContext(NonCancellable) { uploadEdit(edit, getIdProvider) }
5051
}
5152
} }
5253

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/ElementDao.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ class ElementDao(
8585
wayDao.clear()
8686
nodeDao.clear()
8787
}
88+
89+
fun getIdsOlderThan(timestamp: Long, limit: Int? = null): List<ElementKey> {
90+
val result = mutableListOf<ElementKey>()
91+
// get relations first, then ways, then nodes because relations depend on ways depend on nodes.
92+
result.addAll(relationDao.getIdsOlderThan(timestamp, limit?.minus(result.size)).map { ElementKey(RELATION, it) })
93+
result.addAll(wayDao.getIdsOlderThan(timestamp, limit?.minus(result.size)).map { ElementKey(WAY, it) })
94+
result.addAll(nodeDao.getIdsOlderThan(timestamp, limit?.minus(result.size)).map { ElementKey(NODE, it) })
95+
return result
96+
}
8897
}
8998

9099
private fun Iterable<ElementKey>.filterByType(type: ElementType) =

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataApiClient.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class MapDataApiClient(
3333
*
3434
* @param changesetId id of the changeset to upload changes into
3535
* @param changes changes to upload.
36-
```suggestion
3736
* @param ignoreRelation omit any relations for which the given function returns true.
3837
* Such relations can still be referred to as relation members,
3938
* though, the relations themselves are just not included

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/MapDataController.kt

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,21 +241,11 @@ class MapDataController internal constructor(
241241
var elementCount = 0
242242
var geometryCount = 0
243243
lock.withLock {
244-
val relations = relationDB.getIdsOlderThan(timestamp, limit).map { ElementKey(ElementType.RELATION, it) }
245-
val ways = wayDB.getIdsOlderThan(timestamp, limit?.minus(relations.size)).map { ElementKey(ElementType.WAY, it) }
246-
247-
// delete now, so filterNodeIdsWithoutWays works as intended
248-
cache.update(deletedKeys = ways + relations)
249-
val wayAndRelationCount = elementDB.deleteAll(ways + relations)
250-
val nodes = nodeDB.getIdsOlderThan(timestamp, limit?.minus(relations.size + ways.size))
251-
// filter nodes to only delete nodes that are not part of a ways in the database
252-
val filteredNodes = wayDB.filterNodeIdsWithoutWays(nodes).map { ElementKey(ElementType.NODE, it) }
253-
254-
elements = relations + ways + filteredNodes
244+
elements = elementDB.getIdsOlderThan(timestamp, limit)
255245
if (elements.isEmpty()) return 0
256246

257-
cache.update(deletedKeys = filteredNodes)
258-
elementCount = wayAndRelationCount + elementDB.deleteAll(filteredNodes)
247+
cache.update(deletedKeys = elements)
248+
elementCount = elementDB.deleteAll(elements)
259249
geometryCount = geometryDB.deleteAll(elements)
260250
createdElementsController.deleteAll(elements)
261251
}

app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osm/mapdata/WayDao.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,4 @@ class WayDao(private val db: Database) {
124124
limit = limit
125125
) { it.getLong(ID) }
126126
}
127-
128-
fun filterNodeIdsWithoutWays(nodeIds: Collection<Long>): Collection<Long> {
129-
val idsString = nodeIds.joinToString(",")
130-
val nodeIdsWithWays = db.query(NAME_NODES, where = "$NODE_ID IN ($idsString)", columns = arrayOf(NODE_ID)) { c -> c.getLong(NODE_ID) }
131-
return nodeIds - nodeIdsWithWays.toHashSet()
132-
}
133127
}

0 commit comments

Comments
 (0)