Skip to content

Commit 05d78dc

Browse files
authored
Merge pull request #87 from swisscom/feature/45607-change-error-dir-behaviour
Feature/45607 change error dir behaviour * Don't create subdirectories in the error folder. * Ensure that error-file names are not getting too long. * Only move BAD_REQUEST responses to the error folder, other 4xx responses only get renamed and are retried directly or after the client is restarted. * Only allow absolute paths to be used for source/target/archive/error folders.
2 parents cd1d549 + cc170e8 commit 05d78dc

24 files changed

Lines changed: 789 additions & 222 deletions

File tree

cdr-client-common/src/main/kotlin/com/swisscom/health/des/cdr/client/common/Constants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ object Constants {
99
val SHUTDOWN_DELAY: Duration = Duration.ofMillis(SHUTDOWN_DELAY_MILLIS)
1010

1111
const val EMPTY_STRING: String = ""
12+
const val ERROR_DIR_NAME: String = "error"
13+
const val RESTART_FILE_EXTENSION: String = "clientRestartRequired"
1214

1315
const val CONFIG_CHANGE_EXIT_CODE = 29
1416
const val UNKNOWN_EXIT_CODE = 31

cdr-client-common/src/main/kotlin/com/swisscom/health/des/cdr/client/common/DTOs.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ class DTOs {
122122
FILE_BUSY_TEST_TIMEOUT_TOO_LONG,
123123
NO_CONNECTOR_CONFIGURED,
124124
VALUE_IS_PLACEHOLDER,
125-
CREDENTIAL_VALIDATION_FAILED
125+
CREDENTIAL_VALIDATION_FAILED,
126+
ERROR_AS_NON_ERROR_FOLDER_NAME_USED,
127+
ERROR_DIR_OVERLAPS_NON_ERROR_DIR,
128+
DIRECTORY_NEEDS_ABSOLUTE_PATH
126129
}
127130

128131

cdr-client-service/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ dockerCompose {
252252
dockerComposeWorkingDirectory.set(rootProject.file("docker-compose"))
253253
dockerComposeStopTimeout.set(Duration.ofSeconds(5)) // time before docker-compose sends SIGTERM to the running containers after the composeDown task has been started
254254
ignorePullFailure.set(true)
255+
val rancherDocker = file("${System.getProperty("user.home")}/.rd/bin/docker")
256+
if (rancherDocker.exists()) {
257+
dockerExecutable.set(rancherDocker.absolutePath)
258+
}
255259
isRequiredBy(tasks.getByName("integrationTest"))
256260
}
257261
/***************************

cdr-client-service/src/main/kotlin/com/swisscom/health/des/cdr/client/config/CdrClientConfig.kt

Lines changed: 24 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.swisscom.health.des.cdr.client.config
33
import com.fasterxml.jackson.annotation.JsonIgnore
44
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
55
import com.swisscom.health.des.cdr.client.common.Constants.EMPTY_STRING
6+
import com.swisscom.health.des.cdr.client.common.Constants.ERROR_DIR_NAME
67
import com.swisscom.health.des.cdr.client.config.CdrClientConfig.Mode
78
import com.swisscom.health.des.cdr.client.xml.DocumentType
89
import io.github.oshai.kotlinlogging.KotlinLogging
@@ -17,7 +18,6 @@ import java.time.LocalDate
1718
import java.time.format.DateTimeFormatter
1819
import java.time.temporal.ChronoUnit
1920
import kotlin.io.path.createDirectories
20-
import kotlin.io.path.isDirectory
2121

2222

2323
private val logger = KotlinLogging.logger {}
@@ -152,13 +152,8 @@ internal data class Connector(
152152
val sourceArchiveEnabled: Boolean = false,
153153

154154
/**
155-
* Directory to archive uploaded files to. If you specify a relative path, it will be resolved relative to the source directory.
156-
* If you specify an absolute path, the path will be used as is for all archive directories (see [docTypeFolders]).
157-
*
158-
* Beware: On Linux empty string, `.`, and `./` all resolve to the current working directory, while `./archive` (and just `archive`) resolve
159-
* to `<source_dir>/archive`.
160-
*
161-
* Default is the system temp directory.
155+
* Directory to archive uploaded files to.
156+
* Needs to be an absolute path, the path will be used as is for all archive directories (see [docTypeFolders]).
162157
*
163158
* @see sourceArchiveEnabled
164159
* @see getEffectiveSourceArchiveFolder
@@ -167,13 +162,8 @@ internal data class Connector(
167162
val sourceArchiveFolder: Path? = null,
168163

169164
/**
170-
* Directory to move documents to for which the upload has failed. If you specify a relative path, it will be resolved relative
171-
* to the source directory. If you specify an absolute path, the path will be used as is for all error directories, such as for all [docTypeFolders].
172-
*
173-
* Beware: On Linux empty string, `.`, and `./` all resolve to the current working directory, while `./archive` (and just `archive`) resolve
174-
* to `<source_dir>/archive`.
175-
*
176-
* Default is the system temp directory.
165+
* Directory to move documents to for which the upload has failed.
166+
* Needs to be an absolute path, the path will be used as is for all error directories, such as for all [docTypeFolders].
177167
*
178168
* @see getEffectiveSourceErrorFolder
179169
* @see sourceFolder
@@ -199,46 +189,28 @@ internal data class Connector(
199189

200190
companion object {
201191
const val PROPERTY_NAME = ""
202-
203-
@JvmStatic
204-
val TEMP_DIR_PATH: Path = Path.of(System.getProperty("java.io.tmpdir"))
205192
}
206193

207194
/**
208-
* If [sourceArchiveEnabled] is set to `true` returns the archive directory resolved against the source directory with a subdirectory
209-
* for the current date. The directories will be created if they do not exist. If [sourceArchiveEnabled] is `false` returns an
210-
* empty path.
195+
* If [sourceArchiveEnabled] is set to `true` returns the archive directory with a subdirectory for the current date.
196+
* The directory will be created if it does not exist.
197+
* If [sourceArchiveEnabled] is `false` returns an empty path.
211198
*
212199
* @see sourceArchiveEnabled
213200
* @see sourceArchiveFolder
214-
* @see sourceFolder
215201
*/
216202
@JsonIgnore
217-
fun getEffectiveSourceArchiveFolder(path: Path): Path? =
203+
fun getEffectiveSourceArchiveFolder(): Path? =
218204
if (sourceArchiveEnabled) {
219-
if (path.isDirectory()) {
220-
path
221-
} else {
222-
path.parent
223-
}.resolve((sourceArchiveFolder ?: TEMP_DIR_PATH).resolve(getDateNow()))
224-
.also { createDirectoryIfMissing(it) }
205+
createDirectoryIfMissing(when (sourceArchiveFolder) {
206+
null -> sourceFolder.resolve("archive")
207+
sourceFolder -> sourceFolder.resolve("archive")
208+
else -> sourceArchiveFolder
209+
}.resolve(getDateNow()))
225210
} else {
226211
null
227212
}
228213

229-
/**
230-
* Convenience property to get the connector archive directory that is used in all cases where no message type related directories are defined.
231-
* @see getEffectiveSourceArchiveFolder
232-
*/
233-
val effectiveConnectorSourceArchiveFolder: Path?
234-
@JsonIgnore
235-
get() =
236-
if (sourceArchiveEnabled)
237-
sourceFolder.resolve((sourceArchiveFolder ?: TEMP_DIR_PATH).resolve(getDateNow()))
238-
.also { createDirectoryIfMissing(it) }
239-
else
240-
null
241-
242214
/**
243215
* Returns all source directories for all document types of this connector. If a [DocTypeFolders.sourceFolder] is not set, the entry is omitted.
244216
*/
@@ -269,35 +241,20 @@ internal data class Connector(
269241

270242
private fun getDateNow(): String = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
271243

244+
272245
/**
273-
* Returns the error directory resolved against the source directory with a subdirectory for the current date. The directories will be
274-
* created if they do not exist.
246+
* Returns the effective source error folder path. The directory will be created if it does not exist.
275247
*
276248
* @see sourceErrorFolder
277249
* @see sourceFolder
278250
*/
279251
@JsonIgnore
280-
fun getEffectiveSourceErrorFolder(path: Path): Path =
281-
(sourceErrorFolder ?: Path.of(EMPTY_STRING)).let { errorDir ->
282-
if (path.isDirectory()) {
283-
path
284-
} else {
285-
path.parent
286-
}.resolve(errorDir.resolve(getDateNow()))
287-
.also { createDirectoryIfMissing(it) }
288-
}
289-
290-
291-
/**
292-
* Convenience property to get the connector error directory that is used in all cases where no message type related directories are defined.
293-
* @see getEffectiveSourceErrorFolder
294-
*/
295-
val effectiveConnectorSourceErrorFolder: Path
296-
@JsonIgnore
297-
get() = (sourceErrorFolder ?: Path.of(EMPTY_STRING)).let { errorDir ->
298-
sourceFolder.resolve(errorDir.resolve(getDateNow()))
299-
.also { createDirectoryIfMissing(it) }
300-
}
252+
fun getEffectiveSourceErrorFolder(): Path =
253+
when (sourceErrorFolder) {
254+
null -> sourceFolder.resolve(ERROR_DIR_NAME)
255+
sourceFolder -> sourceFolder.resolve(ERROR_DIR_NAME)
256+
else -> sourceErrorFolder
257+
}.let { createDirectoryIfMissing(it) }
301258

302259
override fun toString(): String {
303260
return "Connector(connectorId='$connectorId', targetFolder=$targetFolder, sourceFolder=$sourceFolder, " +
@@ -309,23 +266,8 @@ internal data class Connector(
309266
EMPTY_STRING
310267
} +
311268
"contentType=$contentType, uploadArchiveEnabled=$sourceArchiveEnabled, sourceArchiveFolder=$sourceArchiveFolder, " +
312-
"effectiveSourceArchiveFolder=${effectiveConnectorSourceArchiveFolder}, " +
313-
if (sourceArchiveEnabled && effectiveDocTypeFolders.isNotEmpty())
314-
"additionalEffectiveSourceArchiveFolders=[${
315-
effectiveDocTypeFolders.entries.joinToString("; ") { "${it.key}=${getEffectiveSourceArchiveFolder(it.value.sourceFolder!!)}" }
316-
}], "
317-
else {
318-
EMPTY_STRING
319-
} +
320-
"sourceErrorFolder=$sourceErrorFolder, effectiveSourceErrorFolder=${effectiveConnectorSourceErrorFolder} " +
321-
if (effectiveDocTypeFolders.isNotEmpty())
322-
"additionalEffectiveSourceErrorFolders=[${
323-
effectiveDocTypeFolders.entries.filter { it.value.sourceFolder != null }
324-
.joinToString(", ") { "${it.key}=${getEffectiveSourceErrorFolder(it.value.sourceFolder!!)}" }
325-
}], "
326-
else {
327-
EMPTY_STRING
328-
} +
269+
"effectiveSourceArchiveFolder=${getEffectiveSourceArchiveFolder()}, " +
270+
"sourceErrorFolder=$sourceErrorFolder, effectiveSourceErrorFolder=${getEffectiveSourceErrorFolder()} " +
329271
"mode=$mode)"
330272
}
331273

cdr-client-service/src/main/kotlin/com/swisscom/health/des/cdr/client/config/ConfigConverter.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,20 @@ internal fun Connector.toDto(): DTOs.CdrClientConfig.Connector {
4141
Map<DTOs.CdrClientConfig.DocumentType, DTOs.CdrClientConfig.Connector.DocTypeFolders> {
4242
return map { (key, value) ->
4343
DTOs.CdrClientConfig.DocumentType.entries.first { it.name == key.name } to DTOs.CdrClientConfig.Connector.DocTypeFolders(
44-
sourceFolder = value.sourceFolder?.absolutePathString(),
45-
targetFolder = value.targetFolder?.absolutePathString(),
44+
sourceFolder = value.sourceFolder?.toString(),
45+
targetFolder = value.targetFolder?.toString(),
4646
)
4747
}.toMap()
4848
}
4949

5050
return DTOs.CdrClientConfig.Connector(
5151
connectorId = connectorId.id,
52-
targetFolder = targetFolder.absolutePathString(),
53-
sourceFolder = sourceFolder.absolutePathString(),
52+
targetFolder = targetFolder.toString(),
53+
sourceFolder = sourceFolder.toString(),
5454
contentType = contentType,
5555
sourceArchiveEnabled = sourceArchiveEnabled,
56-
sourceArchiveFolder = sourceArchiveFolder?.absolutePathString(),
57-
sourceErrorFolder = sourceErrorFolder?.absolutePathString(),
56+
sourceArchiveFolder = sourceArchiveFolder?.toString(),
57+
sourceErrorFolder = sourceErrorFolder?.toString(),
5858
mode = DTOs.CdrClientConfig.Mode.entries.first { it.name == mode.name },
5959
docTypeFolders = effectiveDocTypeFolders.toDto(),
6060
)

cdr-client-service/src/main/kotlin/com/swisscom/health/des/cdr/client/handler/CdrApiClient.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,24 +172,40 @@ internal class CdrApiClient(
172172
logger.debug { "Upload '$file' done" }
173173
}
174174

175-
response.code in 400..499 -> UploadDocumentResult.UploadClientErrorResponse(
175+
response.code == HttpStatus.BAD_REQUEST.value() -> UploadDocumentResult.UploadClientErrorResponse(
176176
code = response.code,
177177
responseBody = response.body.string()
178178
).also { result ->
179-
logger.info { "Upload '$file' encountered client error: '$result'" }
179+
logger.debug { "Upload '$file' encountered client error: '$result'" }
180+
}
181+
182+
response.code == HttpStatus.NOT_FOUND.value() || response.code == HttpStatus.TOO_MANY_REQUESTS.value() ->
183+
UploadDocumentResult.UploadClientRetryableErrorResponse(
184+
code = response.code,
185+
responseBody = response.body.string()
186+
).also { result ->
187+
logger.debug { "Upload '$file' encountered client config error: '$result'" }
188+
}
189+
190+
response.code in 400..499 -> UploadDocumentResult.UploadClientConfigNonRetryableErrorResponse(
191+
code = response.code,
192+
responseBody = response.body.string()
193+
).also { result ->
194+
logger.debug { "Upload '$file' encountered client config non-retryable error: '$result'" }
180195
}
181196

182197
else -> UploadDocumentResult.UploadServerErrorResponse(
183198
code = response.code,
184199
responseBody = response.body.string()
185200
).also { result ->
186-
logger.info { "Upload '$file' encountered server error: '$result'" }
201+
logger.debug { "Upload '$file' encountered server error: '$result'" }
187202
}
188203
}
189204
}
190205
}.fold(
191206
onSuccess = { it },
192207
onFailure = { t ->
208+
// 5xx responses from the server will also go through here, because of our okHttpClient Bean
193209
logger.error { "Upload file '$file' failed: ${t.message}" }
194210
UploadDocumentResult.UploadError(message = t.message ?: "Unknown error", t = t)
195211
}
@@ -409,6 +425,8 @@ internal class CdrApiClient(
409425
sealed interface UploadDocumentResult {
410426
object Success : UploadDocumentResult
411427
data class UploadClientErrorResponse(val code: Int, val responseBody: String) : UploadDocumentResult
428+
data class UploadClientRetryableErrorResponse(val code: Int, val responseBody: String) : UploadDocumentResult
429+
data class UploadClientConfigNonRetryableErrorResponse(val code: Int, val responseBody: String) : UploadDocumentResult
412430
data class UploadServerErrorResponse(val code: Int, val responseBody: String) : UploadDocumentResult
413431
data class UploadError(val message: String, val t: Throwable? = null) : UploadDocumentResult
414432
}

0 commit comments

Comments
 (0)