Skip to content

Commit

Permalink
Switch from aws-api to aws sdk v2 via interop
Browse files Browse the repository at this point in the history
Bit of a bummer, but aws-api was copying entire object to heap, and we
don't have enough heap for that. Our database backup is close to 1gb.

Abstracted s3 to its own namespace and protocol to make swapping aws-api back
in the future. Maybe they'll fix the issue, or maybe I was just using it wrong.

See: cognitect-labs/aws-api#257
  • Loading branch information
lread committed Sep 22, 2024
1 parent b8fd03e commit d9b704e
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 201 deletions.
7 changes: 2 additions & 5 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@
raven-clj/raven-clj {:mvn/version "1.7.0"} ;; Sentry service interface

;; s3 for database backups at Exoscale Simple Object Store
org.tcrawley/cognitect-http-client {:mvn/version "1.11.129"} ;; fix for jetty client version conflict
com.cognitect.aws/api {:mvn/version "0.8.692"
:exclusions [com.cognitect/http-client]}
com.cognitect.aws/endpoints {:mvn/version "1.1.12.770"}
com.cognitect.aws/s3 {:mvn/version "869.2.1687.0"}
;; started with aws-api but it loads entire objects on the heap, and we don't have enough heap for that!
software.amazon.awssdk/s3 {:mvn/version "2.28.5"}

;; reaching out to other services
org.eclipse.jgit/org.eclipse.jgit.ssh.jsch {:mvn/version "6.10.0.202406032230-r"} ;; git with jsch
Expand Down
12 changes: 12 additions & 0 deletions doc/adr/0021-Moving-To-Exoscale.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ tar --use-compress-program=zstd -xf dest.tar.zst

Our Lucene full-text database is quickly reconstituted from clojars at startup time, so no need to save a backup of it.

Our backup files are close to 1gb, and because we need to be cheap we have a small heap.
The cognitect aws api, unfortunately, loads an entire file into memory.
This gives us OutOfMemory exceptions.
What to do?
* I had a peek at https://github.com/grzm/awyeah-api and I think it uses byte buffers too.
* Could try amazonica.
* Could try AWS SDK through java interop.
* Could spawn out to upload and download files.
* Could handle this with raw HTTP requests

* I think I might try the AWS SDK next.

=== Packer or Cloud Init?
We currently use packer to build our host image.

Expand Down
8 changes: 4 additions & 4 deletions ops/exoscale/deploy/resources/secrets.edn
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
:builder-project #env! CIRCLE_BUILDER_PROJECT}
:s3 {;; note: we are using Exoscale, if we need to be more flexible in the future could
;; configure a :backups-provider
:backups-bucket-region #env! EXO_BACKUPS_BUCKET_REGION ;; in exoscale these are called zones
:backups-bucket-name #env! EXO_BACKUPS_BUCKET_NAME
:backups-bucket-key #env! EXO_BACKUPS_BUCKET_KEY
:backups-bucket-secret #env! EXO_BACKUPS_BUCKET_SECRET}
:backups {:bucket-region #env! EXO_BACKUPS_BUCKET_REGION ;; in exoscale these are called zones
:bucket-name #env! EXO_BACKUPS_BUCKET_NAME
:bucket-key #env! EXO_BACKUPS_BUCKET_KEY
:bucket-secret #env! EXO_BACKUPS_BUCKET_SECRET}}
:sentry {:dsn #env! SENTRY_DSN}}
5 changes: 1 addition & 4 deletions src/cljdoc/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,7 @@
(-> config
:secrets
:s3
(select-keys [:backups-bucket-region
:backups-bucket-name
:backups-bucket-key
:backups-bucket-secret])))
:backups))

(defn db-backup [config]
(let [enabled? (enable-db-backup? config)]
Expand Down
158 changes: 158 additions & 0 deletions src/cljdoc/s3.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
(ns cljdoc.s3
(:require [cljdoc.server.log-init] ;; to quiet odd jetty DEBUG logging
[clojure.java.io :as io])
(:import (java.lang AutoCloseable)
(software.amazon.awssdk.auth.credentials AwsBasicCredentials AwsCredentialsProvider StaticCredentialsProvider)
(software.amazon.awssdk.core.sync RequestBody ResponseTransformer)
(software.amazon.awssdk.regions Region)
(software.amazon.awssdk.services.s3 S3Client)
(software.amazon.awssdk.services.s3.model CopyObjectRequest DeleteObjectRequest GetObjectRequest ListObjectsV2Request ObjectCannedACL PutObjectRequest S3Object)))

(set! *warn-on-reflection* true)

(defprotocol IObjectStore
"Use a protocol to make switching to different implementation a bit easier
We are currently using aws sdk, but only because aws-api currently blows our heap
by loading entire objects into RAM.
implement specific to our use case:
- always cotained to a single bucket
- public-read acl
- only expose data we care about
- put and get at granularity of file only (no streams or strings, etc)"
(list-objects [object-store])
(put-object [object-store object-key from-file])
(get-object [object-store object-key to-file])
(delete-object [object-store object-key])
(copy-object [object-store source-key dest-key]))

(defrecord AwsSdkObjectStore [^S3Client s3 opts]
IObjectStore AutoCloseable
(list-objects [_]
(let [{:keys [bucket-name]} opts
^ListObjectsV2Request request (-> (ListObjectsV2Request/builder)
(.bucket bucket-name)
.build)]
(->> (.listObjectsV2 s3 request)
.contents
(mapv (fn [^S3Object o] {:key (.key o)})))))
(put-object [_ object-key from-file]
(let [{:keys [bucket-name]} opts
^PutObjectRequest request (-> (PutObjectRequest/builder)
(.bucket bucket-name)
(.key object-key)
(.acl ObjectCannedACL/PUBLIC_READ)
.build)]
(.putObject s3 request (RequestBody/fromFile (io/file from-file)))))
(get-object [_ object-key to-file]
(let [{:keys [bucket-name]} opts
^GetObjectRequest request (-> (GetObjectRequest/builder)
(.bucket bucket-name)
(.key object-key)
.build)]
(.getObject s3 request (ResponseTransformer/toFile (io/file to-file)))))
(delete-object [_ object-key]
(let [{:keys [bucket-name]} opts
^DeleteObjectRequest request (-> (DeleteObjectRequest/builder)
(.bucket bucket-name)
(.key object-key)
.build)]
(.deleteObject s3 request)))
(copy-object [_ source-key dest-key]
(let [{:keys [bucket-name]} opts
^CopyObjectRequest request (-> (CopyObjectRequest/builder)
(.sourceBucket bucket-name)
(.sourceKey source-key)
(.destinationBucket bucket-name)
(.destinationKey dest-key)
.build)]
(.copyObject s3 request)))
(close [_] (.close s3)))

(defn s3-exo-client [{:keys [bucket-key bucket-secret bucket-region]}]
(let [endpoint (format "https://sos-%s.exo.io" bucket-region)
^AwsCredentialsProvider creds-provider (StaticCredentialsProvider/create
(AwsBasicCredentials/create bucket-key bucket-secret))]
(.build (doto (S3Client/builder)
(.region Region/AWS_GLOBAL) ;; AWS SDK requires this even though we are not using AWS services
(.endpointOverride (java.net.URI. endpoint))
(.credentialsProvider creds-provider)))))

(defn make-exo-object-store [opts]
(let [s3 (s3-exo-client opts)]
(AwsSdkObjectStore. s3 opts)))

(comment
(require '[cljdoc.config :as cfg])

;; assumes you've loaded up secrets to a working exo endpoint
(def opts (cfg/db-backup (cfg/config)))

(:bucket-region opts)

(:bucket-name opts)

(spit "target/dummy-file.txt" "foobar")

(def object-store (make-exo-object-store opts))

(list-objects object-store)
;; => [{:key "daily/cljdoc-db-2024-09-03_2024-09-03T20-22-00.tar.zst"}
;; {:key "daily/cljdoc-db-2024-09-17_2024-09-17T18-01-44.tar.zst"}]

(put-object object-store "daily/dummy-file" "target/dummy-file.txt")
;; => #object[software.amazon.awssdk.services.s3.model.PutObjectResponse 0x67c79dcd "PutObjectResponse(ETag=\"3858f62230ac3c915f300c664312c63f\")"]

(list-objects object-store)
;; => [{:key "daily/cljdoc-db-2024-09-03_2024-09-03T20-22-00.tar.zst"}
;; {:key "daily/cljdoc-db-2024-09-17_2024-09-17T18-01-44.tar.zst"}
;; {:key "daily/dummy-file"}]

(get-object object-store "daily/dummy-file" "target/dummy-file.down.txt")
;; => #object[software.amazon.awssdk.services.s3.model.GetObjectResponse 0x7e2790ce "GetObjectResponse(AcceptRanges=bytes, LastModified=2024-09-21T14:12:04Z, ContentLength=6, ETag=\"3858f62230ac3c915f300c664312c63f\", ContentType=text/plain, Metadata={})"]

(slurp "target/dummy-file.down.txt")
;; => "foobar"

(delete-object object-store "daily/dummy-file")
;; => #object[software.amazon.awssdk.services.s3.model.DeleteObjectResponse 0x4465db91 "DeleteObjectResponse()"]

(list-objects object-store)
;; => [{:key "daily/cljdoc-db-2024-09-03_2024-09-03T20-22-00.tar.zst"}
;; {:key "daily/cljdoc-db-2024-09-17_2024-09-17T18-01-44.tar.zst"}]

(put-object object-store "daily/dummy-file" "target/dummy-file.txt")
;; => #object[software.amazon.awssdk.services.s3.model.PutObjectResponse 0x517e713b "PutObjectResponse(ETag=\"3858f62230ac3c915f300c664312c63f\")"]

(copy-object object-store "daily/dummy-file" "daily/dummy-file-copy")
;; => #object[software.amazon.awssdk.services.s3.model.CopyObjectResponse 0x4488e7e1 "CopyObjectResponse(CopyObjectResult=CopyObjectResult(ETag=3858f62230ac3c915f300c664312c63f, LastModified=2024-09-21T14:17:12.018Z))"]

(list-objects object-store)
;; => [{:key "daily/cljdoc-db-2024-09-03_2024-09-03T20-22-00.tar.zst"}
;; {:key "daily/cljdoc-db-2024-09-17_2024-09-17T18-01-44.tar.zst"}
;; {:key "daily/dummy-file"}
;; {:key "daily/dummy-file-copy"}]

(get-object object-store "daily/dummy-file-copy" "target/dummy-file-copy.down.txt")
;; => #object[software.amazon.awssdk.services.s3.model.GetObjectResponse 0x437659bf "GetObjectResponse(AcceptRanges=bytes, LastModified=2024-09-21T14:17:12Z, ContentLength=6, ETag=\"3858f62230ac3c915f300c664312c63f\", ContentType=text/plain, Metadata={})"]

(slurp "target/dummy-file-copy.down.txt")
;; => "foobar"

(delete-object object-store "daily/dummy-file-copy")
;; => #object[software.amazon.awssdk.services.s3.model.DeleteObjectResponse 0x7235fc92 "DeleteObjectResponse()"]

(delete-object object-store "daily/dummy-file")
;; => #object[software.amazon.awssdk.services.s3.model.DeleteObjectResponse 0x2507c9f0 "DeleteObjectResponse()"]

(list-objects object-store)
;; => [{:key "daily/cljdoc-db-2024-09-03_2024-09-03T20-22-00.tar.zst"}
;; {:key "daily/cljdoc-db-2024-09-17_2024-09-17T18-01-44.tar.zst"}]

(.close object-store)

(list-objects object-store)
;; => Execution error (IllegalStateException) at org.apache.http.util.Asserts/check (Asserts.java:34).
;; Connection pool shut down

:eoc)
Loading

0 comments on commit d9b704e

Please sign in to comment.