Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ spring_cloud_openfeign = "4.2.0"
openfeign_hc5 = "13.1"
openfeign_micrometer = "13.1"

# Resilience4j
resilience4j = "2.2.0"

# Bucket4j
bucket4j = "8.15.0"

Expand Down Expand Up @@ -186,6 +189,7 @@ epages_restdocs_api_spec_mock_mvc = { module = "com.epages:restdocs-api-spec-moc
epages_restdocs_api_spec_restassured = { module = "com.epages:restdocs-api-spec-restassured", version.ref = "epages_restdocs_api_spec" }

# Monitoring & Logging Libraries
micrometer_core = { module = "io.micrometer:micrometer-core" }
micrometer_tracing_bridge_brave = { module = "io.micrometer:micrometer-tracing-bridge-brave" }
micrometer_registry_prometheus = { module = "io.micrometer:micrometer-registry-prometheus" }
sentry_spring_boot_starter_jakarta = { module = "io.sentry:sentry-spring-boot-starter-jakarta", version.ref = "sentry" }
Expand All @@ -194,10 +198,18 @@ slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.9" }

# AWS Libraries
aws_sdk_s3 = { module = "software.amazon.awssdk:s3", version.ref = "aws" }
aws_sdk_url_connection_client = { module = "software.amazon.awssdk:url-connection-client", version.ref = "aws" }
aws_sdk_netty_nio_client = { module = "software.amazon.awssdk:netty-nio-client", version.ref = "aws" }

# Firebase
firebase = { module = "com.google.firebase:firebase-admin", version.ref = "firebase" }

# Resilience4j
resilience4j_circuitbreaker = { module = "io.github.resilience4j:resilience4j-circuitbreaker", version.ref = "resilience4j" }
resilience4j_bulkhead = { module = "io.github.resilience4j:resilience4j-bulkhead", version.ref = "resilience4j" }
resilience4j_spring_boot3 = { module = "io.github.resilience4j:resilience4j-spring-boot3", version.ref = "resilience4j" }
resilience4j_micrometer = { module = "io.github.resilience4j:resilience4j-micrometer", version.ref = "resilience4j" }

# Bucket4j
bucket4j_core = { module = "com.bucket4j:bucket4j_jdk17-core", version.ref = "bucket4j" }

Expand All @@ -206,7 +218,8 @@ caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "c

[bundles]
line_kotlin_jdsl = ["line_kotlin_jdsl_jpql_dsl", "line_kotlin_jdsl_jpql_render", "line_kotlin_jdsl_spring_data_jpa_support"]
aws_client = ["aws_sdk_s3"]
aws_client = ["aws_sdk_s3", "aws_sdk_url_connection_client", "aws_sdk_netty_nio_client"]
resilience4j = ["resilience4j_circuitbreaker", "resilience4j_bulkhead"]
openfeign = ["spring_cloud_openfeign", "openfeign_hc5", "openfeign_micrometer"]
jackson = ["jackson_kotlin", "jackson_datatype_jsr310"]
testcontainers_postgres = ["test_containers_postgres", "spring_boot_testcontainers", "test_containers_junit_jupiter", "spring_boot_starter_test"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.pida.client.airquality
import com.pida.support.error.ErrorException
import com.pida.support.error.ErrorType
import com.pida.support.extension.logger
import com.pida.support.resilience.ExternalDependency
import com.pida.support.resilience.ExternalDependencyPolicy
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component

Expand All @@ -17,6 +19,7 @@ class AirKoreaClient internal constructor(
private val stationServiceKey: String,
private val airKoreaAirQualityApi: AirKoreaAirQualityApi,
private val airKoreaStationApi: AirKoreaStationApi,
private val externalDependencyPolicy: ExternalDependencyPolicy,
) {
private val logger by logger()

Expand All @@ -29,10 +32,12 @@ class AirKoreaClient internal constructor(
fun getAirQualityByStation(stationName: String): AirKoreaResponse =
try {
val response =
airKoreaAirQualityApi.getMsrstnAcctoRltmMesureDnsty(
serviceKey = airQualityServiceKey,
stationName = stationName,
)
externalDependencyPolicy.execute(ExternalDependency.AIR_KOREA) {
airKoreaAirQualityApi.getMsrstnAcctoRltmMesureDnsty(
serviceKey = airQualityServiceKey,
stationName = stationName,
)
}

if (response.response.header.resultCode != "00") {
logger.error("AirKorea API error: ${response.response.header.resultMsg}")
Expand Down Expand Up @@ -62,11 +67,13 @@ class AirKoreaClient internal constructor(

return try {
val response =
airKoreaStationApi.getNearbyMsrstnList(
serviceKey = stationServiceKey,
tmX = tmX,
tmY = tmY,
)
externalDependencyPolicy.execute(ExternalDependency.AIR_KOREA) {
airKoreaStationApi.getNearbyMsrstnList(
serviceKey = stationServiceKey,
tmX = tmX,
tmY = tmY,
)
}

if (response.response.header.resultCode != "00") {
logger.error("AirKorea station API error: ${response.response.header.resultMsg}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.pida.client.airquality

import com.pida.support.error.ErrorException
import com.pida.support.error.ErrorType
import com.pida.support.resilience.ExternalDependencyPolicy
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
Expand All @@ -13,7 +14,7 @@ class AirKoreaClientTest {
fun `๊ทผ์ ‘ ์ธก์ •์†Œ ์กฐํšŒ ์„ฑ๊ณต ์‹œ ์ฒซ ๋ฒˆ์งธ ์ธก์ •์†Œ๋ช…์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaStationApi.getNearbyMsrstnList(
Expand Down Expand Up @@ -57,7 +58,7 @@ class AirKoreaClientTest {
fun `๊ทผ์ ‘ ์ธก์ •์†Œ ์กฐํšŒ ๊ฒฐ๊ณผ ์ฝ”๋“œ๊ฐ€ ์‹คํŒจ๋ฉด AIR_QUALITY_STATION_NOT_FOUND๋ฅผ ๋˜์ง„๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaStationApi.getNearbyMsrstnList(
Expand Down Expand Up @@ -86,7 +87,7 @@ class AirKoreaClientTest {
fun `๊ทผ์ ‘ ์ธก์ •์†Œ ์กฐํšŒ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด AIR_QUALITY_STATION_NOT_FOUND๋ฅผ ๋˜์ง„๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaStationApi.getNearbyMsrstnList(
Expand Down Expand Up @@ -115,7 +116,7 @@ class AirKoreaClientTest {
fun `๊ทผ์ ‘ ์ธก์ •์†Œ ์กฐํšŒ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด AIR_QUALITY_STATION_NOT_FOUND๋ฅผ ๋˜์ง„๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaStationApi.getNearbyMsrstnList(
Expand All @@ -137,7 +138,7 @@ class AirKoreaClientTest {
fun `๋Œ€๊ธฐ์งˆ ์กฐํšŒ ๊ฒฐ๊ณผ ์ฝ”๋“œ๊ฐ€ ์‹คํŒจ๋ฉด AIR_QUALITY_API_CALL_FAILED๋ฅผ ๋˜์ง„๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaAirQualityApi.getMsrstnAcctoRltmMesureDnsty(
Expand Down Expand Up @@ -165,7 +166,7 @@ class AirKoreaClientTest {
fun `๋Œ€๊ธฐ์งˆ ์กฐํšŒ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด AIR_QUALITY_API_CALL_FAILED๋ฅผ ๋˜์ง„๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaAirQualityApi.getMsrstnAcctoRltmMesureDnsty(
Expand All @@ -186,7 +187,7 @@ class AirKoreaClientTest {
fun `๋ฉ”์„œ๋“œ๋ณ„๋กœ ์„œ๋กœ ๋‹ค๋ฅธ service key๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค`() {
val airKoreaAirQualityApi = mockk<AirKoreaAirQualityApi>()
val airKoreaStationApi = mockk<AirKoreaStationApi>()
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi)
val client = AirKoreaClient("air-quality-key", "station-key", airKoreaAirQualityApi, airKoreaStationApi, passthroughPolicy())

every {
airKoreaStationApi.getNearbyMsrstnList(
Expand Down Expand Up @@ -251,4 +252,9 @@ class AirKoreaClientTest {
client.getNearbyStation("192968", "4667503")
client.getAirQualityByStation("์ข…๋กœ๊ตฌ")
}

private fun passthroughPolicy(): ExternalDependencyPolicy =
mockk {
every { execute<Any>(any(), any()) } answers { secondArg<() -> Any>().invoke() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.presigner.S3Presigner
import java.net.URI
import java.time.Duration

@Configuration
class AwsConfig(
Expand All @@ -35,11 +39,26 @@ class AwsConfig(

@Bean(destroyMethod = "close") // ์Šคํ”„๋ง ์ข…๋ฃŒ ์‹œ ์ปค๋„ฅ์…˜ ํ’€ ์ •๋ฆฌ
fun s3Client(): S3Client {
val connectionTimeout = Duration.ofMillis(awsProperties.connectionTimeout)
val socketTimeout = Duration.ofMillis(awsProperties.socketTimeout)
val client =
S3Client
.builder()
.credentialsProvider(credentialProvider())
.region(Region.of(awsProperties.region))
.httpClient(
UrlConnectionHttpClient
.builder()
.connectionTimeout(connectionTimeout)
.socketTimeout(socketTimeout)
.build(),
).overrideConfiguration(
ClientOverrideConfiguration
.builder()
.apiCallAttemptTimeout(socketTimeout)
.apiCallTimeout(socketTimeout.plusMillis(500))
.build(),
Comment on lines +49 to +60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

cat -n pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt

Repository: Team-PIDA/Pida-Server

Length of output: 5146


๐ŸŒ Web query:

AWS SDK v2 httpClient vs httpClientBuilder ownership lifecycle

๐Ÿ’ก Result:

In AWS SDK for Java 2.x, SdkHttpClient instances (like ApacheHttpClient) manage connection pools and resources that must be closed explicitly. There are two primary patterns for using HttpClient and HttpClientBuilder: 1. Dedicated HTTP client (recommended best practice): Use serviceClient.builder.httpClientBuilder(HttpClientType.builder.config).build. The service client creates, owns, and manages the lifecycle of the HTTP client. When serviceClient.close is called, it automatically closes the HTTP client, preventing resource leaks. 2. Shared HTTP client: Build the client separately with HttpClientType.builder.config.build, then pass it to multiple serviceClient.builder.httpClient(sharedClient).build. You (the application) own the lifecycle and must explicitly call sharedClient.close after closing all service clients that use it. The SDK does not close shared clients automatically. All SdkHttpClient implementations (ApacheHttpClient, Apache5HttpClient, UrlConnectionHttpClient, etc.) follow this ownership model. Service clients are thread-safe and should be reused where possible; always close them when done to release HTTP resources. Examples from docs: Dedicated: S3Client s3Client = S3Client.builder .httpClientBuilder(ApacheHttpClient.builder.maxConnections(100)) .build; s3Client.close; // Closes embedded HTTP client Shared: SdkHttpClient httpClient = ApacheHttpClient.builder.maxConnections(100).build; S3Client s3 = S3Client.builder.httpClient(httpClient).build; DynamoDbClient ddb = DynamoDbClient.builder.httpClient(httpClient).build; s3.close; ddb.close; httpClient.close; // Required

Citations:


๐ŸŒ Web query:

AWS SDK S3Client close() httpClient lifecycle management behavior

๐Ÿ’ก Result:

In the AWS SDK for Java 2.x, both S3Client (synchronous) and S3AsyncClient (asynchronous) implement AutoCloseable and SdkAutoCloseable. Calling close on these service clients releases resources such as threads and the HTTP connection pool managed by the embedded HTTP client (ApacheHttpClient for sync S3Client by default, NettyNioAsyncHttpClient for async). Key behaviors: - Each service client maintains its own HTTP connection pool for performance (reusing connections avoids establishment overhead). - Service clients are thread-safe and intended for reuse across requests. - close should be called on unused clients to prevent resource leaks (threads, connections). - If you provide a custom/external HTTP client via S3Client.builder.httpClient(yourHttpClient), the service client's close does NOT close the shared HTTP client; you must close it explicitly after all service clients using it are closed. - Default/embedded HTTP clients are managed and closed by the service client. - Post-close, the client should not be used; attempts may fail (no isClosed method exists). Best practice: Reuse a single client instance application-wide; close only at shutdown. For short-lived clients, always close explicitly or use try-with-resources. Example (default embedded HTTP client): S3Client s3 = S3Client.builder.build; // Use s3... s3.close; // Releases embedded ApacheHttpClient connection pool & threads Example (shared external HTTP client): SdkHttpClient httpClient = ApacheHttpClient.builder.build; S3Client s3 = S3Client.builder.httpClient(httpClient).build; DynamoDbClient ddb = DynamoDbClient.builder.httpClient(httpClient).build; // Use clients... s3.close; // Does NOT close httpClient ddb.close; httpClient.close; // Explicitly close shared client last

Citations:


Use .httpClientBuilder() to ensure HTTP client lifecycle is managed by the SDK.

With AWS SDK v2, .httpClient() requires the application to own and manage the HTTP client lifecycle. Passing a built UrlConnectionHttpClient or NettyNioAsyncHttpClient inline means the SDK will not close them when the S3Client or S3AsyncClient beans are closed. Only the service clients themselves are closed via destroyMethod = "close", leaving the HTTP clients' connection pools and threads unreleased.

Prefer .httpClientBuilder() insteadโ€”the SDK will create, own, and automatically close the HTTP client when the service client is destroyed.

โ™ป๏ธ Suggested direction
-                .httpClient(
-                    UrlConnectionHttpClient
-                        .builder()
-                        .connectionTimeout(connectionTimeout)
-                        .socketTimeout(socketTimeout)
-                        .build(),
-                )
+                .httpClientBuilder(
+                    UrlConnectionHttpClient
+                        .builder()
+                        .connectionTimeout(connectionTimeout)
+                        .socketTimeout(socketTimeout),
+                )
-                .httpClient(
-                    NettyNioAsyncHttpClient
-                        .builder()
-                        .connectionTimeout(connectionTimeout)
-                        .readTimeout(socketTimeout)
-                        .build(),
-                )
+                .httpClientBuilder(
+                    NettyNioAsyncHttpClient
+                        .builder()
+                        .connectionTimeout(connectionTimeout)
+                        .readTimeout(socketTimeout),
+                )

Also applies to: 78-89

๐Ÿค– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@pida-clients/aws-client/src/main/kotlin/com/pida/client/aws/config/AwsConfig.kt`
around lines 49 - 60, The AWS SDK HTTP clients are being built and passed into
.httpClient(...), which forces the app to manage their lifecycle; instead pass
the builders to .httpClientBuilder(...) so the SDK creates and closes the HTTP
client automatically. Update the sections that currently call
UrlConnectionHttpClient.builder().connectionTimeout(...).socketTimeout(...).build()
(and any NettyNioAsyncHttpClient.builder() variants) to pass the builder itself
to
.httpClientBuilder(UrlConnectionHttpClient.builder().connectionTimeout(...).socketTimeout(...))
(i.e., remove the final .build()), and do the analogous change where
S3Client/S3AsyncClient are configured so the SDK owns and closes the HTTP client
when the client beans are closed.

)
awsProperties.endpoint?.let {
client.endpointOverride(URI.create(awsProperties.endpoint))
}
Expand All @@ -49,11 +68,26 @@ class AwsConfig(

@Bean(destroyMethod = "close")
fun s3AsyncClient(): S3AsyncClient {
val connectionTimeout = Duration.ofMillis(awsProperties.connectionTimeout)
val socketTimeout = Duration.ofMillis(awsProperties.socketTimeout)
val client =
S3AsyncClient
.builder()
.credentialsProvider(credentialProvider())
.region(Region.of(awsProperties.region))
.httpClient(
NettyNioAsyncHttpClient
.builder()
.connectionTimeout(connectionTimeout)
.readTimeout(socketTimeout)
.build(),
).overrideConfiguration(
ClientOverrideConfiguration
.builder()
.apiCallAttemptTimeout(socketTimeout)
.apiCallTimeout(socketTimeout.plusMillis(500))
.build(),
)
awsProperties.endpoint?.let {
client.endpointOverride(URI.create(awsProperties.endpoint))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ data class AwsProperties(
val s3: S3Properties,
val region: String,
val endpoint: String?,
val connectionTimeout: Long,
val socketTimeout: Long,
)

data class CredentialsProperties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.pida.support.aws.PresignedUrlRateLimiter
import com.pida.support.aws.S3ImageInfo
import com.pida.support.aws.S3ImageUrl
import com.pida.support.aws.S3UploadResult
import com.pida.support.resilience.ExternalDependency
import com.pida.support.resilience.ExternalDependencyPolicy
import org.springframework.stereotype.Component
import software.amazon.awssdk.services.s3.model.S3Object
import java.time.Duration
Expand All @@ -21,6 +23,7 @@ class ImageS3Processor(
private val awsProperties: AwsProperties,
private val imageFileConstructor: ImageFileConstructor,
private val rateLimiter: PresignedUrlRateLimiter,
private val externalDependencyPolicy: ExternalDependencyPolicy,
) : ImageS3Caller {
companion object {
private val SEOUL_ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
Expand Down Expand Up @@ -56,53 +59,56 @@ class ImageS3Processor(
prefix: String,
prefixId: Long,
fileName: String?,
): List<S3ImageInfo> {
val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId)
): List<S3ImageInfo> =
externalDependencyPolicy.executeSuspend(ExternalDependency.AWS_S3) {
val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId)

return fileName
?.let {
listOf(presignedGet(imageFilePath, it)) // fileName์ด ์žˆ์œผ๋ฉด ํŠน์ • ์ด๋ฏธ์ง€ ์กฐํšŒ
} ?: listPresignedGets(imageFilePath) // ์•„๋‹ˆ๋ฉด ํ•ด๋‹น ๊ฒฝ๋กœ ์•„๋ž˜ ๋ชจ๋“  ์ด๋ฏธ์ง€ ํƒ์ƒ‰
}
fileName
?.let {
listOf(presignedGet(imageFilePath, it))
} ?: listPresignedGets(imageFilePath)
}

override fun uploadImage(
prefix: String,
prefixId: Long,
subPath: String,
contentType: String,
bytes: ByteArray,
): S3UploadResult {
val filePath = imageFileConstructor.imageFilePath(prefix, prefixId)
val fileName = imageFileConstructor.imageFileName()
val s3Key = "$filePath/$subPath/$fileName"
): S3UploadResult =
externalDependencyPolicy.execute(ExternalDependency.AWS_S3) {
val filePath = imageFileConstructor.imageFilePath(prefix, prefixId)
val fileName = imageFileConstructor.imageFileName()
val s3Key = "$filePath/$subPath/$fileName"

awsS3Client.putObject(
bucketName = awsProperties.s3.bucket,
key = s3Key,
contentType = contentType,
bytes = bytes,
)
awsS3Client.putObject(
bucketName = awsProperties.s3.bucket,
key = s3Key,
contentType = contentType,
bytes = bytes,
)

return S3UploadResult(
s3Key = s3Key,
publicUrl = "${awsProperties.s3.imageOriginUrl}/$s3Key",
)
}
S3UploadResult(
s3Key = s3Key,
publicUrl = "${awsProperties.s3.imageOriginUrl}/$s3Key",
)
}

override suspend fun getPreviewImage(
prefix: String,
prefixId: Long,
): S3ImageInfo? {
val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId)
): S3ImageInfo? =
externalDependencyPolicy.executeSuspend(ExternalDependency.AWS_S3) {
val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId)

return awsS3AsyncClient
.listObjects(
bucketName = awsProperties.s3.bucket,
filePath = imageFilePath,
).filterNot { it.key().endsWith("/") }
.maxByOrNull(S3Object::lastModified)
?.toImageInfo(imageFilePath, Duration.ofSeconds(30))
}
awsS3AsyncClient
.listObjects(
bucketName = awsProperties.s3.bucket,
filePath = imageFilePath,
).filterNot { it.key().endsWith("/") }
.maxByOrNull(S3Object::lastModified)
?.toImageInfo(imageFilePath, Duration.ofSeconds(30))
}

override fun generatePresignedUrl(s3Key: String): String {
val filePath = s3Key.substringBeforeLast("/")
Expand Down
12 changes: 6 additions & 6 deletions pida-clients/aws-client/src/main/resources/aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ cloud:
image-origin-url: ${AWS_S3_IMAGE_ORIGIN_URL}
stack:
auto: false
connection-time-out: 5000
socket-time-out: 30000
connection-time-out: 1000
socket-time-out: 2000
---
spring:
config:
Expand All @@ -31,8 +31,8 @@ cloud:
image-origin-url: ${AWS_S3_IMAGE_ORIGIN_URL}
stack:
auto: false
connection-time-out: 900000
socket-time-out: 900000
connection-time-out: 1000
socket-time-out: 2000
---
spring:
config:
Expand All @@ -49,5 +49,5 @@ cloud:
image-origin-url: ${AWS_S3_IMAGE_ORIGIN_URL}
stack:
auto: false
connection-time-out: 900000
socket-time-out: 900000
connection-time-out: 1000
socket-time-out: 2000
Loading
Loading