Fork me on GitHub
#clojure
<
2023-11-07
>
hifumi12306:11:52

When using the Cognitect AWS API, how does one generate a presigned URL (specifically PresignPutObject)? I took a look at https://github.com/cognitect-labs/aws-api/issues/236 but I would like to avoid having to duplicate client setup logic by calling out to the Java SDK, if possible.

p-himik08:11:35

I create a dummy client that uses the same arguments but overrides :credentials-provider (not sure why - just in case, probably) and :http-client with this:

(reify cognitect.aws.http/HttpClient
  (-submit [_ _request channel]
    (a/go
      (a/>! channel {:status  200
                     :headers {}}))
    channel)
  (-stop [_]))
After that, I do this:
(defn get-public-object-url [bucket key s3-dummy]
  (let [result (aws/invoke s3-dummy {:op      :GetObject
                                     :request {:Bucket bucket
                                               :Key    key}})
        {:keys [scheme server-name server-port uri]} (-> result check-aws-result meta :http-request)
        ;; Having the default port in there interferes with the URL signing algorithm of the client apps.
        default-port (case scheme
                       :http 80
                       :https 443
                       nil)
        colon+port (when (not= server-port default-port)
                     (str ":" server-port))
        slash+uri (cond->> uri
                    (not (str/starts-with? uri "/"))
                    (str "/"))]
    (str (name scheme) "://" server-name colon+port slash+uri)))
The check-aws-result just checks for anomalies and the status code.

p-himik08:11:07

Oh, hold on. That code is probably not what you're looking for. I think I started writing it as an attempt to get a presigned URL but ended up just making the specific objects public (was OK in that case) and just didn't want to generate the whole URL myself.

p-himik08:11:26

Yeah, I remember now. Instead of presigning URLs for private objects, I ended up proxying the data. That particular project works only with small objects, and proxying also gave me a bit of flexibility in how I deal with the data. Sorry for misleading. :)

Jacob Emcken12:11:18

There might be some relevant information here as well: https://github.com/cognitect-labs/aws-api/issues/5

Jacob Emcken12:11:11

I guess .generatePresignedUrl takes an AmazonS3Client (whatever that is), and it might be stored somewhere inside Cognitect client (`(def s3 (aws/client {:api :s3}))`) on which you can extract some data like https://github.com/cognitect-labs/aws-api/blob/main/src/cognitect/aws/client/api.clj#L66 E.g. (:service s3) Presigning does not require a HTTP client because it doesn't talk contact AWS to sign a URL. It does the signing entirely client side, by using the access key, secret key and session token. I ended up creating a small lib that does only that for some GET URL's because I wanted a "slim" Babashka script without a lot of external dependencies (read no AWS Java SDK): https://github.com/jacobemcken/aws-simple-sign (it does not support PUT URLs though).

jeroenvandijk10:11:57

I have been able to do presigned PUT requests with the code here https://gist.github.com/jeroenvandijk/516e13fd9ee3b7adbe010455dc128d8c#file-presigned_aws_api-clj-L21-L78 (also in the PR mentioned above). The aws-lib doesn't support it yet. This can easily be ported to the babashka aw-yeah too

dclojure11:11:26

I am using the Cognitect AWS API and we have vpce endpoints for both sts and s3. I am using the :endopoint-override to configure the vpce. But they don't seem to be working - in the logs I see calls going out to sts.amazon... Am I missing something?

viesti12:11:36

Hmm

user=> (let [uuid (random-uuid)]
  #_=>   (-> {:a {uuid {:b 1}}} :a uuid :b))
Execution error (ClassCastException) at user/eval62321 (REPL:2).
class java.util.UUID cannot be cast to class clojure.lang.IFn
I wonder what would break if java.util.UUID was invokable

viesti12:11:55

just a random not-well-formed thought 🙂 was thinking that most often use uuid as a key, the above -> navigation can be done with get-in

user=> (let [uuid (random-uuid)]
  #_=>   (get-in {:a {uuid {:b 1}}} [:a uuid :b]))
1
but thought what if the threading form would be supported too, if you have something not invocable in between but now realising that can write such with
user=> (let [uuid (random-uuid)]
  #_=>   (-> {:a {uuid {:b 1}}} :a (get uuid) :b))
1

viesti12:11:02

well, maybe too random thought

💯 1
teodorlu12:11:32

In this example:

user=> (let [uuid (random-uuid)]
  #_=>   (-> {:a {uuid {:b 1}}} :a uuid :b))
Execution error (ClassCastException) at user/eval62321 (REPL:2).
class java.util.UUID cannot be cast to class clojure.lang.IFn
you’re explicitly treating uuid as a function (between :a and :b). If that isn’t what you wanted to do, you could change it! Example:
(let [uuid (random-uuid)]
  (-> {:a {uuid {:b 1}}} :a (get uuid) :b))
;; => 1

teodorlu12:11:05

I also struggle to grok exactly what you were thinking about!

oyakushev13:11:19

To be callable, an object has to implement clojure.lang.IFn interface. Since java.uil.UUID is not a Clojure-defined class, it can't implement extra interfaces after the fact.

viesti13:11:51

faint memory that was the situation different in ClojureScript?

nezaj21:11:08

(comment
  (defn random-q []  [(str "SELECT '" (java.util.UUID/randomUUID) "'")])

  (do
    (def pool-size 50)
    (when (bound? #'conn-pool)
      (.close conn-pool))
    (def conn-pool
      (let [pool (next.jdbc.connection/->pool
                  com.zaxxer.hikari.HikariDataSource
                  (assoc (instant.config/get-aurora-config)
                         :maxLifetime (* 10 60 1000)
                         :maximumPoolSize pool-size))]
        (.close (next.jdbc/get-connection pool))
        pool)))
  
  (time
   (->> (range 0 1000)
        (mapv (fn [x]
                (future (next.jdbc.sql/query conn-pool (random-q)))))
        (mapv deref))))
Postgres logs say that the results of (random-q) are all completed in about 0.02ms. Running Aurora PostgreSQL 13.8 db.t3.medium The server and the machine are in the same datacenter. Executing just one query from the perspective of clojure takes about 2 ms. Given this, I would expect the following:
number of parallel batches we can run: 1000 / 50  = 20 
time per batch = 2ms 
expected latency: 40ms
But I get between 100-300ms of latency. I can’t quite understand why. I feel like I am missing some deeper understanding. What would you do to get deeper here?

❤️ 1
p-himik21:11:49

I'd try profiling.

hiredman22:11:13

have you run entirely sequentially to see if the runtime is two seconds?

kwladyka22:11:56

do time measure only on SQL query part, here you count more, than just SQL query. Not fair comparison.

kwladyka22:11:26

plus there is network latency anyway

hiredman22:11:13

UUID uses a single SecureRandom, so your threads are contending on getting random bytes from that, you'd hope getting the random bytes would be fast, but I dunno, so maybe try bench marking with the uuids all pre-allocated

hiredman22:11:38

but anyway, you should benchmark a sequential run, and the difference between that and your parallel run will tell you how much overhead you have when doing it in parallel

hiredman22:11:53

where overhead when doing things in parallel is things like contending on shared locks (the SecureRandom case involves calling some synchronized methods), spinning up new threads, etc

nezaj22:11:04

In case it's relevant, (str "SELECT '" (java.util.UUID/randomUUID) "'") was chosen to avoid potential caching (e.g. instead of doing SELECT 1) in my production system I need to concurrently fire several independent queries and I'm trying to tune performance

hiredman22:11:37

https://en.wikipedia.org/wiki/Amdahl's_law might be an interesting read in this context if you are not already familiar with it

devn23:11:09

At what layer would that Postgres query get cached? At the DB?

devn23:11:31

I have a select ‘ok’ as part of a health check and don’t really care about the timings. What is the random uuid supposed to surface performance wise?

devn23:11:12

I’ve been in pg performance tuning mode here and there recently but it’s never been at the level you’re describing, but perhaps it would be interesting! Mostly just reading explain analyze buffers etc plans and checking the stats to figure how to make the planner play nice or to identify a possible index.

devn23:11:36

Between Hikari, rds proxy, Aurora, I dont know how much I’d get from throwing a random uuid in with network jitter and all the indirection, but am here to learn!

oyakushev23:11:44

If I remember correctly, future uses an agent threadpool with an upper bound on threads. Instead of future, try doing (.start (Thread. (fn [] ...)))

hiredman23:11:03

that is incorrect

hiredman23:11:27

there are two agent threadpools an unbounded and a bounded one, futures use the unbounded one

oyakushev23:11:03

Oh, ok. Thought it used the latter 🤷

devn23:11:23

I’ve been confused by that more than once. Ain’t no shame there.

devn23:11:13

of course, the unbounded has its own “is this a good idea for performance” associated with it :)

hiredman00:11:23

the reason I brought up https://en.wikipedia.org/wiki/Amdahl's_law is because it looks like the question basically comes down to "why am I not seeing a linear speed up when I parallelize this?"

hiredman00:11:45

and the commentary explaining the law makes it clear that generally you will never see a linear speed up, and the amount of speed up you see is determined by how much is actually serialized (waiting on locks, other shared resources, etc)

devn00:11:05

I was writing up a crude version of “spending time spawning threads and doing uuid gen…” which is basically that :D

devn00:11:20

I have the smooth brain version lol

hiredman00:11:07

there is a newer thing(from 1993) the Universal Law of Computational Scalability, which uh, if I understand it is sort of https://en.wikipedia.org/wiki/Amdahl's_law but more fine grained, you get other factors to account for other kinds of delays and overhead, where Amdahl's law just has p

hiredman00:11:42

some quick playing around with criterium.core/quick-bench shows creating 1e6 uuids in parallel (no database access at all) is much slower than creating them sequentially

👀 5
nezaj16:11:34

Hey team, wanted to followup: 1. UUIDs and SELECT 1 made no difference The parallel versions both took around ~100ms

(def conn-pool-size 50)
(defn random-q []  [(str "SELECT '" (java.util.UUID/randomUUID) "'")])
(defn same-q [] ["SELECT 1"])

(time
   (->> (range 0 1000)
        (mapv (fn [x]
                (future (next.jdbc.sql/query conn-pool (random-q)))))
        (mapv deref)))

(time
   (->> (range 0 1000)
        (mapv (fn [x]
                (future (next.jdbc.sql/query conn-pool (same-q)))))
        (mapv deref)))
2. Parallel execution was 10x sequential Sequential execution took ~1000ms, so we're seeing 10x improvement with conn pool size of 50. 3. We noticed there wasn't a lift in parallelism when we increased conn pool size. To our surprise, increasing the conn pool size seemed to make performance worse. It seemed like a pool size of 20 did even better. We did 20 rounds of 10,000 queries with pool sizes of 20, 40, and 200 and made a histogram. Code
(comment

  (defn same-q [] ["SELECT 1"])

  (defn run-timer [pool-size]
    (when (bound? #'conn-pool)
      (.close conn-pool))
    (def conn-pool
      (let [pool (next.jdbc.connection/->pool
                  com.zaxxer.hikari.HikariDataSource
                  (assoc (instant.config/get-aurora-config)
                         :maxLifetime (* 10 60 1000)
                         :maximumPoolSize pool-size))]
        (.close (next.jdbc/get-connection pool))
        pool))
    ;; Capture the time without printing
    (let [start-time (System/currentTimeMillis)
          _ (->> (range 0 10000)
                 (mapv (fn [x]
                         (future (next.jdbc.sql/query conn-pool (same-q)))))
                 (mapv deref))
          end-time (System/currentTimeMillis)]
      (- end-time start-time)))

  (defn output-data []
    (let [pool-sizes [20 40 200]
          results (for [p pool-sizes
                        _ (range 0 20)]
                    [p (run-timer p)])]
      results))

  ;; This will return the vector of results
  (def res  (output-data)))
Histogram (attached)

kwladyka16:11:01

size of pool consume server too, each connection is not free

nezaj16:11:28

Question: Why is it that increasing conn pool size in this scenario is not helpful? Is 10x speedup of sequential the best we can do?

kwladyka16:11:29

this is expected to work in that way

kwladyka16:11:07

potentially try better hardware (like increase memory)

oyakushev16:11:19

Do you track the timing distribution on the SQL server as well?

oyakushev16:11:01

Ah, you do, saw it in the initial message.

kwladyka16:11:02

also SQL server configuration can limit operations

oyakushev16:11:13

I would indeed try profiling with some wall-clock profiler like e.g. VisualVM. It might tell you where the threads spend the most time waiting.

kwladyka16:11:31

In first row I would bet on: • SQL server configuration which limit resources or parallel queries • not enough memory / CPU in server or in clj app • I am not sure if poll :maximumPoolSize go up to 50, this is max after all and can be limited by resources under the hood or server reject more

nezaj16:11:08

In terms of specs we are using: Aurora on rds https://instances.vantage.sh/aws/rds/db.t3.medium (2 vCPU, 4GB mem) Clojure on ec2 https://instances.vantage.sh/aws/ec2/t4g.xlarge (4 vCPU, 16GB mem) Now reading through this article on https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing.

nezaj16:11:33

(Appreciate all the engagement team)

doug kirk16:11:27

Is optimizing the parallellization the right thing to optimize? You're optimizing the latency of a request to the database. However, for most any database workloads it's far more rewarding to optimize the queries. Check also the AWS configurations. Login to the EC2 machine and see what the ping time is to the DB host. Trace the route if necessary. Look at VPC configuration attached. There are a lot of knobs on AWS database instances, and a lot of knobs for the network.

nezaj17:11:04

In our case, we have a bunch of very simple queries hitting the DB. We'll look into the AWS configuration. Is there some resource you would recommend?

doug kirk21:11:50

Most of my experience for AWS docs is really via Terraform, which are better organized than AWS docs IMO. Also, I remember reading this article[1] about RDS Postgres vs. Aurora awhile back, and anecdotally we found Aurora to be somewhat slower for our workload than straight RDS Postgres without any special tuning. You'll likely want to look at IOPS tuning. [1] https://www.migops.com/blog/is-aurora-postgresql-really-faster-and-cheaper-than-rds-postgresql-benchmarking/

devn01:11:44

RDS proxy is a thing as well

devn01:11:54

sizing your connection pool is also a weird science that hikari directs based on platter drives and logical cores, but in rds and Aurora land vcpus and ssd storage is a different matter

nezaj18:11:50

Closing loop on thiis, we'll take a pause on our pg investigation to move on to other things, but here's where we've left things for now.

nezaj18:11:03

Appreciate all the engagement!

devn02:11:26

Since you mention RDS sizing having a material impact, what are the query plans that are slow?