├── .gitignore ├── CLIENT_DESIGN.md ├── Dockerfile ├── README.md ├── Taskfile.yaml ├── buf.gen.yaml ├── buf.yaml ├── go.mod ├── go.sum ├── gologger └── gologger.go ├── http_server ├── custom_context.go ├── error_handler.go └── http_server.go ├── k6-perf.js ├── k6-simple.js ├── main.go ├── observability └── internal_http.go ├── proto └── api │ └── v1 │ ├── api.pb.go │ └── api.proto ├── raft ├── epoch_host.go ├── file.go ├── logger_factory.go ├── raft.go └── statemachine.go ├── ring ├── ring.go └── ring_test.go ├── tracing └── tracing.go └── utils ├── env.go ├── errors.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | epoch*.json 4 | _raft 5 | *.pem -------------------------------------------------------------------------------- /CLIENT_DESIGN.md: -------------------------------------------------------------------------------- 1 | # Client Design (WIP) 2 | 3 | 4 | * [Client Design (WIP)](#client-design-wip) 5 | * [Client-aware routing](#client-aware-routing) 6 | * [Choosing a protocol](#choosing-a-protocol) 7 | * [Batching requests](#batching-requests) 8 | * [Notes on Raft](#notes-on-raft) 9 | 10 | 11 | ## Client-aware routing 12 | 13 | To reduce latency, clients should route directly to the leader. 14 | 15 | They may learn about this via the HTTP `/members` route. A client can read this from any node that is part of the cluster. 16 | 17 | (NOT IMPLEMENTED YET) The HTTP interface will respond with a 301 to the correct host. The client should refresh their leader membership and follow the redirect. 18 | 19 | Other interfaces such as gRPC will reject the get with a `ErrNotLeader` error, indicating that the client should refresh membership info from any node and retry. 20 | 21 | ## Choosing a protocol 22 | 23 | While HTTP/3 should be the no-brainer, you will want to test the performance of h2c vs h3 for your client, as h3 is not super widely supported so some community implementations could end up being slower than a good h2c implementation (or on good networks, http/2 is actually shown to be faster). 24 | 25 | Avoid HTTP/1.1 when ever you can, it will have a monstrous negative performance impact. 26 | 27 | ## Batching requests 28 | 29 | (WIP) if you have a client that might be doing many concurrent transactions, batching up their creation on some interval (e.g. every 5ms) and then fetching multiple timestamps from EpicEpoch will dramatically improve the throughput in terms of timestamps/sec that can be generated. 30 | 31 | ## Notes on Raft 32 | 33 | This is expected to run with very low latency, and by default the raft RTT is set very low. 34 | 35 | Dragonboat (the underlying package for Raft) still has some decently high election times, so consider that it can take upwards of 100 RTTs to elect a new leader. In this time requests could time out, so clients must retry. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.1-bullseye as build 2 | 3 | WORKDIR /app 4 | 5 | # START GIT PRIVATE SECTION - delete if not using private packages 6 | ARG GIT_INSTEAD_OF=ssh://git@github.com/ 7 | ARG GO_ARGS="" 8 | 9 | # Need ssh for private packages 10 | RUN mkdir /root/.ssh && echo "# github.com\n|1|ljja8g3oSggsnjO9rsrgs7Udx2s=|I6pPqynzf/0nwAnJ3LQ4n9n6Gc8= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n|1|rFMG6UlqGl4xrNGGKf6FYK56sMU=|bLF794kw2BGoKCjiN696DX+dMh4= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> /root/.ssh/known_hosts 11 | RUN go env -w GOPRIVATE=github.com/ 12 | RUN git config --global url."${GIT_INSTEAD_OF}".insteadOf https://github.com/ 13 | # END GIT PRIVATE SECTION 14 | 15 | COPY go.* /app/ 16 | 17 | RUN --mount=type=cache,target=/go/pkg/mod \ 18 | --mount=type=cache,target=/root/.cache/go-build \ 19 | --mount=type=ssh \ 20 | go mod download 21 | 22 | COPY . . 23 | 24 | RUN --mount=type=cache,target=/go/pkg/mod \ 25 | --mount=type=cache,target=/root/.cache/go-build \ 26 | go build $GO_ARGS -o /app/outbin 27 | 28 | # Need glibc 29 | FROM gcr.io/distroless/base-debian11 30 | 31 | ENTRYPOINT ["/app/outbin"] 32 | COPY --from=build /app/outbin /app/ 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EpicEpoch 2 | 3 | A highly concurrent, low latency, highly available monotonic hybrid timestamp service. 4 | 5 | Used for distributed systems and clients, like distributed transactions. Self-sufficient via Raft. 6 | 7 | 8 | * [EpicEpoch](#epicepoch) 9 | * [Getting started](#getting-started) 10 | * [Configuration (env vars)](#configuration-env-vars) 11 | * [Motivation (Why make this?)](#motivation-why-make-this) 12 | * [Reading the timestamp value](#reading-the-timestamp-value) 13 | * [HTTP endpoints (HTTP/1.1, H2C, HTTP/3 self-signed)](#http-endpoints-http11-h2c-http3-self-signed) 14 | * [Client design](#client-design) 15 | * [Latency and concurrency](#latency-and-concurrency) 16 | * [Latency optimizations](#latency-optimizations) 17 | * [Concurrency optimizations](#concurrency-optimizations) 18 | * [Performance testing (HTTP/1.1)](#performance-testing-http11) 19 | * [Simple test (buffer 10k)](#simple-test-buffer-10k) 20 | * [Performance test (buffer 10k)](#performance-test-buffer-10k) 21 | * [Pushing beyond a single node](#pushing-beyond-a-single-node) 22 | 23 | 24 | ## Getting started 25 | 26 | WIP 27 | 28 | - cmd to run 29 | - data directory `_raft` folder, snapshots at `epoch-{nodeID}.json`, never delete these unless you know what you're doing. 30 | - some info about disk usage (it's quite small) 31 | 32 | Note that the HTTP/3 server will write a `cert.pem` and `key.pem` in the same directory as the binary if they do not already exist. 33 | 34 | Note that currently this is hard-coded to run 3 nodes locally (see https://github.com/danthegoodman1/EpicEpoch/issues/9): 35 | 36 | ``` 37 | 1: "localhost:60001" 38 | 2: "localhost:60002" 39 | 3: "localhost:60003" 40 | ``` 41 | 42 | ## Configuration (env vars) 43 | 44 | Configuration is done through environment variables 45 | 46 | | **ENV VAR** | **Required** | **Default** | **Description** | 47 | |--------------------------|--------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 48 | | DEBUG | no | | Enables debug logging if set to `1` | 49 | | PRETTY | no | | Enabled pretty print logging if set to `1` | 50 | | LOG_TIME_MS | no | | Formats the time as unix milliseconds in logs | 51 | | NODE_ID | yes | `0` (invalid value) | Sets the Raft node ID, must be unique | 52 | | TIMESTAMP_REQUEST_BUFFER | yes | 10000 | Sets the channel buffer length for pending requests. Requests that are blocked when this buffer is full are responded to in random order, unlike requests that are in the buffer. | 53 | | EPOCH_INTERVAL_MS | yes | 100 | The interval at which the Raft leader will increment the epoch (and reset the epoch index). | 54 | | EPOCH_DEADLINE_LIMIT | yes | 100 | How many deadline exceeded errors incrementing the epoch can be tolerated before the system crashes | 55 | | RAFT_ADDR | yes | | The address which raft is exposed | 56 | | HTTP_PORT | yes | 8080 | The address which the HTTP port is exposed (for interfacing with clients) | 57 | 58 | 59 | ## Motivation (Why make this?) 60 | 61 | Time is perhaps the most important thing to a distributed system. 62 | 63 | Without reliable time handling distributed isolation become nearly impossible. You can't guarantee commits happen, or that they happen with the expected isolation. You can't guarantee that your data was replicated. It's a big deal. 64 | 65 | I won't dive in to the details as to why time is so important, or so hard, for distributed systems, but just know that it's the bane of dist sys eng's existence. If we had perfect clocks, then dist systems would be orders of magnitude faster, have far fewer bugs, and be far easier to develop. 66 | 67 | But we don't have perfect clocks, so we have to play some tricks instead like using hybrid timestamps: Combining real time and "fake time" in the form of an incrementing counter that's reset when the real time changes. 68 | 69 | In building my own [Percolator clients](https://github.com/danthegoodman1/Percolators), the last remaining task was to have a global timestamp oracle that could be used to guarantee monotonic hybrid timestamps for transactions. This, as a standalone service, didn't really exist. Plus I thought it would be super fun to make (it was) as I'm arguably obsessed with distributed systems. 70 | 71 | So EpicEpoch (name change possibly pending) was born for that sole purpose: Serve monotonic hybrid timestamps as fast as possible, to as many clients as possible. 72 | 73 | Building a bespoke timestamp oracle service has massive benefits over using something like etcd: performance and latency. 74 | 75 | The system is designed with the sole purpose of serving unique timestamps as fast as possible. It only needs to replicate a single value over Raft, and store 46 bytes to disk as a raft snapshot. It leverages linearizable reads, request collapsing, and an efficient actor model to ensure that it can maximize the available compute to serve timestamps at the highest possible concurrency. It supports HTTP/1.1, HTTP/2 (cleartext), and HTTP/3 to give clients the most performant option they can support. 76 | 77 | Generalized solutions couldn't meet 1% of the performance EpicEpoch can for monotonic hybrid timestamps while also maintain guarantees during failure scenarios (I'm looking at you in particular, Redis). 78 | 79 | This is not considered production ready (at least for anyone but me, or who ever is ready to write code about it). There is still lots of testing, instrumenting, documentation, and more to be done before I'd dare to suggest someone else try it. At a minimum, it can serve as an implementation reference. 80 | 81 | ## Reading the timestamp value 82 | 83 | The timestamp value is 16 bytes, constructed of an 8 byte timestamp (unix nanoseconds) + an 8 byte epoch index. 84 | 85 | This ensures that transactions are always unique and in order, latency and concurrency for serving timestamp requests is maximized, and timestamps can be reversed for time-range queries against your data. 86 | 87 | This also ensures that the request and response are each a single TCP frame. 88 | 89 | ## HTTP endpoints (HTTP/1.1, H2C, HTTP/3 self-signed) 90 | 91 | `/up` exists to check if the HTTP server is running 92 | `/ready` checks to see if the node has joined the cluster and is ready to serve requests 93 | 94 | 95 | `/timestamp` can be used to fetch a unique monotonic 16 byte hybrid timestamp value (this is the one you want to use). 96 | 97 | This will return a 409 if the node is not the leader. Clients should use client-aware routing to update their local address cache if they encounter this (see [CLIENT_DESIGN.md](CLIENT_DESIGN.md)). 98 | 99 | Can use the query param `n` to specify a number >= 1, which will return multiple timestamps that are guaranteed to share the same epoch and have a sequential epoch index. These timestamps are appended to each other, so `n=2` will return a 32 byte body. 100 | 101 | 102 | `/members` returns a JSON in the shape of: 103 | 104 | ```json 105 | { 106 | "leader": { 107 | "nodeID": 1, 108 | "addr": "addr1" 109 | }, 110 | "members": [ 111 | { 112 | "nodeID": 1, 113 | "addr": "addr1" 114 | } 115 | ] 116 | } 117 | ``` 118 | 119 | This is used for client-aware routing. 120 | 121 | ## Client design 122 | 123 | See [CLIENT_DESIGN.md](CLIENT_DESIGN.md) 124 | 125 | ## Latency and concurrency 126 | 127 | Optimizations have been made to reduce the latency and increase concurrency as much as possible, trading concurrency for latency where needed. 128 | 129 | ### Latency optimizations 130 | 131 | Instead of using channels, ring buffers are used where ever practical (with some exceptions due to convenience). [Ring buffers are multiple times faster under high concurrency situations](https://bravenewgeek.com/so-you-wanna-go-fast/). 132 | 133 | For example, all requests queue to have a timestamp generated by putting a request into a ring buffer. The reader agent is poked with a channel (for select convenience with shutdown), fetches the current epoch, reads from the ring buffer to generate timestamps, and responds to the request with a ring buffer provided in the request. 134 | 135 | ### Concurrency optimizations 136 | 137 | The nature of the hybrid timestamp allows concurrency limited only by the epoch interval and a uint64. Within an epoch interval, a monotonic counter is incremented for every request, meaning that we are not bound to the write of raft to serve a request, and we can serve up to the max uint64 requests for a single epoch interval (which should be far faster than any server could serve pending requests). 138 | 139 | As a result the latency ceiling of request time is roughly the latency of a linearizable read through raft + the time to respond to all pending requests. The raft read is amortized across all pending requests. 140 | 141 | To ensure that this node is the leader, a linearizable read across the cluster must take place, but many requests can share this via request collapsing. 142 | 143 | ## Performance testing (HTTP/1.1) 144 | 145 | Using k6 on a 200 core C3D from GCP, running all 3 instances and the test, the following was observed. 146 | 147 | TLDR: Lack of h2c or h3 support from k6, and not tuning the tcp stack or ulimit on the machine greatly reduced potential results. This could easily do 1M+ on the same hardware with more capable clients. 148 | 149 | ### Simple test (buffer 10k) 150 | 151 | 5-7 cores used at peak 152 | 153 | followers using 50-80% cpu 154 | 155 | ``` 156 | scenarios: (100.00%) 1 scenario, 100 max VUs, 2m30s max duration (incl. graceful stop): 157 | * default: Up to 100 looping VUs for 2m0s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) 158 | 159 | 160 | running (0m22.0s), 073/100 VUs, 1385955 complete and 0 interrupted iteration 161 | 162 | ✗ is status 200 163 | ↳ 99% — ✓ 10270369 / ✗ 443 164 | 165 | checks.........................: 99.99% ✓ 10270369 ✗ 443 166 | data_received..................: 1.5 GB 13 MB/s 167 | data_sent......................: 914 MB 7.6 MB/s 168 | http_req_blocked...............: avg=1.42µs min=320ns med=1.1µs max=51.93ms p(90)=1.88µs p(95)=2.23µs 169 | http_req_connecting............: avg=1ns min=0s med=0s max=1.07ms p(90)=0s p(95)=0s 170 | ✓ http_req_duration..............: avg=822.67µs min=132.05µs med=576.33µs max=1s p(90)=1.21ms p(95)=1.78ms 171 | { expected_response:true }...: avg=779.52µs min=132.05µs med=576.33µs max=1s p(90)=1.21ms p(95)=1.78ms 172 | ✓ { staticAsset:yes }..........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 173 | ✓ http_req_failed................: 0.00% ✓ 443 ✗ 10270369 174 | http_req_receiving.............: avg=21.84µs min=5.02µs med=18.35µs max=51.44ms p(90)=25.42µs p(95)=30.34µs 175 | http_req_sending...............: avg=7.32µs min=1.71µs med=5.85µs max=31.67ms p(90)=7.98µs p(95)=9.27µs 176 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 177 | http_req_waiting...............: avg=793.5µs min=122.14µs med=548.77µs max=1s p(90)=1.17ms p(95)=1.73ms 178 | http_reqs......................: 10270812 85589.648371/s 179 | iteration_duration.............: avg=871.13µs min=151.51µs med=621.92µs max=1s p(90)=1.26ms p(95)=1.86ms 180 | iterations.....................: 10270812 85589.648371/s 181 | vus............................: 1 min=1 max=100 182 | vus_max........................: 100 min=100 max=100 183 | 184 | 185 | running (2m00.0s), 000/100 VUs, 10270812 complete and 0 interrupted iterations 186 | ``` 187 | 188 | ### Performance test (buffer 10k) 189 | 190 | _Also tested with 1M buffer, didn't make a difference in this test._ 191 | 192 | 63-70 cores used at peak during the test 193 | 55-65 cores used by the test runner 194 | <7GB of ram used 195 | 196 | followers using 50-80% cpu 197 | 198 | ``` 199 | scenarios: (100.00%) 1 scenario, 10000 max VUs, 4m30s max duration (incl. graceful stop): 200 | * default: Up to 10000 looping VUs for 4m0s over 7 stages (gracefulRampDown: 30s, gracefulStop: 30s) 201 | 202 | 203 | ✗ is status 200 204 | ↳ 99% — ✓ 34288973 / ✗ 61 205 | 206 | checks.........................: 99.99% ✓ 34288973 ✗ 61 207 | data_received..................: 5.0 GB 21 MB/s 208 | data_sent......................: 3.1 GB 13 MB/s 209 | http_req_blocked...............: avg=7.89µs min=320ns med=1.9µs max=221.57ms p(90)=2.71µs p(95)=3.24µs 210 | http_req_connecting............: avg=294ns min=0s med=0s max=123.89ms p(90)=0s p(95)=0s 211 | ✓ http_req_duration..............: avg=16.99ms min=172.36µs med=6.31ms max=1s p(90)=67.13ms p(95)=83.12ms 212 | { expected_response:true }...: avg=16.99ms min=172.36µs med=6.31ms max=1s p(90)=67.13ms p(95)=83.12ms 213 | ✓ { staticAsset:yes }..........: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 214 | ✓ http_req_failed................: 0.00% ✓ 61 ✗ 34288973 215 | http_req_receiving.............: avg=147.78µs min=5.3µs med=16.97µs max=215.76ms p(90)=27.1µs p(95)=41.97µs 216 | http_req_sending...............: avg=92.2µs min=1.76µs med=6.3µs max=222.21ms p(90)=9.36µs p(95)=20.79µs 217 | http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s 218 | http_req_waiting...............: avg=16.75ms min=149.66µs med=6.22ms max=1s p(90)=66.73ms p(95)=82.74ms 219 | http_reqs......................: 34289034 142866.904307/s 220 | iteration_duration.............: avg=30.51ms min=205.04µs med=19.54ms max=1s p(90)=87.72ms p(95)=108.99ms 221 | iterations.....................: 34289034 142866.904307/s 222 | vus............................: 6 min=6 max=10000 223 | vus_max........................: 10000 min=10000 max=10000 224 | 225 | 226 | running (4m00.0s), 00000/10000 VUs, 34289034 complete and 0 interrupted iterations 227 | ``` 228 | 229 | It seems like the request completion rate did not grow much past 300 vus, so considering that we ran up to 10k vus this is HTTP/1.1 rearing it's less performant head. 230 | 231 | Some log output of the duration between reading from raft and writing to the pending request channels with incremented hybrid timestamps: 232 | ``` 233 | raft.(*EpochHost).generateTimestamps() > Served 27 requests in 14.72µs 234 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 5.75µs 235 | raft.(*EpochHost).generateTimestamps() > Served 6 requests in 5.9µs 236 | raft.(*EpochHost).generateTimestamps() > Served 2 requests in 6.52µs 237 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 1.78µs 238 | raft.(*EpochHost).generateTimestamps() > Served 67 requests in 63.51µs 239 | raft.(*EpochHost).generateTimestamps() > Served 87 requests in 83.8µs 240 | raft.(*EpochHost).generateTimestamps() > Served 212 requests in 2.81964ms 241 | raft.(*EpochHost).generateTimestamps() > Served 7139 requests in 6.427489ms 242 | raft.(*EpochHost).generateTimestamps() > Served 26 requests in 497.96µs 243 | raft.(*EpochHost).generateTimestamps() > Served 2034 requests in 1.50941ms 244 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 4.66µs 245 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 3.84µs 246 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 1.55µs 247 | raft.(*EpochHost).generateTimestamps() > Served 1 requests in 1.35µs 248 | ``` 249 | 250 | Combined with: 251 | ``` 252 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 152.25µs 253 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 274.583µs 254 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 234.166µs 255 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 191.709µs 256 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 188.583µs 257 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 196.792µs 258 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 203.333µs 259 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 145.833µs 260 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 169.792µs 261 | http_server.(*HTTPServer).GetTimestamp() > handled request (HTTP/1.1) in 226.542µs 262 | ``` 263 | 264 | It's quite clear that either k6 (the load tester), or it's lack of h2c/h3 support is to blame, considering the known performance of the echo framework, the fact that it used ~70% of available CPU on the machine, and the logs above. 265 | 266 | There would also likely be massive performance gains by increaseing the ulimit and tuning the tcp stack. 267 | 268 | ## Pushing beyond a single node 269 | 270 | With Percolator-style transactions, reliable ordered time is needed. We currently get around this by electing a single node, and incrementing hybrid timestamp intervals within a logical time internal. 271 | 272 | Restricting this to a single node removes the issue of clock-drift, causing one timestamp oracle to think it's further/behind another. 273 | 274 | But this introduces a scale problem: What if we need to perform 100M txn/s? 275 | 276 | Well the simple solution is just client batching. You can trivially exceed 1B timestamps/s with that. 277 | 278 | We can achieve multi-node timestamp oracles while preserving the same semantics, just by changing clients to be a bit smarter. 279 | 280 | We can actually drop the hybrid timestamp, and adjust the time interval to be in-line with our maximum clock drift. Then, clients finding an equivalent timestamp or higher, consider that qualification for a retry: They wait out the uncertainty. 281 | 282 | With the inclusion of atomic clocks in cloud datacenters (AWS, GCP, etc.), time drift between nodes, even across datacenters, [is typically under 100 microseconds](https://aws.amazon.com/blogs/compute/its-about-time-microsecond-accurate-clocks-on-amazon-ec2-instances/). 283 | 284 | Now we don't want to reduce our intervals that far, 10ms is a very reasonable timescale for us to reduce Raft activity, while also reducing the probability of time-contention in transactions. If you have high-contention transactions, check out [Chardonnay](https://www.usenix.org/conference/osdi23/presentation/eldeeb) as an alternative to Percolator. But you have to guarantee that this is the max drift. 285 | 286 | Because of the guaranteed time drift bounds, we can even use this in a globally distributed setting: As long as we have acceptable drift because of atomic clocks, we can truncate time down to the nearest interval. 287 | 288 | This is very similar to how Spanner handles transactions, in effect, waiting out the uncertainty. Last I checked, they used 7ms as their time interval. 289 | 290 | EpicEpoch doesn't support distributed time intervals at the moment, however it (or a derivative project) might in the future. -------------------------------------------------------------------------------- /Taskfile.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | dotenv: ['.env'] 4 | 5 | vars: 6 | sql_migrate_version: v1.5.2 7 | sql_c_version: v1.19.1 8 | 9 | env: 10 | DOCKER_BUILDKIT: "1" 11 | 12 | tasks: 13 | default: 14 | - go run . 15 | 16 | single-test: 17 | cmds: 18 | - go test --count=1 -v {{.CLI_ARGS}} 19 | 20 | gen: 21 | cmds: 22 | - buf generate -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | managed: 3 | enabled: true 4 | override: 5 | - file_option: go_package_prefix 6 | value: github.com/bufbuild/buf-tour/gen 7 | plugins: 8 | - remote: buf.build/protocolbuffers/go 9 | out: proto 10 | opt: paths=source_relative 11 | # - remote: buf.build/connectrpc/go 12 | # out: gen 13 | # opt: paths=source_relative 14 | inputs: 15 | - directory: proto 16 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | version: v2 2 | modules: 3 | - path: proto 4 | lint: 5 | use: 6 | - DEFAULT 7 | breaking: 8 | use: 9 | - FILE 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danthegoodman1/EpicEpoch 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/UltimateTournament/backoff/v4 v4.2.1 7 | github.com/cockroachdb/cockroach-go/v2 v2.3.5 8 | github.com/go-playground/validator/v10 v10.11.1 9 | github.com/google/uuid v1.3.1 10 | github.com/jackc/pgtype v1.12.0 11 | github.com/jackc/pgx/v5 v5.4.3 12 | github.com/labstack/echo/v4 v4.11.1 13 | github.com/lni/dragonboat/v3 v3.3.8 14 | github.com/matoous/go-nanoid/v2 v2.0.0 15 | github.com/prometheus/client_golang v1.19.1 16 | github.com/quic-go/quic-go v0.45.1 17 | github.com/rs/zerolog v1.29.1 18 | github.com/segmentio/ksuid v1.0.4 19 | github.com/stretchr/testify v1.8.4 20 | github.com/uber-go/tally/v4 v4.1.7 21 | go.opentelemetry.io/otel v1.21.0 22 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 23 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 24 | go.opentelemetry.io/otel/sdk v1.21.0 25 | go.opentelemetry.io/otel/trace v1.21.0 26 | golang.org/x/net v0.25.0 27 | google.golang.org/protobuf v1.33.0 28 | ) 29 | 30 | require ( 31 | github.com/VictoriaMetrics/metrics v1.6.2 // indirect 32 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 35 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 36 | github.com/cockroachdb/errors v1.7.5 // indirect 37 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect 38 | github.com/cockroachdb/pebble v0.0.0-20210331181633-27fc006b8bfb // indirect 39 | github.com/cockroachdb/redact v1.0.6 // indirect 40 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect 41 | github.com/davecgh/go-spew v1.1.1 // indirect 42 | github.com/go-logr/logr v1.3.0 // indirect 43 | github.com/go-logr/stdr v1.2.2 // indirect 44 | github.com/go-playground/locales v0.14.0 // indirect 45 | github.com/go-playground/universal-translator v0.18.0 // indirect 46 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 47 | github.com/gogo/protobuf v1.3.1 // indirect 48 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 49 | github.com/golang/protobuf v1.5.3 // indirect 50 | github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3 // indirect 51 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 52 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 53 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 54 | github.com/hashicorp/errwrap v1.0.0 // indirect 55 | github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 56 | github.com/hashicorp/go-msgpack v0.5.3 // indirect 57 | github.com/hashicorp/go-multierror v1.0.0 // indirect 58 | github.com/hashicorp/go-sockaddr v1.0.0 // indirect 59 | github.com/hashicorp/golang-lru v0.5.0 // indirect 60 | github.com/hashicorp/memberlist v0.2.2 // indirect 61 | github.com/jackc/pgio v1.0.0 // indirect 62 | github.com/jackc/pgpassfile v1.0.0 // indirect 63 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 64 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 65 | github.com/jackc/puddle/v2 v2.2.1 // indirect 66 | github.com/juju/ratelimit v1.0.2-0.20191002062651-f60b32039441 // indirect 67 | github.com/kr/pretty v0.3.1 // indirect 68 | github.com/kr/text v0.2.0 // indirect 69 | github.com/labstack/gommon v0.4.0 // indirect 70 | github.com/leodido/go-urn v1.2.1 // indirect 71 | github.com/lni/goutils v1.3.0 // indirect 72 | github.com/mattn/go-colorable v0.1.13 // indirect 73 | github.com/mattn/go-isatty v0.0.19 // indirect 74 | github.com/miekg/dns v1.1.26 // indirect 75 | github.com/onsi/ginkgo/v2 v2.9.5 // indirect 76 | github.com/pkg/errors v0.9.1 // indirect 77 | github.com/pmezard/go-difflib v1.0.0 // indirect 78 | github.com/prometheus/client_model v0.5.0 // indirect 79 | github.com/prometheus/common v0.48.0 // indirect 80 | github.com/prometheus/procfs v0.12.0 // indirect 81 | github.com/quic-go/qpack v0.4.0 // indirect 82 | github.com/rogpeppe/go-internal v1.10.0 // indirect 83 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect 84 | github.com/twmb/murmur3 v1.1.5 // indirect 85 | github.com/valyala/bytebufferpool v1.0.0 // indirect 86 | github.com/valyala/fastrand v1.0.0 // indirect 87 | github.com/valyala/fasttemplate v1.2.2 // indirect 88 | github.com/valyala/histogram v1.0.1 // indirect 89 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect 90 | go.opentelemetry.io/otel/metric v1.21.0 // indirect 91 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 92 | go.uber.org/atomic v1.10.0 // indirect 93 | go.uber.org/mock v0.4.0 // indirect 94 | golang.org/x/crypto v0.23.0 // indirect 95 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect 96 | golang.org/x/mod v0.17.0 // indirect 97 | golang.org/x/sync v0.7.0 // indirect 98 | golang.org/x/sys v0.20.0 // indirect 99 | golang.org/x/text v0.15.0 // indirect 100 | golang.org/x/time v0.5.0 // indirect 101 | golang.org/x/tools v0.21.0 // indirect 102 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect 103 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 104 | google.golang.org/grpc v1.59.0 // indirect 105 | gopkg.in/yaml.v3 v3.0.1 // indirect 106 | ) 107 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 4 | github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 7 | github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= 8 | github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= 9 | github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= 10 | github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= 11 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 12 | github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= 13 | github.com/UltimateTournament/backoff/v4 v4.2.1 h1:3qmPcFjNOwjlmIGivXnDXt/w5DLidixtnXcwkvA9+ps= 14 | github.com/UltimateTournament/backoff/v4 v4.2.1/go.mod h1:Ch9kw9v89oy8lo6jaxSaoBg9jV3kC8oZFg68Upmslig= 15 | github.com/VictoriaMetrics/metrics v1.6.2 h1:VMe8c8ZBPgNVZkPoT06LsoU2nb+8e7iPaOWbVRNhxjo= 16 | github.com/VictoriaMetrics/metrics v1.6.2/go.mod h1:LU2j9qq7xqZYXz8tF3/RQnB2z2MbZms5TDiIg9/NHiQ= 17 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 18 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 19 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 20 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 21 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 22 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 23 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 24 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= 25 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 26 | github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 27 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 28 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 29 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 30 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 31 | github.com/cactus/go-statsd-client/v5 v5.0.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY= 32 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 33 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 34 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 35 | github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 36 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 37 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 38 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 39 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 40 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 41 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 42 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 43 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 44 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 45 | github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0= 46 | github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= 47 | github.com/cockroachdb/datadriven v1.0.0/go.mod h1:5Ib8Meh+jk1RlHIXej6Pzevx/NLlNvQB9pmSBZErGA4= 48 | github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= 49 | github.com/cockroachdb/errors v1.6.1/go.mod h1:tm6FTP5G81vwJ5lC0SizQo374JNCOPrHyXGitRJoDqM= 50 | github.com/cockroachdb/errors v1.7.5 h1:ptyO1BLW+sBxwBTSKJfS6kGzYCVKhI7MyBhoXAnPIKM= 51 | github.com/cockroachdb/errors v1.7.5/go.mod h1:m/IWRCPXYZ6TvLLDuC0kfLR1pp/+BiZ0h16WHaBMRMM= 52 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f h1:o/kfcElHqOiXqcou5a3rIlMc7oJbMQkeLk0VQJ7zgqY= 53 | github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= 54 | github.com/cockroachdb/pebble v0.0.0-20210331181633-27fc006b8bfb h1:dqFirML/6RMDwkge7Tqf33qE0ORbF6rRJOLjCmmwTNg= 55 | github.com/cockroachdb/pebble v0.0.0-20210331181633-27fc006b8bfb/go.mod h1:hU7vhtrqonEphNF+xt8/lHdaBprxmV1h8BOGrd9XwmQ= 56 | github.com/cockroachdb/redact v0.0.0-20200622112456-cd282804bbd3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 57 | github.com/cockroachdb/redact v1.0.6 h1:W34uRRyNR4dlZFd0MibhNELsZSgMkl52uRV/tA1xToY= 58 | github.com/cockroachdb/redact v1.0.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 59 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 h1:IKgmqgMQlVJIZj19CdocBeSfSaiCbEBZGKODaixqtHM= 60 | github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2/go.mod h1:8BT+cPK6xvFOcRlk0R8eg+OTkcqI6baNH4xAkpiYVvQ= 61 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 62 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 63 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 64 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 65 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 66 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 67 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 68 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 69 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 70 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 71 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 72 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 73 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 74 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 75 | github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= 76 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 77 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 78 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 79 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= 80 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 81 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 82 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 83 | github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= 84 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 85 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 86 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= 87 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 88 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 89 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 90 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 91 | github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= 92 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 93 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 94 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 95 | github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= 96 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 97 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 98 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 99 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 100 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 101 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 102 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 103 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 104 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 105 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 106 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 107 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 108 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 109 | github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= 110 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 111 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 112 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 113 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 114 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 115 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 116 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 117 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 118 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 119 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 120 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 121 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 122 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 123 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 124 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 125 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 126 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 127 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 128 | github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 129 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 130 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 131 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 132 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 133 | github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM= 134 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 135 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 136 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 137 | github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= 138 | github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= 139 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 140 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 141 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 142 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 143 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 144 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 145 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 146 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 147 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 148 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 149 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 150 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 151 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 152 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 153 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 154 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 155 | github.com/golang/snappy v0.0.2-0.20190904063534-ff6b7dc882cf/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 156 | github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3 h1:ur2rms48b3Ep1dxh7aUV2FZEQ8jEVO2F6ILKx8ofkAg= 157 | github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 158 | github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 159 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= 160 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 161 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 162 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 163 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 164 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 165 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 166 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 167 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 168 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 169 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 170 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 171 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 172 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 173 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 174 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 175 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 176 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 177 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 178 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= 179 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= 180 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 181 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 182 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 183 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 184 | github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= 185 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 186 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 187 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 188 | github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= 189 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 190 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 191 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 192 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 193 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 194 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 195 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 196 | github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= 197 | github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= 198 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 199 | github.com/hydrogen18/memlistener v0.0.0-20141126152155-54553eb933fb/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE= 200 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 201 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 202 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 203 | github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= 204 | github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= 205 | github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= 206 | github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= 207 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 208 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 209 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 210 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 211 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 212 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 213 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 214 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 215 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 216 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 217 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 218 | github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= 219 | github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 220 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 221 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 222 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 223 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 224 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 225 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 226 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 227 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 228 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 229 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 230 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 231 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 232 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 233 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 234 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 235 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 236 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 237 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 238 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 239 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 240 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 241 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 242 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 243 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 244 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 245 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 246 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 247 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 248 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 249 | github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= 250 | github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= 251 | github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= 252 | github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 253 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 254 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 255 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 256 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 257 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 258 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 259 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 260 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 261 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 262 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 263 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 264 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 265 | github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 266 | github.com/juju/ratelimit v1.0.2-0.20191002062651-f60b32039441 h1:b5Jqi7ir58EzfeZDyp7OSYQG/IVgyY4JWfHuJUF2AZI= 267 | github.com/juju/ratelimit v1.0.2-0.20191002062651-f60b32039441/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= 268 | github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 269 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 270 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 271 | github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= 272 | github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk= 273 | github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= 274 | github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= 275 | github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= 276 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 277 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 278 | github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 279 | github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 280 | github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 281 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 282 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 283 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 284 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 285 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 286 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 287 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 288 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 289 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 290 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 291 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 292 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 293 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 294 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 295 | github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= 296 | github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= 297 | github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 298 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 299 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 300 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 301 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 302 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 303 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 304 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 305 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 306 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 307 | github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= 308 | github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 309 | github.com/lni/dragonboat/v3 v3.3.8 h1:rcBfKT6i+73dFbye0zJC2aZdtYMcyAfWXLLi0J59npY= 310 | github.com/lni/dragonboat/v3 v3.3.8/go.mod h1:TPFbFA4HaFi9N9Ijj1n1japvv/45TKeTy+uy/W+V44c= 311 | github.com/lni/goutils v1.3.0 h1:oBhV7Z5DjNWbcy/c3fFj6qo4SnHcpyTY28qfKvLy6UM= 312 | github.com/lni/goutils v1.3.0/go.mod h1:PUPtBAnZlRPUKWUXCsYkIRWubJbtNHpTAee0sczhlf4= 313 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 314 | github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= 315 | github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= 316 | github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= 317 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 318 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 319 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 320 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 321 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 322 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 323 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 324 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 325 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 326 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 327 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 328 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 329 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 330 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 331 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 332 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 333 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 334 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 335 | github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg= 336 | github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= 337 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 338 | github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= 339 | github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 340 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 341 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 342 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 343 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 344 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 345 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 346 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 347 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 348 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 349 | github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM= 350 | github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4= 351 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 352 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 353 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 354 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 355 | github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= 356 | github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= 357 | github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= 358 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 359 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 360 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 361 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 362 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= 363 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 364 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 365 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 366 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 367 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 368 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 369 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 370 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 371 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 372 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 373 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 374 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 375 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 376 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 377 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 378 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 379 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 380 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 381 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 382 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 383 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 384 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 385 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 386 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 387 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 388 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 389 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 390 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 391 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 392 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 393 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 394 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 395 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 396 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 397 | github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= 398 | github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= 399 | github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA= 400 | github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= 401 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 402 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 403 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 404 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 405 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 406 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 407 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 408 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 409 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 410 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 411 | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= 412 | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 413 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 414 | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 415 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 416 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 417 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= 418 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 419 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 420 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 421 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 422 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 423 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 424 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 425 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 426 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 427 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 428 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 429 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 430 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 431 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 432 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 433 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 434 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 435 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 436 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 437 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 438 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 439 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 440 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 441 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 442 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 443 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 444 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 445 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 446 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 447 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 448 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 449 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 450 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 451 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 452 | github.com/twmb/murmur3 v1.1.5 h1:i9OLS9fkuLzBXjt6dptlAEyk58fJsSTXbRg3SgVyqgk= 453 | github.com/twmb/murmur3 v1.1.5/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= 454 | github.com/uber-go/tally/v4 v4.1.7 h1:YiKvvMKCCXlCKXI0i1hVk+xda8YxdIpjeFXohpvn8Zo= 455 | github.com/uber-go/tally/v4 v4.1.7/go.mod h1:pPR56rjthjtLB8xQlEx2I1VwAwRGCh/i4xMUcmG+6z4= 456 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 457 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 458 | github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= 459 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 460 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 461 | github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= 462 | github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI= 463 | github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 464 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 465 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 466 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 467 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 468 | github.com/valyala/histogram v1.0.1 h1:FzA7n2Tz/wKRMejgu3PV1vw3htAklTjjuoI6z3d4KDg= 469 | github.com/valyala/histogram v1.0.1/go.mod h1:lQy0xA4wUz2+IUnf97SivorsJIp8FxsnRd6x25q7Mto= 470 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 471 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 472 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 473 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 474 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 475 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 476 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 477 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 478 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 479 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 480 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 481 | go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 482 | go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 483 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= 484 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= 485 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= 486 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= 487 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= 488 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= 489 | go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 490 | go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 491 | go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= 492 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 493 | go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 494 | go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 495 | go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= 496 | go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= 497 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 498 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 499 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 500 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 501 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 502 | go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= 503 | go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 504 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 505 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 506 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 507 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 508 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 509 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 510 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 511 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 512 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 513 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 514 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 515 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 516 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 517 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 518 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 519 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 520 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 521 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 522 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 523 | golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 524 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 525 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 526 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 527 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 528 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 529 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 530 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 531 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 532 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 533 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 534 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 535 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 536 | golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= 537 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 538 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 539 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 540 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 541 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 542 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 543 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 544 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 545 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 546 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 547 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 548 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 549 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 550 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 551 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 552 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 553 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 554 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 555 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 556 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 557 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 558 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 559 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 560 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 561 | golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 562 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 563 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 564 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 565 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 566 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 567 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 568 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 569 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 570 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 571 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 572 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 573 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 574 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 575 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 576 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 577 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 578 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 579 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 580 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 581 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 582 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 583 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 584 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 585 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 586 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 587 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 588 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 589 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 590 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 591 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 592 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 593 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 594 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 595 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 596 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 597 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 598 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 599 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 600 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 601 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 602 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 603 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 604 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 605 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 606 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 607 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 608 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 609 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 610 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 611 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 612 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 613 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 614 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 615 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 616 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 617 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 618 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 619 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 620 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 621 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 622 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 623 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 624 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 625 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 626 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 627 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 628 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 629 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 630 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 631 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 632 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 633 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 634 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 635 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 636 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 637 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 638 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 639 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 640 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 641 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 642 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 643 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 644 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 645 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 646 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 647 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 648 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 649 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 650 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 651 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 652 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 653 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 654 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 655 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 656 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 657 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 658 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 659 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 660 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 661 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 662 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 663 | golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 664 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 665 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 666 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 667 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 668 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 669 | golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 670 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 671 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 672 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 673 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 674 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 675 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 676 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 677 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= 678 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 679 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 680 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 681 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 682 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 683 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 684 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 685 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 686 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 687 | google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 688 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 689 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 690 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= 691 | google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= 692 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= 693 | google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= 694 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 695 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 696 | google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 697 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 698 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 699 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 700 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 701 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 702 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 703 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 704 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 705 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 706 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 707 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 708 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 709 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 710 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 711 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 712 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 713 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 714 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 715 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 716 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 717 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 718 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 719 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 720 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 721 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 722 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 723 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 724 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 725 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 726 | gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= 727 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 728 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 729 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 730 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 731 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 732 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 733 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 734 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 735 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 736 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 737 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 738 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 739 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 740 | -------------------------------------------------------------------------------- /gologger/gologger.go: -------------------------------------------------------------------------------- 1 | package gologger 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | type ctxKey string 16 | 17 | const ReqIDKey ctxKey = "reqID" 18 | 19 | func init() { 20 | l := NewLogger() 21 | zerolog.DefaultContextLogger = &l 22 | zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { 23 | function := "" 24 | fun := runtime.FuncForPC(pc) 25 | if fun != nil { 26 | funName := fun.Name() 27 | slash := strings.LastIndex(funName, "/") 28 | if slash > 0 { 29 | funName = funName[slash+1:] 30 | } 31 | function = " " + funName + "()" 32 | } 33 | return file + ":" + strconv.Itoa(line) + function 34 | } 35 | } 36 | 37 | func GetEnvOrDefault(env, defaultVal string) string { 38 | e := os.Getenv(env) 39 | if e == "" { 40 | return defaultVal 41 | } else { 42 | return e 43 | } 44 | } 45 | 46 | // Makes context.Canceled errors a warn (for when people abandon requests) 47 | func LvlForErr(err error) zerolog.Level { 48 | if errors.Is(err, context.Canceled) { 49 | return zerolog.WarnLevel 50 | } 51 | return zerolog.ErrorLevel 52 | } 53 | 54 | func NewLogger() zerolog.Logger { 55 | if os.Getenv("LOG_TIME_MS") == "1" { 56 | // Log with milliseconds 57 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 58 | } else { 59 | zerolog.TimeFieldFormat = time.RFC3339Nano 60 | } 61 | 62 | zerolog.LevelFieldName = GetEnvOrDefault("LOG_LEVEL_KEY", "level") 63 | 64 | zerolog.TimestampFieldName = "time" 65 | 66 | logger := zerolog.New(os.Stdout).With().Timestamp().Logger() 67 | 68 | logger = logger.Hook(CallerHook{}) 69 | 70 | if os.Getenv("PRETTY") == "1" { 71 | logger = logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 72 | } 73 | if os.Getenv("TRACE") == "1" { 74 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 75 | } else if os.Getenv("DEBUG") == "1" { 76 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 77 | } else { 78 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 79 | } 80 | 81 | return logger 82 | } 83 | 84 | type CallerHook struct{} 85 | 86 | func (h CallerHook) Run(e *zerolog.Event, _ zerolog.Level, _ string) { 87 | e.Caller(3) 88 | } 89 | -------------------------------------------------------------------------------- /http_server/custom_context.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/danthegoodman1/EpicEpoch/gologger" 9 | "github.com/google/uuid" 10 | "github.com/labstack/echo/v4" 11 | "github.com/rs/zerolog" 12 | ) 13 | 14 | type CustomContext struct { 15 | echo.Context 16 | RequestID string 17 | UserID string 18 | } 19 | 20 | func CreateReqContext(next echo.HandlerFunc) echo.HandlerFunc { 21 | return func(c echo.Context) error { 22 | reqID := uuid.NewString() 23 | ctx := context.WithValue(c.Request().Context(), gologger.ReqIDKey, reqID) 24 | ctx = logger.WithContext(ctx) 25 | c.SetRequest(c.Request().WithContext(ctx)) 26 | logger := zerolog.Ctx(ctx) 27 | logger.UpdateContext(func(c zerolog.Context) zerolog.Context { 28 | return c.Str("reqID", reqID) 29 | }) 30 | cc := &CustomContext{ 31 | Context: c, 32 | RequestID: reqID, 33 | } 34 | return next(cc) 35 | } 36 | } 37 | 38 | // Casts to custom context for the handler, so this doesn't have to be done per handler 39 | func ccHandler(h func(*CustomContext) error) echo.HandlerFunc { 40 | // TODO: Include the path? 41 | return func(c echo.Context) error { 42 | return h(c.(*CustomContext)) 43 | } 44 | } 45 | 46 | func (c *CustomContext) internalErrorMessage() string { 47 | return "api error, request id: " + c.RequestID 48 | } 49 | 50 | func (c *CustomContext) InternalError(err error, msg string) error { 51 | if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 52 | zerolog.Ctx(c.Request().Context()).Warn().CallerSkipFrame(1).Msg(err.Error()) 53 | } else { 54 | zerolog.Ctx(c.Request().Context()).Error().CallerSkipFrame(1).Err(err).Msg(msg) 55 | } 56 | return c.String(http.StatusInternalServerError, c.internalErrorMessage()) 57 | } 58 | -------------------------------------------------------------------------------- /http_server/error_handler.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "errors" 5 | "github.com/labstack/echo/v4" 6 | "github.com/rs/zerolog" 7 | "net/http" 8 | ) 9 | 10 | func customHTTPErrorHandler(err error, c echo.Context) { 11 | var he *echo.HTTPError 12 | if errors.As(err, &he) { 13 | c.String(he.Code, he.Message.(string)) 14 | return 15 | } 16 | 17 | logger := zerolog.Ctx(c.Request().Context()) 18 | logger.Error().Err(err).Msg("unhandled api error") 19 | 20 | c.String(http.StatusInternalServerError, "Something went wrong internally, an error has been logged") 21 | } 22 | -------------------------------------------------------------------------------- /http_server/http_server.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "github.com/danthegoodman1/EpicEpoch/raft" 15 | "github.com/quic-go/quic-go/http3" 16 | "math/big" 17 | "net" 18 | "net/http" 19 | "os" 20 | "strconv" 21 | "time" 22 | 23 | "github.com/danthegoodman1/EpicEpoch/gologger" 24 | "github.com/danthegoodman1/EpicEpoch/utils" 25 | "github.com/go-playground/validator/v10" 26 | "github.com/labstack/echo/v4" 27 | "github.com/labstack/echo/v4/middleware" 28 | "github.com/rs/zerolog" 29 | "golang.org/x/net/http2" 30 | ) 31 | 32 | var logger = gologger.NewLogger() 33 | 34 | type HTTPServer struct { 35 | Echo *echo.Echo 36 | EpochHost *raft.EpochHost 37 | quicServer *http3.Server 38 | } 39 | 40 | type CustomValidator struct { 41 | validator *validator.Validate 42 | } 43 | 44 | func StartHTTPServer(epochHost *raft.EpochHost) *HTTPServer { 45 | listener, err := net.Listen("tcp", fmt.Sprintf(":%s", utils.GetEnvOrDefault("HTTP_PORT", "8080"))) 46 | if err != nil { 47 | logger.Error().Err(err).Msg("error creating tcp listener, exiting") 48 | os.Exit(1) 49 | } 50 | s := &HTTPServer{ 51 | Echo: echo.New(), 52 | EpochHost: epochHost, 53 | } 54 | s.Echo.HideBanner = true 55 | s.Echo.HidePort = true 56 | s.Echo.JSONSerializer = &utils.NoEscapeJSONSerializer{} 57 | s.Echo.HTTPErrorHandler = customHTTPErrorHandler 58 | 59 | s.Echo.Use(CreateReqContext) 60 | s.Echo.Use(LoggerMiddleware) 61 | s.Echo.Use(middleware.CORS()) 62 | s.Echo.Validator = &CustomValidator{validator: validator.New()} 63 | 64 | s.Echo.GET("/up", s.UpCheck) 65 | s.Echo.GET("/ready", s.ReadyCheck) 66 | s.Echo.GET("/timestamp", s.GetTimestamp) 67 | s.Echo.GET("/membership", s.GetMembership) 68 | 69 | s.Echo.Listener = listener 70 | go func() { 71 | logger.Info().Msg("starting h2c server on " + listener.Addr().String()) 72 | err := s.Echo.StartH2CServer("", &http2.Server{}) 73 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 74 | logger.Error().Err(err).Msg("failed to start h2c server, exiting") 75 | os.Exit(1) 76 | } 77 | }() 78 | 79 | // Start http/3 server 80 | go func() { 81 | tlsCert, err := generateTLSCert() 82 | if err != nil { 83 | logger.Fatal().Err(err).Msg("failed to generate self-signed cert") 84 | } 85 | 86 | // TLS configuration 87 | tlsConfig := &tls.Config{ 88 | Certificates: []tls.Certificate{tlsCert}, 89 | NextProtos: []string{"h3"}, 90 | } 91 | 92 | // Create HTTP/3 server 93 | s.quicServer = &http3.Server{ 94 | Addr: listener.Addr().String(), 95 | Handler: s.Echo, 96 | TLSConfig: tlsConfig, 97 | } 98 | 99 | logger.Info().Msg("starting h3 server on " + listener.Addr().String()) 100 | err = s.quicServer.ListenAndServe() 101 | 102 | // Start the server 103 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 104 | logger.Error().Err(err).Msg("failed to start h2c server, exiting") 105 | os.Exit(1) 106 | } 107 | }() 108 | 109 | return s 110 | } 111 | 112 | func (cv *CustomValidator) Validate(i interface{}) error { 113 | if err := cv.validator.Struct(i); err != nil { 114 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 115 | } 116 | return nil 117 | } 118 | 119 | func ValidateRequest(c echo.Context, s interface{}) error { 120 | if err := c.Bind(s); err != nil { 121 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 122 | } 123 | // needed because POST doesn't have query param binding (https://echo.labstack.com/docs/binding#multiple-sources) 124 | if err := (&echo.DefaultBinder{}).BindQueryParams(c, s); err != nil { 125 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 126 | } 127 | if err := c.Validate(s); err != nil { 128 | return err 129 | } 130 | return nil 131 | } 132 | 133 | // UpCheck is just whether the HTTP server is running, 134 | // not necessarily that it's able to serve requests 135 | func (s *HTTPServer) UpCheck(c echo.Context) error { 136 | return c.String(http.StatusOK, "ok") 137 | } 138 | 139 | // ReadyCheck checks whether everything is ready to start serving requests 140 | func (s *HTTPServer) ReadyCheck(c echo.Context) error { 141 | ctx := c.Request().Context() 142 | logger := zerolog.Ctx(ctx) 143 | // Verify that raft leadership information is available 144 | leader, available, err := s.EpochHost.GetLeader() 145 | if err != nil { 146 | return fmt.Errorf("error in NodeHost.GetLeaderID: %w", err) 147 | } 148 | 149 | if !available { 150 | return c.String(http.StatusInternalServerError, "raft leadership not ready") 151 | } 152 | 153 | if leader == utils.NodeID { 154 | logger.Debug().Msgf("Is leader (%d)", leader) 155 | } 156 | 157 | return c.String(http.StatusOK, fmt.Sprintf("leader=%d nodeID=%d raftAvailable=%t\n", leader, utils.NodeID, available)) 158 | } 159 | 160 | func (s *HTTPServer) GetTimestamp(c echo.Context) error { 161 | ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second) 162 | defer cancel() 163 | // Verify that this is the raft leader 164 | leader, available, err := s.EpochHost.GetLeader() 165 | if err != nil { 166 | return fmt.Errorf("error in NodeHost.GetLeaderID: %w", err) 167 | } 168 | 169 | if !available { 170 | return c.String(http.StatusInternalServerError, "raft leadership not ready") 171 | } 172 | 173 | if leader != utils.NodeID { 174 | return c.String(http.StatusConflict, fmt.Sprintf("node (%d) is not the leader (%d)", utils.NodeID, leader)) 175 | } 176 | 177 | // Get one or more timestamps 178 | count := 1 179 | if n := c.QueryParam("n"); n != "" { 180 | count, err = strconv.Atoi(n) 181 | if err != nil || count < 1 { 182 | return c.String(http.StatusBadRequest, fmt.Sprintf("invalid n param, must be a number >= 1 if provided")) 183 | } 184 | } 185 | 186 | payload, err := s.EpochHost.GetUniqueTimestamp(ctx, count) 187 | if err != nil { 188 | return fmt.Errorf("error in EpochHost.GetUniqueTimestamp: %w", err) 189 | } 190 | 191 | return c.Blob(http.StatusOK, "application/octet-stream", payload) 192 | } 193 | 194 | func (s *HTTPServer) GetMembership(c echo.Context) error { 195 | ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second) 196 | defer cancel() 197 | 198 | membership, err := s.EpochHost.GetMembership(ctx) 199 | if err != nil { 200 | return fmt.Errorf("error in EpochHost.GetMembership: %w", err) 201 | } 202 | 203 | return c.JSON(http.StatusOK, membership) 204 | } 205 | 206 | func (s *HTTPServer) Shutdown(ctx context.Context) error { 207 | err := s.quicServer.Close() 208 | if err != nil { 209 | return fmt.Errorf("error in quicServer.Close: %w", err) 210 | } 211 | 212 | err = s.Echo.Shutdown(ctx) 213 | if err != nil { 214 | return fmt.Errorf("error shutting down echo: %w", err) 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func LoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 221 | return func(c echo.Context) error { 222 | start := time.Now() 223 | if err := next(c); err != nil { 224 | // default handler 225 | c.Error(err) 226 | } 227 | stop := time.Since(start) 228 | // Log otherwise 229 | logger := zerolog.Ctx(c.Request().Context()) 230 | req := c.Request() 231 | res := c.Response() 232 | 233 | p := req.URL.Path 234 | if p == "" { 235 | p = "/" 236 | } 237 | 238 | cl := req.Header.Get(echo.HeaderContentLength) 239 | if cl == "" { 240 | cl = "0" 241 | } 242 | logger.Debug().Str("method", req.Method).Str("remote_ip", c.RealIP()).Str("req_uri", req.RequestURI).Str("handler_path", c.Path()).Str("path", p).Int("status", res.Status).Int64("latency_ns", int64(stop)).Str("protocol", req.Proto).Str("bytes_in", cl).Int64("bytes_out", res.Size).Msg("req recived") 243 | return nil 244 | } 245 | } 246 | 247 | const ( 248 | certFile = "cert.pem" 249 | keyFile = "key.pem" 250 | ) 251 | 252 | func generateTLSCert() (tls.Certificate, error) { 253 | // Check if certificate and key files exist 254 | if fileExists(certFile) && fileExists(keyFile) { 255 | // Load existing certificate and key 256 | return tls.LoadX509KeyPair(certFile, keyFile) 257 | } 258 | 259 | // Generate a new certificate and key 260 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 261 | if err != nil { 262 | return tls.Certificate{}, err 263 | } 264 | 265 | template := x509.Certificate{ 266 | SerialNumber: big.NewInt(1), 267 | Subject: pkix.Name{ 268 | Organization: []string{"Example Co"}, 269 | }, 270 | NotBefore: time.Now(), 271 | NotAfter: time.Now().Add(time.Hour * 24 * 180), // Valid for 180 days 272 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 273 | ExtKeyUsage: []x509.ExtKeyUsage{ 274 | x509.ExtKeyUsageServerAuth, 275 | }, 276 | BasicConstraintsValid: true, 277 | } 278 | 279 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) 280 | if err != nil { 281 | return tls.Certificate{}, err 282 | } 283 | 284 | // Save the certificate 285 | certOut, err := os.Create(certFile) 286 | if err != nil { 287 | return tls.Certificate{}, err 288 | } 289 | pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) 290 | certOut.Close() 291 | 292 | // Save the key 293 | keyOut, err := os.Create(keyFile) 294 | if err != nil { 295 | return tls.Certificate{}, err 296 | } 297 | keyBytes, err := x509.MarshalECPrivateKey(privateKey) 298 | if err != nil { 299 | return tls.Certificate{}, err 300 | } 301 | pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}) 302 | keyOut.Close() 303 | 304 | // Load the newly created certificate and key 305 | return tls.LoadX509KeyPair(certFile, keyFile) 306 | } 307 | 308 | func fileExists(filename string) bool { 309 | _, err := os.Stat(filename) 310 | return !os.IsNotExist(err) 311 | } 312 | -------------------------------------------------------------------------------- /k6-perf.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep } from 'k6'; 3 | import { check } from 'k6'; 4 | 5 | export let options = { 6 | stages: [ 7 | { duration: '30s', target: 1000 }, // Ramp up to 1,000 users in 1 minute 8 | { duration: '30s', target: 5000 }, // Ramp up to 5,000 users in 1 minute 9 | { duration: '30s', target: 10000 }, // Ramp up to 10,000 users in 1 minute 10 | { duration: '1m', target: 10000 }, // Stay at 10,000 users for 3 minutes 11 | { duration: '30s', target: 5000 }, // Ramp down to 5,000 users in 1 minute 12 | { duration: '30s', target: 1000 }, // Ramp down to 1,000 users in 1 minute 13 | { duration: '30s', target: 0 }, // Ramp down to 0 users in 1 minute 14 | ], 15 | thresholds: { 16 | http_req_duration: ['p(99)<500'], // 99% of requests must complete below 500ms 17 | 'http_req_duration{staticAsset:yes}': ['p(99)<300'], // 99% of static asset requests should be below 300ms 18 | http_req_failed: ['rate<0.001'], // Error rate should be less than 01% 19 | }, 20 | }; 21 | 22 | export default function () { 23 | let res = http.get('http://localhost:8881/timestamp'); 24 | check(res, { 25 | 'is status 200': (r) => r.status === 200, 26 | }); 27 | // sleep(1); // Adjust the sleep duration as needed 28 | } 29 | -------------------------------------------------------------------------------- /k6-simple.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep } from 'k6'; 3 | import { check } from 'k6'; 4 | 5 | export let options = { 6 | stages: [ 7 | { duration: '30s', target: 100 }, // Ramp up to 1,000 users in 1 minute 8 | { duration: '1m', target: 100 }, // Stay at 10,000 users for 3 minutes 9 | { duration: '30s', target: 0 }, // Ramp down to 0 users in 1 minute 10 | ], 11 | thresholds: { 12 | http_req_duration: ['p(99)<500'], // 99% of requests must complete below 500ms 13 | 'http_req_duration{staticAsset:yes}': ['p(99)<300'], // 99% of static asset requests should be below 300ms 14 | http_req_failed: ['rate<0.001'], // Error rate should be less than 01% 15 | }, 16 | }; 17 | 18 | export default function () { 19 | let res = http.get('http://localhost:8881/timestamp'); 20 | check(res, { 21 | 'is status 200': (r) => r.status === 200, 22 | }); 23 | // sleep(1); // Adjust the sleep duration as needed 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/danthegoodman1/EpicEpoch/gologger" 6 | "github.com/danthegoodman1/EpicEpoch/http_server" 7 | "github.com/danthegoodman1/EpicEpoch/raft" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | var logger = gologger.NewLogger() 15 | 16 | func main() { 17 | logger.Debug().Msg("starting epic epoch api") 18 | 19 | // start raft 20 | nodeHost, err := raft.StartRaft() 21 | if err != nil { 22 | logger.Error().Err(err).Msg("raft couldn't start") 23 | os.Exit(1) 24 | } 25 | 26 | // prometheusReporter := observability.NewPrometheusReporter() 27 | // go func() { 28 | // err := observability.StartInternalHTTPServer(":8042", prometheusReporter) 29 | // if err != nil && !errors.Is(err, http.ErrServerClosed) { 30 | // logger.Error().Err(err).Msg("api server couldn't start") 31 | // os.Exit(1) 32 | // } 33 | // }() 34 | 35 | httpServer := http_server.StartHTTPServer(nodeHost) 36 | 37 | c := make(chan os.Signal, 1) 38 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 39 | <-c 40 | logger.Warn().Msg("received shutdown signal!") 41 | 42 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 43 | defer cancel() 44 | if err := httpServer.Shutdown(ctx); err != nil { 45 | logger.Error().Err(err).Msg("failed to shutdown HTTP server") 46 | } else { 47 | logger.Info().Msg("successfully shutdown HTTP server") 48 | } 49 | 50 | nodeHost.Stop() 51 | } 52 | -------------------------------------------------------------------------------- /observability/internal_http.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import ( 4 | "github.com/danthegoodman1/EpicEpoch/gologger" 5 | "log" 6 | "net/http" 7 | httppprof "net/http/pprof" 8 | 9 | "github.com/labstack/echo/v4" 10 | prom "github.com/prometheus/client_golang/prometheus" 11 | "github.com/uber-go/tally/v4/prometheus" 12 | ) 13 | 14 | var logger = gologger.NewLogger() 15 | 16 | func StartInternalHTTPServer(address string, prom prometheus.Reporter) error { 17 | server := echo.New() 18 | server.HideBanner = true 19 | server.HidePort = true 20 | logger.Info().Str("address", address).Msg("Starting Internal API") 21 | server.GET("/metrics", echo.WrapHandler(prom.HTTPHandler())) 22 | server.Any("/debug/pprof/*", echo.WrapHandler(http.HandlerFunc(httppprof.Index))) 23 | server.Any("/debug/pprof/cmdline", echo.WrapHandler(http.HandlerFunc(httppprof.Cmdline))) 24 | server.Any("/debug/pprof/profile", echo.WrapHandler(http.HandlerFunc(httppprof.Profile))) 25 | server.Any("/debug/pprof/symbol", echo.WrapHandler(http.HandlerFunc(httppprof.Symbol))) 26 | server.Any("/debug/pprof/trace", echo.WrapHandler(http.HandlerFunc(httppprof.Trace))) 27 | return server.Start(address) 28 | } 29 | 30 | func NewPrometheusReporter() prometheus.Reporter { 31 | c := prometheus.Configuration{ 32 | TimerType: "histogram", 33 | } 34 | reporter, err := c.NewReporter( 35 | prometheus.ConfigurationOptions{ 36 | Registry: prom.DefaultRegisterer.(*prom.Registry), 37 | OnError: func(err error) { 38 | log.Println("error in prometheus reporter", err) 39 | }, 40 | }, 41 | ) 42 | if err != nil { 43 | log.Fatalln("error creating prometheus reporter", err) 44 | } 45 | return reporter 46 | } 47 | -------------------------------------------------------------------------------- /proto/api/v1/api.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.34.2 4 | // protoc (unknown) 5 | // source: api/v1/api.proto 6 | 7 | package apiv1 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type HybridTimestamp struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Timestamp []byte `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 29 | } 30 | 31 | func (x *HybridTimestamp) Reset() { 32 | *x = HybridTimestamp{} 33 | if protoimpl.UnsafeEnabled { 34 | mi := &file_api_v1_api_proto_msgTypes[0] 35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 36 | ms.StoreMessageInfo(mi) 37 | } 38 | } 39 | 40 | func (x *HybridTimestamp) String() string { 41 | return protoimpl.X.MessageStringOf(x) 42 | } 43 | 44 | func (*HybridTimestamp) ProtoMessage() {} 45 | 46 | func (x *HybridTimestamp) ProtoReflect() protoreflect.Message { 47 | mi := &file_api_v1_api_proto_msgTypes[0] 48 | if protoimpl.UnsafeEnabled && x != nil { 49 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 50 | if ms.LoadMessageInfo() == nil { 51 | ms.StoreMessageInfo(mi) 52 | } 53 | return ms 54 | } 55 | return mi.MessageOf(x) 56 | } 57 | 58 | // Deprecated: Use HybridTimestamp.ProtoReflect.Descriptor instead. 59 | func (*HybridTimestamp) Descriptor() ([]byte, []int) { 60 | return file_api_v1_api_proto_rawDescGZIP(), []int{0} 61 | } 62 | 63 | func (x *HybridTimestamp) GetTimestamp() []byte { 64 | if x != nil { 65 | return x.Timestamp 66 | } 67 | return nil 68 | } 69 | 70 | type Empty struct { 71 | state protoimpl.MessageState 72 | sizeCache protoimpl.SizeCache 73 | unknownFields protoimpl.UnknownFields 74 | } 75 | 76 | func (x *Empty) Reset() { 77 | *x = Empty{} 78 | if protoimpl.UnsafeEnabled { 79 | mi := &file_api_v1_api_proto_msgTypes[1] 80 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 81 | ms.StoreMessageInfo(mi) 82 | } 83 | } 84 | 85 | func (x *Empty) String() string { 86 | return protoimpl.X.MessageStringOf(x) 87 | } 88 | 89 | func (*Empty) ProtoMessage() {} 90 | 91 | func (x *Empty) ProtoReflect() protoreflect.Message { 92 | mi := &file_api_v1_api_proto_msgTypes[1] 93 | if protoimpl.UnsafeEnabled && x != nil { 94 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 95 | if ms.LoadMessageInfo() == nil { 96 | ms.StoreMessageInfo(mi) 97 | } 98 | return ms 99 | } 100 | return mi.MessageOf(x) 101 | } 102 | 103 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead. 104 | func (*Empty) Descriptor() ([]byte, []int) { 105 | return file_api_v1_api_proto_rawDescGZIP(), []int{1} 106 | } 107 | 108 | var File_api_v1_api_proto protoreflect.FileDescriptor 109 | 110 | var file_api_v1_api_proto_rawDesc = []byte{ 111 | 0x0a, 0x10, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 112 | 0x74, 0x6f, 0x12, 0x06, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x22, 0x2f, 0x0a, 0x0f, 0x48, 0x79, 113 | 0x62, 0x72, 0x69, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1c, 0x0a, 114 | 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 115 | 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x07, 0x0a, 0x05, 0x45, 116 | 0x6d, 0x70, 0x74, 0x79, 0x32, 0x4e, 0x0a, 0x12, 0x48, 0x79, 0x62, 0x72, 0x69, 0x64, 0x54, 0x69, 117 | 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x41, 0x50, 0x49, 0x12, 0x38, 0x0a, 0x0c, 0x47, 0x65, 118 | 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x0d, 0x2e, 0x61, 0x70, 0x69, 119 | 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 120 | 0x76, 0x31, 0x2e, 0x48, 0x79, 0x62, 0x72, 0x69, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 121 | 0x6d, 0x70, 0x22, 0x00, 0x42, 0x7e, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 122 | 0x76, 0x31, 0x42, 0x08, 0x41, 0x70, 0x69, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 123 | 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 124 | 0x69, 0x6c, 0x64, 0x2f, 0x62, 0x75, 0x66, 0x2d, 0x74, 0x6f, 0x75, 0x72, 0x2f, 0x67, 0x65, 0x6e, 125 | 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x31, 0xa2, 0x02, 0x03, 126 | 0x41, 0x58, 0x58, 0xaa, 0x02, 0x06, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x06, 0x41, 127 | 0x70, 0x69, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x12, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x5c, 0x47, 128 | 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x41, 0x70, 0x69, 129 | 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 130 | } 131 | 132 | var ( 133 | file_api_v1_api_proto_rawDescOnce sync.Once 134 | file_api_v1_api_proto_rawDescData = file_api_v1_api_proto_rawDesc 135 | ) 136 | 137 | func file_api_v1_api_proto_rawDescGZIP() []byte { 138 | file_api_v1_api_proto_rawDescOnce.Do(func() { 139 | file_api_v1_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_v1_api_proto_rawDescData) 140 | }) 141 | return file_api_v1_api_proto_rawDescData 142 | } 143 | 144 | var file_api_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 145 | var file_api_v1_api_proto_goTypes = []any{ 146 | (*HybridTimestamp)(nil), // 0: api.v1.HybridTimestamp 147 | (*Empty)(nil), // 1: api.v1.Empty 148 | } 149 | var file_api_v1_api_proto_depIdxs = []int32{ 150 | 1, // 0: api.v1.HybridTimestampAPI.GetTimestamp:input_type -> api.v1.Empty 151 | 0, // 1: api.v1.HybridTimestampAPI.GetTimestamp:output_type -> api.v1.HybridTimestamp 152 | 1, // [1:2] is the sub-list for method output_type 153 | 0, // [0:1] is the sub-list for method input_type 154 | 0, // [0:0] is the sub-list for extension type_name 155 | 0, // [0:0] is the sub-list for extension extendee 156 | 0, // [0:0] is the sub-list for field type_name 157 | } 158 | 159 | func init() { file_api_v1_api_proto_init() } 160 | func file_api_v1_api_proto_init() { 161 | if File_api_v1_api_proto != nil { 162 | return 163 | } 164 | if !protoimpl.UnsafeEnabled { 165 | file_api_v1_api_proto_msgTypes[0].Exporter = func(v any, i int) any { 166 | switch v := v.(*HybridTimestamp); i { 167 | case 0: 168 | return &v.state 169 | case 1: 170 | return &v.sizeCache 171 | case 2: 172 | return &v.unknownFields 173 | default: 174 | return nil 175 | } 176 | } 177 | file_api_v1_api_proto_msgTypes[1].Exporter = func(v any, i int) any { 178 | switch v := v.(*Empty); i { 179 | case 0: 180 | return &v.state 181 | case 1: 182 | return &v.sizeCache 183 | case 2: 184 | return &v.unknownFields 185 | default: 186 | return nil 187 | } 188 | } 189 | } 190 | type x struct{} 191 | out := protoimpl.TypeBuilder{ 192 | File: protoimpl.DescBuilder{ 193 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 194 | RawDescriptor: file_api_v1_api_proto_rawDesc, 195 | NumEnums: 0, 196 | NumMessages: 2, 197 | NumExtensions: 0, 198 | NumServices: 1, 199 | }, 200 | GoTypes: file_api_v1_api_proto_goTypes, 201 | DependencyIndexes: file_api_v1_api_proto_depIdxs, 202 | MessageInfos: file_api_v1_api_proto_msgTypes, 203 | }.Build() 204 | File_api_v1_api_proto = out.File 205 | file_api_v1_api_proto_rawDesc = nil 206 | file_api_v1_api_proto_goTypes = nil 207 | file_api_v1_api_proto_depIdxs = nil 208 | } 209 | -------------------------------------------------------------------------------- /proto/api/v1/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package api.v1; 3 | 4 | option go_package = "github.com/danthegoodman1/EpicEpoch/proto/api/v1"; 5 | 6 | message HybridTimestamp { 7 | bytes timestamp = 1; 8 | } 9 | 10 | message Empty {}; 11 | 12 | service HybridTimestampAPI { 13 | rpc GetTimestamp(Empty) returns (HybridTimestamp) {}; 14 | } -------------------------------------------------------------------------------- /raft/epoch_host.go: -------------------------------------------------------------------------------- 1 | package raft 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/lni/dragonboat/v3" 11 | 12 | "github.com/danthegoodman1/EpicEpoch/utils" 13 | ) 14 | 15 | type ( 16 | EpochHost struct { 17 | nodeHost *dragonboat.NodeHost 18 | 19 | // The monotonic incrementing index of a single epoch. 20 | // Each request must be servied a unique (lastEpoch, epochIndex) value 21 | epochIndex atomic.Uint64 22 | 23 | // lastEpoch was the last epoch that we served to a request, 24 | // used to check whether we need to swap the epoch index 25 | lastEpoch atomic.Uint64 26 | 27 | readerAgentStopChan chan struct{} 28 | 29 | requestChan chan *pendingRead 30 | 31 | readerAgentReading atomic.Bool 32 | 33 | // pokeChan is used to poke the reader to generate timestamps 34 | pokeChan chan struct{} 35 | 36 | updateTicker *time.Ticker 37 | } 38 | 39 | pendingRead struct { 40 | // callbackChan is a channel to write back to with the produced timestamp 41 | callbackChan chan []byte 42 | count int 43 | } 44 | ) 45 | 46 | // readerAgentLoop should be launched in a goroutine 47 | func (e *EpochHost) readerAgentLoop() { 48 | for { 49 | logger.Debug().Msg("reader agent waiting...") 50 | select { 51 | case <-e.readerAgentStopChan: 52 | logger.Warn().Msg("reader agent loop received on stop chan, stopping") 53 | return 54 | case <-e.pokeChan: 55 | logger.Debug().Msg("reader agent poked") 56 | e.generateTimestamps() 57 | } 58 | } 59 | } 60 | 61 | // generateTimestamps generates timestamps for pending requests, and handles looping if more requests come in 62 | func (e *EpochHost) generateTimestamps() { 63 | // Capture the current pending requests so there's no case we get locked 64 | pendingRequests := len(e.requestChan) 65 | logger.Debug().Msgf("Serving %d pending requests", pendingRequests) 66 | 67 | // Read the epoch 68 | s := time.Now() 69 | ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(raftRttMs)*100) 70 | defer cancel() 71 | currentEpochI, err := e.nodeHost.SyncRead(ctx, ClusterID, nil) 72 | if err != nil { 73 | // This is never good, crash 74 | logger.Fatal().Err(err).Msg("error in nodeHost.SyncRead") 75 | return 76 | } 77 | logger.Debug().Msgf("Read from raft in %+v", time.Since(s)) 78 | 79 | currentEpoch, ok := currentEpochI.(PersistenceEpoch) 80 | if !ok { 81 | // This is never good, crash 82 | logger.Fatal().Err(err).Bool("ok", ok).Interface("currentEpoch", currentEpoch).Msg("lookup did not return a valid current epoch") 83 | return 84 | } 85 | 86 | if currentEpoch.Epoch == 0 { 87 | // Must be new, write one first 88 | currentEpoch.Epoch = uint64(time.Now().UnixNano()) 89 | logger.Warn().Msgf("read current epoch 0, writing first value %d", currentEpoch.Epoch) 90 | err = e.proposeNewEpoch(currentEpoch.Epoch) 91 | if err != nil { 92 | // This is never good, crash 93 | logger.Fatal().Err(err).Msg("error in nodeHost.SyncPropose") 94 | return 95 | } 96 | } else if e.lastEpoch.Load() == 0 { 97 | // We recently became the leader, we must increment the epoch 98 | currentEpoch.Epoch = uint64(time.Now().UnixNano()) 99 | logger.Warn().Msgf("we must have been elected, incrementing epoch %d", currentEpoch.Epoch) 100 | if currentEpoch.Epoch <= e.lastEpoch.Load() { 101 | logger.Error().Uint64("newEpoch", currentEpoch.Epoch).Uint64("lastEpoch", e.lastEpoch.Load()).Msg("new epoch less than last epoch, there must be clock drift, incrementing new epoch by 1") 102 | currentEpoch.Epoch++ 103 | } 104 | err = e.proposeNewEpoch(currentEpoch.Epoch) 105 | if err != nil { 106 | // This is never good, crash 107 | logger.Fatal().Err(err).Msg("error in nodeHost.SyncPropose") 108 | return 109 | } 110 | } 111 | 112 | if currentEpoch.Epoch != e.lastEpoch.Load() { 113 | // We need to push it forward and reset the index 114 | e.lastEpoch.Store(currentEpoch.Epoch) 115 | e.epochIndex.Store(0) 116 | } 117 | 118 | s = time.Now() 119 | for range pendingRequests { 120 | // Write to the pending requests 121 | req := <-e.requestChan 122 | 123 | // Build the timestamp 124 | timestamp := make([]byte, 16*req.count) // 8 for epoch, 8 for index, multiply for each 125 | logger.Debug().Msgf("writing for %d", req.count) 126 | reqIndex := e.epochIndex.Add(uint64(req.count)) // do a single atomic increment for the entire batch 127 | reqIndex -= uint64(req.count) // reset it down to what it was before we incremented 128 | for i := 0; i < req.count; i++ { 129 | reqIndex++ // increment our request index 130 | offset := i * 16 131 | binary.BigEndian.PutUint64(timestamp[offset:offset+8], currentEpoch.Epoch) 132 | binary.BigEndian.PutUint64(timestamp[offset+8:offset+16], reqIndex) 133 | } 134 | 135 | select { 136 | case req.callbackChan <- timestamp: 137 | default: 138 | logger.Warn().Msg("did not have listener on callback chan when generating timestamp") 139 | } 140 | } 141 | logger.Debug().Msgf("Served %d requests in %+v", pendingRequests, time.Since(s)) 142 | 143 | if len(e.requestChan) > 0 { 144 | // There are more requests, generating more timestamps 145 | logger.Debug().Msg("found more requests in request channel, generating more timestamps") 146 | e.generateTimestamps() 147 | } 148 | } 149 | 150 | // GetLeader returns the leader node ID of the specified Raft cluster based 151 | // on local node's knowledge. The returned boolean value indicates whether the 152 | // leader information is available. 153 | func (e *EpochHost) GetLeader() (uint64, bool, error) { 154 | return e.nodeHost.GetLeaderID(ClusterID) 155 | } 156 | 157 | func (e *EpochHost) Stop() { 158 | e.updateTicker.Stop() 159 | e.readerAgentStopChan <- struct{}{} 160 | e.nodeHost.Stop() 161 | } 162 | 163 | // GetUniqueTimestamp gets a unique hybrid timestamp to serve to a client 164 | func (e *EpochHost) GetUniqueTimestamp(ctx context.Context, count int) ([]byte, error) { 165 | if count < 1 { 166 | return nil, fmt.Errorf("count must be >= 1") 167 | } 168 | pr := &pendingRead{callbackChan: make(chan []byte, 1), count: count} 169 | 170 | // Register request 171 | err := utils.WriteWithContext(ctx, e.requestChan, pr) 172 | if err != nil { 173 | return nil, fmt.Errorf("error writing nil, pending request to request buffer: %w", err) 174 | } 175 | 176 | // Try to poke the reader goroutine 177 | select { 178 | case e.pokeChan <- struct{}{}: 179 | logger.Debug().Msg("poked reader agent") 180 | default: 181 | logger.Debug().Msg("poke chan had nothing waiting") 182 | } 183 | 184 | // Wait for the response 185 | timestamp, err := utils.ReadWithContext(ctx, pr.callbackChan) 186 | if err != nil { 187 | return nil, fmt.Errorf("error reading from callback channel with context: %w", err) 188 | } 189 | 190 | return timestamp, nil 191 | } 192 | 193 | func (e *EpochHost) proposeNewEpoch(newEpoch uint64) error { 194 | session := e.nodeHost.GetNoOPSession(ClusterID) 195 | ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(raftRttMs)*200) 196 | defer cancel() 197 | _, err := e.nodeHost.SyncPropose(ctx, session, utils.MustMarshal(PersistenceEpoch{Epoch: newEpoch})) 198 | if err != nil { 199 | return fmt.Errorf("error in nodeHost.SyncPropose: %w", err) 200 | } 201 | 202 | return nil 203 | } 204 | 205 | type ( 206 | Membership struct { 207 | Leader Member `json:"leader"` 208 | Members []Member `json:"members"` 209 | } 210 | 211 | Member struct { 212 | NodeID uint64 `json:"nodeID"` 213 | Addr string `json:"addr"` 214 | } 215 | ) 216 | 217 | func (e *EpochHost) GetMembership(ctx context.Context) (*Membership, error) { 218 | leader, available, err := e.nodeHost.GetLeaderID(ClusterID) 219 | if err != nil { 220 | return nil, fmt.Errorf("error getting membership: %e", err) 221 | } 222 | 223 | if !available { 224 | return nil, fmt.Errorf("raft membership not avilable") 225 | } 226 | 227 | membership, err := e.nodeHost.SyncGetClusterMembership(ctx, ClusterID) 228 | if err != nil { 229 | return nil, fmt.Errorf("error in nodeHost.SyncGetClusterMembership: %w", err) 230 | } 231 | 232 | m := &Membership{} 233 | for id, node := range membership.Nodes { 234 | if id == leader { 235 | m.Leader = Member{ 236 | NodeID: id, 237 | Addr: node, 238 | } 239 | } 240 | m.Members = append(m.Members, Member{ 241 | NodeID: id, 242 | Addr: node, 243 | }) 244 | } 245 | 246 | return m, nil 247 | } 248 | -------------------------------------------------------------------------------- /raft/file.go: -------------------------------------------------------------------------------- 1 | package raft 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | // WriteFileAtomic atomically writes to a file, even if it already exists 11 | // from https://github.com/tailscale/tailscale/blob/main/atomicfile/atomicfile.go 12 | func WriteFileAtomic(filename string, data []byte, perm os.FileMode) (err error) { 13 | fi, err := os.Stat(filename) 14 | if err == nil && !fi.Mode().IsRegular() { 15 | return fmt.Errorf("%s already exists and is not a regular file", filename) 16 | } 17 | f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp") 18 | if err != nil { 19 | return err 20 | } 21 | tmpName := f.Name() 22 | defer func() { 23 | if err != nil { 24 | f.Close() 25 | os.Remove(tmpName) 26 | } 27 | }() 28 | if _, err := f.Write(data); err != nil { 29 | return err 30 | } 31 | if runtime.GOOS != "windows" { 32 | if err := f.Chmod(perm); err != nil { 33 | return err 34 | } 35 | } 36 | if err := f.Sync(); err != nil { 37 | return err 38 | } 39 | if err := f.Close(); err != nil { 40 | return err 41 | } 42 | return os.Rename(tmpName, filename) 43 | } 44 | -------------------------------------------------------------------------------- /raft/logger_factory.go: -------------------------------------------------------------------------------- 1 | package raft 2 | 3 | import ( 4 | "github.com/danthegoodman1/EpicEpoch/gologger" 5 | dragonlogger "github.com/lni/dragonboat/v3/logger" 6 | "github.com/rs/zerolog" 7 | ) 8 | 9 | type RaftGoLogger struct { 10 | level dragonlogger.LogLevel 11 | logger zerolog.Logger 12 | } 13 | 14 | // Note that info and debug log levels are stepped down a level 15 | 16 | func (r *RaftGoLogger) SetLevel(level dragonlogger.LogLevel) { 17 | r.level = level 18 | } 19 | 20 | func (r *RaftGoLogger) Debugf(format string, args ...interface{}) { 21 | r.logger.Trace().Msgf(format, args...) 22 | } 23 | 24 | func (r *RaftGoLogger) Infof(format string, args ...interface{}) { 25 | r.logger.Debug().Msgf(format, args...) 26 | } 27 | 28 | func (r *RaftGoLogger) Warningf(format string, args ...interface{}) { 29 | r.logger.Warn().Msgf(format, args...) 30 | } 31 | 32 | func (r *RaftGoLogger) Errorf(format string, args ...interface{}) { 33 | r.logger.Error().Msgf(format, args...) 34 | } 35 | 36 | func (r *RaftGoLogger) Panicf(format string, args ...interface{}) { 37 | r.logger.Panic().Msgf(format, args...) 38 | } 39 | 40 | func CreateLogger(name string) dragonlogger.ILogger { 41 | l := &RaftGoLogger{ 42 | logger: gologger.NewLogger().With().Str("loggerName", name).Str("dragonboat", "y").Logger(), 43 | } 44 | 45 | return l 46 | } 47 | -------------------------------------------------------------------------------- /raft/raft.go: -------------------------------------------------------------------------------- 1 | package raft 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/danthegoodman1/EpicEpoch/utils" 8 | "github.com/lni/dragonboat/v3" 9 | "github.com/lni/dragonboat/v3/config" 10 | dragonlogger "github.com/lni/dragonboat/v3/logger" 11 | "os" 12 | "path/filepath" 13 | "sync/atomic" 14 | "time" 15 | ) 16 | 17 | const ClusterID = 100 18 | 19 | var raftRttMs = uint64(utils.GetEnvOrDefaultInt("RTT_MS", 10)) 20 | 21 | func StartRaft() (*EpochHost, error) { 22 | nodeID := utils.NodeID 23 | rc := config.Config{ 24 | NodeID: nodeID, 25 | ClusterID: ClusterID, 26 | ElectionRTT: 10, 27 | HeartbeatRTT: 1, 28 | CheckQuorum: true, 29 | SnapshotEntries: 10, 30 | CompactionOverhead: 5, 31 | } 32 | datadir := filepath.Join("_raft", fmt.Sprintf("node%d", nodeID)) 33 | nhc := config.NodeHostConfig{ 34 | WALDir: datadir, 35 | NodeHostDir: datadir, 36 | RTTMillisecond: raftRttMs, 37 | RaftAddress: os.Getenv("RAFT_ADDR"), 38 | } 39 | dragonlogger.SetLoggerFactory(CreateLogger) 40 | nh, err := dragonboat.NewNodeHost(nhc) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | err = nh.StartOnDiskCluster(map[uint64]dragonboat.Target{ 46 | 1: "localhost:60001", 47 | 2: "localhost:60002", 48 | 3: "localhost:60003", 49 | }, false, NewEpochStateMachine, rc) 50 | if err != nil { 51 | return nil, fmt.Errorf("error in StartOnDiskCluster: %w", err) 52 | } 53 | 54 | eh := &EpochHost{ 55 | nodeHost: nh, 56 | epochIndex: atomic.Uint64{}, 57 | lastEpoch: atomic.Uint64{}, 58 | readerAgentStopChan: make(chan struct{}), 59 | requestChan: make(chan *pendingRead, utils.TimestampRequestBuffer), 60 | readerAgentReading: atomic.Bool{}, 61 | pokeChan: make(chan struct{}), 62 | updateTicker: time.NewTicker(time.Millisecond * time.Duration(utils.EpochIntervalMS)), 63 | } 64 | eh.epochIndex.Store(0) 65 | eh.lastEpoch.Store(0) 66 | eh.readerAgentReading.Store(false) 67 | 68 | // Debug log loop 69 | go func() { 70 | warnedClockDrift := false // dedupe for logging clock drift 71 | deadlines := int64(0) // propose deadlines counter to eventually crash 72 | for { 73 | _, ok := <-eh.updateTicker.C 74 | if !ok { 75 | logger.Warn().Msg("ticker channel closed, returning") 76 | return 77 | } 78 | leader, available, err := nh.GetLeaderID(ClusterID) 79 | if err != nil { 80 | logger.Fatal().Err(err).Msg("error getting leader id, crashing") 81 | return 82 | } 83 | // logger.Debug().Err(err).Msgf("Leader=%d available=%+v", leader, available) 84 | if available && leader == utils.NodeID { 85 | // Update the epoch 86 | newEpoch := uint64(time.Now().UnixNano()) 87 | if newEpoch <= eh.lastEpoch.Load() && !warnedClockDrift { 88 | warnedClockDrift = true 89 | logger.Error().Uint64("newEpoch", newEpoch).Uint64("lastEpoch", eh.lastEpoch.Load()).Msg("new epoch less than last epoch, there must be clock drift, incrementing new epoch by 1") 90 | newEpoch++ 91 | } else { 92 | warnedClockDrift = false // re-enable the warn trigger 93 | deadlines = 0 // reset the deadlines counter 94 | } 95 | 96 | // Write the new value 97 | err = eh.proposeNewEpoch(newEpoch) 98 | if errors.Is(err, context.DeadlineExceeded) { 99 | deadlines++ 100 | logger.Error().Str("crashTreshold", fmt.Sprintf("%d/%d", deadlines, utils.EpochIntervalDeadlineLimit)).Msg("deadline exceeded proposing new epoch") 101 | if deadlines >= utils.EpochIntervalDeadlineLimit { 102 | logger.Fatal().Msg("new epoch deadline threshold exceeded, crashing") 103 | return 104 | } 105 | } 106 | if err != nil { 107 | // Bad, crash 108 | logger.Fatal().Err(err).Msg("error in proposeNewEpoch, crashing") 109 | return 110 | } 111 | } 112 | } 113 | }() 114 | 115 | go eh.readerAgentLoop() 116 | 117 | return eh, nil 118 | } 119 | -------------------------------------------------------------------------------- /raft/statemachine.go: -------------------------------------------------------------------------------- 1 | package raft 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/danthegoodman1/EpicEpoch/gologger" 8 | "github.com/danthegoodman1/EpicEpoch/utils" 9 | "github.com/lni/dragonboat/v3/statemachine" 10 | "github.com/rs/zerolog" 11 | "io" 12 | "os" 13 | ) 14 | 15 | // TODO replace JSON serialization to disk and network with protobuf? 16 | 17 | type ( 18 | EpochStateMachine struct { 19 | ClusterID uint64 20 | NodeID uint64 21 | EpochFile string 22 | epoch PersistenceEpoch 23 | closed bool 24 | logger zerolog.Logger 25 | } 26 | 27 | PersistenceEpoch struct { 28 | RaftIndex uint64 29 | Epoch uint64 30 | } 31 | ) 32 | 33 | var ( 34 | logger = gologger.NewLogger() 35 | ) 36 | 37 | func NewEpochStateMachine(clusterID, nodeID uint64) statemachine.IOnDiskStateMachine { 38 | epochFile := fmt.Sprintf("./epoch-%d.json", nodeID) // TODO make this configurable 39 | 40 | sm := &EpochStateMachine{ 41 | ClusterID: clusterID, 42 | NodeID: nodeID, 43 | EpochFile: epochFile, 44 | logger: gologger.NewLogger(), 45 | } 46 | 47 | return sm 48 | } 49 | 50 | func (e *EpochStateMachine) Open(stopChan <-chan struct{}) (uint64, error) { 51 | e.logger.Debug().Msg("open") 52 | // Read the current epoch now, crash if we can't 53 | if _, err := os.Stat(e.EpochFile); errors.Is(err, os.ErrNotExist) { 54 | e.epoch = PersistenceEpoch{ 55 | RaftIndex: 0, 56 | Epoch: 0, 57 | } 58 | } else if err != nil { 59 | logger.Fatal().Err(err).Msg("error opening persistence file") 60 | } else { 61 | fileBytes, err := os.ReadFile(e.EpochFile) 62 | if err != nil { 63 | logger.Fatal().Err(err).Msg("error reading persistence file") 64 | } 65 | 66 | err = json.Unmarshal(fileBytes, &e.epoch) 67 | if err != nil { 68 | logger.Fatal().Err(err).Msg("error deserializing persistence file, is it corrupted?") 69 | } 70 | } 71 | 72 | return e.epoch.RaftIndex, nil 73 | } 74 | 75 | func (e *EpochStateMachine) Update(entries []statemachine.Entry) ([]statemachine.Entry, error) { 76 | e.logger.Debug().Interface("entries", entries).Msg("update") 77 | if e.closed { 78 | panic("Update called after close!") 79 | } 80 | 81 | // Since all writes are to the same key, we can just take the last one and write it 82 | // Update it in memory 83 | // Verify that it's newer than the current time 84 | var newEpoch PersistenceEpoch 85 | err := json.Unmarshal(entries[len(entries)-1].Cmd, &newEpoch) 86 | if err != nil { 87 | return nil, fmt.Errorf("error in json.Unmarshal: %w", err) 88 | } 89 | 90 | if newEpoch.Epoch <= e.epoch.Epoch { 91 | return nil, fmt.Errorf("update epoch was not greater than the current epoch, there is clock drift or a bug") 92 | } 93 | // Otherwise we can update it 94 | e.epoch = newEpoch 95 | 96 | e.epoch.RaftIndex = entries[len(entries)-1].Index 97 | 98 | err = WriteFileAtomic(e.EpochFile, utils.MustMarshal(e.epoch), 0777) 99 | if err != nil { 100 | return nil, fmt.Errorf("error writing atomically to file %s: %w", e.EpochFile, err) 101 | } 102 | 103 | // Just return the entries, there isn't a status code here, it either worked or exploded 104 | return entries, nil 105 | } 106 | 107 | var ErrAlreadyClosed = errors.New("already closed") 108 | 109 | func (e *EpochStateMachine) Lookup(i interface{}) (interface{}, error) { 110 | e.logger.Debug().Interface("lookup", i).Msg("lookup") 111 | if e.closed { 112 | return nil, ErrAlreadyClosed 113 | } 114 | 115 | // Only need to return the current epoch 116 | return e.epoch, nil 117 | } 118 | 119 | func (e *EpochStateMachine) Sync() error { 120 | e.logger.Debug().Msg("sync") 121 | if e.closed { 122 | panic("Sync called after close!") 123 | } 124 | // Because we write atomically in Update, we do not need to do anything here 125 | return nil 126 | } 127 | 128 | func (e *EpochStateMachine) PrepareSnapshot() (interface{}, error) { 129 | e.logger.Debug().Msg("prepare snapshot") 130 | if e.closed { 131 | panic("PrepareSnapshot called after close!") 132 | } 133 | 134 | // Need to save a serialization of the state 135 | return utils.MustMarshal(e.epoch), nil 136 | } 137 | 138 | func (e *EpochStateMachine) SaveSnapshot(i interface{}, writer io.Writer, stopChan <-chan struct{}) error { 139 | e.logger.Debug().Msg("save snapshot") 140 | if e.closed { 141 | panic("SaveSnapshot called after close!") 142 | } 143 | 144 | serializedEpoch, ok := i.([]byte) 145 | if !ok { 146 | return fmt.Errorf("prepared snapshot was not bytes") 147 | } 148 | 149 | _, err := writer.Write(serializedEpoch) 150 | if err != nil { 151 | return fmt.Errorf("error in writer.Write: %w", err) 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (e *EpochStateMachine) RecoverFromSnapshot(reader io.Reader, stopChan <-chan struct{}) error { 158 | e.logger.Debug().Msg("recover from snapshot") 159 | if e.closed { 160 | panic("RecoverFromSnapshot called after close!") 161 | } 162 | 163 | serializedEpoch, err := io.ReadAll(reader) 164 | if err != nil { 165 | return fmt.Errorf("error in io.ReadAll: %w", err) 166 | } 167 | 168 | // First, save to memory to make sure it is good 169 | err = json.Unmarshal(serializedEpoch, &e.epoch) 170 | if err != nil { 171 | return fmt.Errorf("error in json.Unmarshal: %w", err) 172 | } 173 | 174 | // Then write it to disk 175 | err = WriteFileAtomic(e.EpochFile, serializedEpoch, 0777) 176 | if err != nil { 177 | return fmt.Errorf("error in WriteFileAtomic: %w", err) 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (e *EpochStateMachine) Close() error { 184 | e.logger.Debug().Msg("close") 185 | // Dragonboat claims that nothing else besides Lookup may be called after closed, 186 | // but we can also be safe :) 187 | if e.closed { 188 | panic("already closed state machine!") 189 | } 190 | e.closed = true 191 | return nil 192 | } 193 | -------------------------------------------------------------------------------- /ring/ring.go: -------------------------------------------------------------------------------- 1 | // Generic modification of 2 | // https://github.com/Workiva/go-datastructures/blob/master/queue/ring.go 3 | 4 | package ring 5 | 6 | import ( 7 | "errors" 8 | "runtime" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | var ( 14 | // ErrDisposed is returned when an operation is performed on a disposed 15 | // queue. 16 | ErrDisposed = errors.New(`queue: disposed`) 17 | 18 | // ErrTimeout is returned when an applicable queue operation times out. 19 | ErrTimeout = errors.New(`queue: poll timed out`) 20 | 21 | // ErrEmptyQueue is returned when an non-applicable queue operation was called 22 | // due to the queue's empty item state 23 | ErrEmptyQueue = errors.New(`queue: empty queue`) 24 | ) 25 | 26 | // roundUp takes a uint64 greater than 0 and rounds it up to the next 27 | // power of 2. 28 | func roundUp(v uint64) uint64 { 29 | v-- 30 | v |= v >> 1 31 | v |= v >> 2 32 | v |= v >> 4 33 | v |= v >> 8 34 | v |= v >> 16 35 | v |= v >> 32 36 | v++ 37 | return v 38 | } 39 | 40 | type node struct { 41 | position uint64 42 | data any 43 | } 44 | 45 | type nodes []node 46 | 47 | // RingBuffer is a MPMC buffer that achieves threadsafety with CAS operations 48 | // only. A put on full or get on empty call will block until an item 49 | // is put or retrieved. Calling Dispose on the RingBuffer will unblock 50 | // any blocked threads with an error. This buffer is similar to the buffer 51 | // described here: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue 52 | // with some minor additions. 53 | type RingBuffer[T any] struct { 54 | _padding0 [8]uint64 55 | queue uint64 56 | _padding1 [8]uint64 57 | dequeue uint64 58 | _padding2 [8]uint64 59 | mask, disposed uint64 60 | _padding3 [8]uint64 61 | nodes nodes 62 | } 63 | 64 | func (rb *RingBuffer[T]) init(size uint64) { 65 | size = roundUp(size) 66 | rb.nodes = make(nodes, size) 67 | for i := uint64(0); i < size; i++ { 68 | rb.nodes[i] = node{position: i} 69 | } 70 | rb.mask = size - 1 // so we don't have to do this with every put/get operation 71 | } 72 | 73 | // Put adds the provided item to the queue. If the queue is full, this 74 | // call will block until an item is added to the queue or Dispose is called 75 | // on the queue. An error will be returned if the queue is disposed. 76 | func (rb *RingBuffer[T]) Put(item T) error { 77 | _, err := rb.put(item, false) 78 | return err 79 | } 80 | 81 | // Offer adds the provided item to the queue if there is space. If the queue 82 | // is full, this call will return false. An error will be returned if the 83 | // queue is disposed. 84 | func (rb *RingBuffer[T]) Offer(item T) (bool, error) { 85 | return rb.put(item, true) 86 | } 87 | 88 | func (rb *RingBuffer[T]) put(item T, offer bool) (bool, error) { 89 | var n *node 90 | pos := atomic.LoadUint64(&rb.queue) 91 | L: 92 | for { 93 | if atomic.LoadUint64(&rb.disposed) == 1 { 94 | return false, ErrDisposed 95 | } 96 | 97 | n = &rb.nodes[pos&rb.mask] 98 | seq := atomic.LoadUint64(&n.position) 99 | switch dif := seq - pos; { 100 | case dif == 0: 101 | if atomic.CompareAndSwapUint64(&rb.queue, pos, pos+1) { 102 | break L 103 | } 104 | case dif < 0: 105 | panic(`Ring buffer in a compromised state during a put operation.`) 106 | default: 107 | pos = atomic.LoadUint64(&rb.queue) 108 | } 109 | 110 | if offer { 111 | return false, nil 112 | } 113 | 114 | runtime.Gosched() // free up the cpu before the next iteration 115 | } 116 | 117 | n.data = item 118 | atomic.StoreUint64(&n.position, pos+1) 119 | return true, nil 120 | } 121 | 122 | // Get will return the next item in the queue. This call will block 123 | // if the queue is empty. This call will unblock when an item is added 124 | // to the queue or Dispose is called on the queue. An error will be returned 125 | // if the queue is disposed. 126 | func (rb *RingBuffer[T]) Get() (T, error) { 127 | return rb.Poll(0) 128 | } 129 | 130 | // Poll will return the next item in the queue. This call will block 131 | // if the queue is empty. This call will unblock when an item is added 132 | // to the queue, Dispose is called on the queue, or the timeout is reached. An 133 | // error will be returned if the queue is disposed or a timeout occurs. A 134 | // non-positive timeout will block indefinitely. 135 | func (rb *RingBuffer[T]) Poll(timeout time.Duration) (t T, err error) { 136 | var ( 137 | n *node 138 | pos = atomic.LoadUint64(&rb.dequeue) 139 | start time.Time 140 | ) 141 | if timeout > 0 { 142 | start = time.Now() 143 | } 144 | L: 145 | for { 146 | if atomic.LoadUint64(&rb.disposed) == 1 { 147 | err = ErrDisposed 148 | return 149 | } 150 | 151 | n = &rb.nodes[pos&rb.mask] 152 | seq := atomic.LoadUint64(&n.position) 153 | switch dif := seq - (pos + 1); { 154 | case dif == 0: 155 | if atomic.CompareAndSwapUint64(&rb.dequeue, pos, pos+1) { 156 | break L 157 | } 158 | case dif < 0: 159 | panic(`Ring buffer in compromised state during a get operation.`) 160 | default: 161 | pos = atomic.LoadUint64(&rb.dequeue) 162 | } 163 | 164 | if timeout > 0 && time.Since(start) >= timeout { 165 | err = ErrTimeout 166 | return 167 | } 168 | 169 | runtime.Gosched() // free up the cpu before the next iteration 170 | } 171 | data := n.data 172 | n.data = nil 173 | atomic.StoreUint64(&n.position, pos+rb.mask+1) 174 | return data.(T), nil 175 | } 176 | 177 | // Len returns the number of items in the queue. 178 | func (rb *RingBuffer[T]) Len() uint64 { 179 | return atomic.LoadUint64(&rb.queue) - atomic.LoadUint64(&rb.dequeue) 180 | } 181 | 182 | // Cap returns the capacity of this ring buffer. 183 | func (rb *RingBuffer[T]) Cap() uint64 { 184 | return uint64(len(rb.nodes)) 185 | } 186 | 187 | // Dispose will dispose of this queue and free any blocked threads 188 | // in the Put and/or Get methods. Calling those methods on a disposed 189 | // queue will return an error. 190 | func (rb *RingBuffer[T]) Dispose() { 191 | atomic.CompareAndSwapUint64(&rb.disposed, 0, 1) 192 | } 193 | 194 | // IsDisposed will return a bool indicating if this queue has been 195 | // disposed. 196 | func (rb *RingBuffer[T]) IsDisposed() bool { 197 | return atomic.LoadUint64(&rb.disposed) == 1 198 | } 199 | 200 | // NewRingBuffer will allocate, initialize, and return a ring buffer 201 | // with the specified size. 202 | func NewRingBuffer[T any](size uint64) *RingBuffer[T] { 203 | rb := &RingBuffer[T]{} 204 | rb.init(size) 205 | return rb 206 | } 207 | -------------------------------------------------------------------------------- /ring/ring_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Workiva, LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ring 18 | 19 | import ( 20 | "sync" 21 | "sync/atomic" 22 | "testing" 23 | "time" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestRingInsert(t *testing.T) { 29 | rb := NewRingBuffer[any](5) 30 | assert.Equal(t, uint64(8), rb.Cap()) 31 | 32 | err := rb.Put(5) 33 | if !assert.Nil(t, err) { 34 | return 35 | } 36 | 37 | result, err := rb.Get() 38 | if !assert.Nil(t, err) { 39 | return 40 | } 41 | 42 | assert.Equal(t, 5, result) 43 | } 44 | 45 | func TestRingMultipleInserts(t *testing.T) { 46 | rb := NewRingBuffer[any](5) 47 | 48 | err := rb.Put(1) 49 | if !assert.Nil(t, err) { 50 | return 51 | } 52 | 53 | err = rb.Put(2) 54 | if !assert.Nil(t, err) { 55 | return 56 | } 57 | 58 | result, err := rb.Get() 59 | if !assert.Nil(t, err) { 60 | return 61 | } 62 | 63 | assert.Equal(t, 1, result) 64 | 65 | result, err = rb.Get() 66 | if assert.Nil(t, err) { 67 | return 68 | } 69 | 70 | assert.Equal(t, 2, result) 71 | } 72 | 73 | func TestIntertwinedGetAndPut(t *testing.T) { 74 | rb := NewRingBuffer[any](5) 75 | err := rb.Put(1) 76 | if !assert.Nil(t, err) { 77 | return 78 | } 79 | 80 | result, err := rb.Get() 81 | if !assert.Nil(t, err) { 82 | return 83 | } 84 | 85 | assert.Equal(t, 1, result) 86 | 87 | err = rb.Put(2) 88 | if !assert.Nil(t, err) { 89 | return 90 | } 91 | 92 | result, err = rb.Get() 93 | if !assert.Nil(t, err) { 94 | return 95 | } 96 | 97 | assert.Equal(t, 2, result) 98 | } 99 | 100 | func TestPutToFull(t *testing.T) { 101 | rb := NewRingBuffer[any](3) 102 | 103 | for i := 0; i < 4; i++ { 104 | err := rb.Put(i) 105 | if !assert.Nil(t, err) { 106 | return 107 | } 108 | } 109 | 110 | var wg sync.WaitGroup 111 | wg.Add(2) 112 | 113 | go func() { 114 | err := rb.Put(4) 115 | assert.Nil(t, err) 116 | wg.Done() 117 | }() 118 | 119 | go func() { 120 | defer wg.Done() 121 | result, err := rb.Get() 122 | if !assert.Nil(t, err) { 123 | return 124 | } 125 | 126 | assert.Equal(t, 0, result) 127 | }() 128 | 129 | wg.Wait() 130 | } 131 | 132 | func TestOffer(t *testing.T) { 133 | rb := NewRingBuffer[any](2) 134 | 135 | ok, err := rb.Offer("foo") 136 | assert.True(t, ok) 137 | assert.Nil(t, err) 138 | ok, err = rb.Offer("bar") 139 | assert.True(t, ok) 140 | assert.Nil(t, err) 141 | ok, err = rb.Offer("baz") 142 | assert.False(t, ok) 143 | assert.Nil(t, err) 144 | 145 | item, err := rb.Get() 146 | assert.Nil(t, err) 147 | assert.Equal(t, "foo", item) 148 | item, err = rb.Get() 149 | assert.Nil(t, err) 150 | assert.Equal(t, "bar", item) 151 | } 152 | 153 | func TestRingGetEmpty(t *testing.T) { 154 | rb := NewRingBuffer[any](3) 155 | 156 | var wg sync.WaitGroup 157 | wg.Add(1) 158 | 159 | // want to kick off this consumer to ensure it blocks 160 | go func() { 161 | wg.Done() 162 | result, err := rb.Get() 163 | assert.Nil(t, err) 164 | assert.Equal(t, 0, result) 165 | wg.Done() 166 | }() 167 | 168 | wg.Wait() 169 | wg.Add(2) 170 | 171 | go func() { 172 | defer wg.Done() 173 | err := rb.Put(0) 174 | assert.Nil(t, err) 175 | }() 176 | 177 | wg.Wait() 178 | } 179 | 180 | func TestRingPollEmpty(t *testing.T) { 181 | rb := NewRingBuffer[any](3) 182 | 183 | _, err := rb.Poll(1) 184 | assert.Equal(t, ErrTimeout, err) 185 | } 186 | 187 | func TestRingPoll(t *testing.T) { 188 | rb := NewRingBuffer[any](10) 189 | 190 | // should be able to Poll() before anything is present, without breaking future Puts 191 | rb.Poll(time.Millisecond) 192 | 193 | rb.Put(`test`) 194 | result, err := rb.Poll(0) 195 | if !assert.Nil(t, err) { 196 | return 197 | } 198 | 199 | assert.Equal(t, `test`, result) 200 | assert.Equal(t, uint64(0), rb.Len()) 201 | 202 | rb.Put(`1`) 203 | rb.Put(`2`) 204 | 205 | result, err = rb.Poll(time.Millisecond) 206 | if !assert.Nil(t, err) { 207 | return 208 | } 209 | 210 | assert.Equal(t, `1`, result) 211 | assert.Equal(t, uint64(1), rb.Len()) 212 | 213 | result, err = rb.Poll(time.Millisecond) 214 | if !assert.Nil(t, err) { 215 | return 216 | } 217 | 218 | assert.Equal(t, `2`, result) 219 | 220 | before := time.Now() 221 | _, err = rb.Poll(5 * time.Millisecond) 222 | // This delta is normally 1-3 ms but running tests in CI with -race causes 223 | // this to run much slower. For now, just bump up the threshold. 224 | assert.InDelta(t, 5, time.Since(before).Seconds()*1000, 10) 225 | assert.Equal(t, ErrTimeout, err) 226 | } 227 | 228 | func TestRingLen(t *testing.T) { 229 | rb := NewRingBuffer[any](4) 230 | assert.Equal(t, uint64(0), rb.Len()) 231 | 232 | rb.Put(1) 233 | assert.Equal(t, uint64(1), rb.Len()) 234 | 235 | rb.Get() 236 | assert.Equal(t, uint64(0), rb.Len()) 237 | 238 | for i := 0; i < 4; i++ { 239 | rb.Put(1) 240 | } 241 | assert.Equal(t, uint64(4), rb.Len()) 242 | 243 | rb.Get() 244 | assert.Equal(t, uint64(3), rb.Len()) 245 | } 246 | 247 | func TestDisposeOnGet(t *testing.T) { 248 | numThreads := 8 249 | var wg sync.WaitGroup 250 | wg.Add(numThreads) 251 | rb := NewRingBuffer[any](4) 252 | var spunUp sync.WaitGroup 253 | spunUp.Add(numThreads) 254 | 255 | for i := 0; i < numThreads; i++ { 256 | go func() { 257 | spunUp.Done() 258 | defer wg.Done() 259 | _, err := rb.Get() 260 | assert.NotNil(t, err) 261 | }() 262 | } 263 | 264 | spunUp.Wait() 265 | rb.Dispose() 266 | 267 | wg.Wait() 268 | assert.True(t, rb.IsDisposed()) 269 | } 270 | 271 | func TestDisposeOnPut(t *testing.T) { 272 | numThreads := 8 273 | var wg sync.WaitGroup 274 | wg.Add(numThreads) 275 | rb := NewRingBuffer[any](4) 276 | var spunUp sync.WaitGroup 277 | spunUp.Add(numThreads) 278 | 279 | // fill up the queue 280 | for i := 0; i < 4; i++ { 281 | rb.Put(i) 282 | } 283 | 284 | // it's now full 285 | for i := 0; i < numThreads; i++ { 286 | go func(i int) { 287 | spunUp.Done() 288 | defer wg.Done() 289 | err := rb.Put(i) 290 | assert.NotNil(t, err) 291 | }(i) 292 | } 293 | 294 | spunUp.Wait() 295 | 296 | rb.Dispose() 297 | 298 | wg.Wait() 299 | 300 | assert.True(t, rb.IsDisposed()) 301 | } 302 | 303 | func BenchmarkRBLifeCycle(b *testing.B) { 304 | rb := NewRingBuffer[any](64) 305 | 306 | counter := uint64(0) 307 | var wg sync.WaitGroup 308 | wg.Add(1) 309 | 310 | go func() { 311 | defer wg.Done() 312 | for { 313 | _, err := rb.Get() 314 | assert.Nil(b, err) 315 | 316 | if atomic.AddUint64(&counter, 1) == uint64(b.N) { 317 | return 318 | } 319 | } 320 | }() 321 | 322 | b.ResetTimer() 323 | 324 | for i := 0; i < b.N; i++ { 325 | rb.Put(i) 326 | } 327 | 328 | wg.Wait() 329 | } 330 | 331 | func BenchmarkRBLifeCycleContention(b *testing.B) { 332 | rb := NewRingBuffer[any](64) 333 | 334 | var wwg sync.WaitGroup 335 | var rwg sync.WaitGroup 336 | wwg.Add(10) 337 | rwg.Add(10) 338 | 339 | for i := 0; i < 10; i++ { 340 | go func() { 341 | for { 342 | _, err := rb.Get() 343 | if err == ErrDisposed { 344 | rwg.Done() 345 | return 346 | } else { 347 | assert.Nil(b, err) 348 | } 349 | } 350 | }() 351 | } 352 | 353 | b.ResetTimer() 354 | 355 | for i := 0; i < 10; i++ { 356 | go func() { 357 | for j := 0; j < b.N; j++ { 358 | rb.Put(j) 359 | } 360 | wwg.Done() 361 | }() 362 | } 363 | 364 | wwg.Wait() 365 | rb.Dispose() 366 | rwg.Wait() 367 | } 368 | 369 | func BenchmarkRBPut(b *testing.B) { 370 | rb := NewRingBuffer[any](uint64(b.N)) 371 | 372 | b.ResetTimer() 373 | 374 | for i := 0; i < b.N; i++ { 375 | ok, err := rb.Offer(i) 376 | if !ok { 377 | b.Fail() 378 | } 379 | if err != nil { 380 | b.Log(err) 381 | b.Fail() 382 | } 383 | } 384 | } 385 | 386 | func BenchmarkRBGet(b *testing.B) { 387 | rb := NewRingBuffer[any](uint64(b.N)) 388 | 389 | for i := 0; i < b.N; i++ { 390 | rb.Offer(i) 391 | } 392 | 393 | b.ResetTimer() 394 | 395 | for i := 0; i < b.N; i++ { 396 | rb.Get() 397 | } 398 | } 399 | 400 | func BenchmarkRBAllocation(b *testing.B) { 401 | for i := 0; i < b.N; i++ { 402 | NewRingBuffer[any](1024) 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /tracing/tracing.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/danthegoodman1/EpicEpoch/utils" 7 | "github.com/rs/zerolog" 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 10 | "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 11 | "go.opentelemetry.io/otel/propagation" 12 | "go.opentelemetry.io/otel/sdk/resource" 13 | "go.opentelemetry.io/otel/sdk/trace" 14 | semconv "go.opentelemetry.io/otel/semconv/v1.21.0" 15 | oteltrace "go.opentelemetry.io/otel/trace" 16 | "os" 17 | "strings" 18 | ) 19 | 20 | var ( 21 | Tracer = otel.Tracer(utils.Env_TracingServiceName) 22 | ) 23 | 24 | // InitTracer creates a new OLTP trace provider instance and registers it as global trace provider. 25 | func InitTracer(ctx context.Context) (tp *trace.TracerProvider, err error) { 26 | logger := zerolog.Ctx(ctx) 27 | var exporter trace.SpanExporter 28 | if utils.Env_OLTPEndpoint != "" { 29 | exporter, err = otlptracegrpc.New(ctx, 30 | otlptracegrpc.WithEndpoint(utils.Env_OLTPEndpoint), 31 | otlptracegrpc.WithInsecure(), 32 | ) 33 | if err != nil { 34 | return nil, err 35 | } 36 | } else { 37 | logger.Warn().Msg("No OLTP endpoint provided, tracing to stdout") 38 | // exporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint()) 39 | exporter, err = stdouttrace.New() 40 | if err != nil { 41 | return nil, err 42 | } 43 | } 44 | hostname, err := os.Hostname() 45 | if err != nil { 46 | return nil, fmt.Errorf("error in os.Hostname: %w", err) 47 | } 48 | tp = trace.NewTracerProvider( 49 | // trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.01))), 50 | trace.WithSampler(trace.AlwaysSample()), 51 | trace.WithBatcher(exporter), 52 | trace.WithResource(resource.NewSchemaless( 53 | semconv.ServiceName(utils.Env_TracingServiceName), 54 | semconv.HostName(hostname), 55 | )), 56 | ) 57 | otel.SetTracerProvider(tp) 58 | otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) 59 | return tp, nil 60 | } 61 | 62 | func CreateSpan(ctx context.Context, tracer oteltrace.Tracer, s string) (context.Context, oteltrace.Span) { 63 | queryName, _, _ := strings.Cut(s, "\n") 64 | opts := []oteltrace.SpanStartOption{ 65 | oteltrace.WithSpanKind(oteltrace.SpanKindServer), 66 | } 67 | ctx, span := tracer.Start(ctx, queryName, opts...) 68 | return ctx, span 69 | } 70 | -------------------------------------------------------------------------------- /utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | var ( 6 | Env = os.Getenv("ENV") 7 | Env_TracingServiceName = os.Getenv("TRACING_SERVICE_NAME") 8 | Env_OLTPEndpoint = os.Getenv("OLTP_ENDPOINT") 9 | 10 | NodeID = uint64(GetEnvOrDefaultInt("NODE_ID", 0)) 11 | 12 | TimestampRequestBuffer = uint64(GetEnvOrDefaultInt("TIMESTAMP_REQUEST_BUFFER", 10000)) 13 | EpochIntervalMS = uint64(GetEnvOrDefaultInt("EPOCH_INTERVAL_MS", 100)) 14 | 15 | EpochIntervalDeadlineLimit = GetEnvOrDefaultInt("EPOCH_DEADLINE_LIMIT", 100) 16 | ) 17 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type PermError string 4 | 5 | func (e PermError) Error() string { 6 | return string(e) 7 | } 8 | 9 | func (e PermError) IsPermanent() bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "os" 11 | "reflect" 12 | "runtime" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/jackc/pgtype" 18 | 19 | "github.com/cockroachdb/cockroach-go/v2/crdb/crdbpgxv5" 20 | "github.com/danthegoodman1/EpicEpoch/gologger" 21 | "github.com/labstack/echo/v4" 22 | gonanoid "github.com/matoous/go-nanoid/v2" 23 | "github.com/rs/zerolog" 24 | "github.com/segmentio/ksuid" 25 | 26 | "github.com/UltimateTournament/backoff/v4" 27 | "github.com/jackc/pgx/v5" 28 | "github.com/jackc/pgx/v5/pgconn" 29 | "github.com/jackc/pgx/v5/pgxpool" 30 | ) 31 | 32 | var logger = gologger.NewLogger() 33 | 34 | func GetEnvOrDefault(env, defaultVal string) string { 35 | e := os.Getenv(env) 36 | if e == "" { 37 | return defaultVal 38 | } else { 39 | return e 40 | } 41 | } 42 | 43 | func GetEnvOrDefaultInt(env string, defaultVal int64) int64 { 44 | e := os.Getenv(env) 45 | if e == "" { 46 | return defaultVal 47 | } else { 48 | intVal, err := strconv.ParseInt(e, 10, 64) 49 | if err != nil { 50 | logger.Error().Msg(fmt.Sprintf("Failed to parse string to int '%s'", env)) 51 | os.Exit(1) 52 | } 53 | 54 | return intVal 55 | } 56 | } 57 | 58 | func GenRandomID(prefix string) string { 59 | return prefix + gonanoid.MustGenerate("abcdefghijklmonpqrstuvwxyzABCDEFGHIJKLMONPQRSTUVWXYZ0123456789", 22) 60 | } 61 | 62 | func GenKSortedID(prefix string) string { 63 | return prefix + ksuid.New().String() 64 | } 65 | 66 | // Cannot use to set something to "", must manually use sq.NullString for that 67 | func SQLNullString(s string) sql.NullString { 68 | return sql.NullString{ 69 | String: s, 70 | Valid: s != "", 71 | } 72 | } 73 | 74 | func SQLNullStringP(s *string) sql.NullString { 75 | return sql.NullString{ 76 | String: Deref(s, ""), 77 | Valid: s != nil, 78 | } 79 | } 80 | 81 | func SQLNullInt64P(s *int64) sql.NullInt64 { 82 | return sql.NullInt64{ 83 | Int64: Deref(s, 0), 84 | Valid: s != nil, 85 | } 86 | } 87 | 88 | func SQLNullBoolP(s *bool) sql.NullBool { 89 | return sql.NullBool{ 90 | Bool: Deref(s, false), 91 | Valid: s != nil, 92 | } 93 | } 94 | 95 | // Cannot use to set something to 0, must manually use sq.NullInt64 for that 96 | func SQLNullInt64(s int64) sql.NullInt64 { 97 | return sql.NullInt64{ 98 | Int64: s, 99 | Valid: true, 100 | } 101 | } 102 | 103 | func GenRandomShortID() string { 104 | // reduced character set that's less probable to mis-type 105 | // change for conflicts is still only 1:128 trillion 106 | return gonanoid.MustGenerate("abcdefghikmonpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789", 8) 107 | } 108 | 109 | func DaysUntil(t time.Time, d time.Weekday) int { 110 | delta := d - t.Weekday() 111 | if delta < 0 { 112 | delta += 7 113 | } 114 | return int(delta) 115 | } 116 | 117 | // this wrapper exists so caller stack skipping works 118 | func ReliableExec(ctx context.Context, pool *pgxpool.Pool, tryTimeout time.Duration, f func(ctx context.Context, conn *pgxpool.Conn) error) error { 119 | return reliableExec(ctx, pool, tryTimeout, func(ctx context.Context, tx *pgxpool.Conn) error { 120 | return f(ctx, tx) 121 | }) 122 | } 123 | 124 | func ReliableExecInTx(ctx context.Context, pool *pgxpool.Pool, tryTimeout time.Duration, f func(ctx context.Context, conn pgx.Tx) error) error { 125 | return reliableExec(ctx, pool, tryTimeout, func(ctx context.Context, tx *pgxpool.Conn) error { 126 | return crdbpgx.ExecuteTx(ctx, tx, pgx.TxOptions{}, func(tx pgx.Tx) error { 127 | return f(ctx, tx) 128 | }) 129 | }) 130 | } 131 | 132 | func reliableExec(ctx context.Context, pool *pgxpool.Pool, tryTimeout time.Duration, f func(ctx context.Context, conn *pgxpool.Conn) error) error { 133 | cfg := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3) 134 | 135 | return backoff.RetryNotify(func() error { 136 | conn, err := pool.Acquire(ctx) 137 | if err != nil { 138 | if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { 139 | return backoff.Permanent(err) 140 | } 141 | return err 142 | } 143 | defer conn.Release() 144 | tryCtx, cancel := context.WithTimeout(ctx, tryTimeout) 145 | defer cancel() 146 | err = f(tryCtx, conn) 147 | if errors.Is(err, pgx.ErrNoRows) { 148 | return backoff.Permanent(err) 149 | } 150 | if IsPermSQLErr(err) { 151 | return backoff.Permanent(err) 152 | } 153 | // not context.DeadlineExceeded as that's expected due to `tryTimeout` 154 | if errors.Is(err, context.Canceled) { 155 | return backoff.Permanent(err) 156 | } 157 | return err 158 | }, cfg, func(err error, d time.Duration) { 159 | reqID, _ := ctx.Value(gologger.ReqIDKey).(string) 160 | l := zerolog.Ctx(ctx).Info().Err(err).CallerSkipFrame(5) 161 | if reqID != "" { 162 | l.Str(string(gologger.ReqIDKey), reqID) 163 | } 164 | l.Msg("ReliableExec retrying") 165 | }) 166 | } 167 | 168 | func IsPermSQLErr(err error) bool { 169 | if err == nil { 170 | return false 171 | } 172 | var pgErr *pgconn.PgError 173 | if errors.As(err, &pgErr) { 174 | if pgErr.Code == "23505" { 175 | // This is a duplicate key - unique constraint 176 | return true 177 | } 178 | if pgErr.Code == "42703" { 179 | // Column does not exist 180 | return true 181 | } 182 | } 183 | return false 184 | } 185 | 186 | func IsUniqueConstraint(err error) bool { 187 | if err == nil { 188 | return false 189 | } 190 | var pgErr *pgconn.PgError 191 | if errors.As(err, &pgErr) { 192 | if pgErr.Code == "23505" { 193 | // This is a duplicate key - unique constraint 194 | return true 195 | } 196 | } 197 | return false 198 | } 199 | 200 | func Ptr[T any](s T) *T { 201 | return &s 202 | } 203 | 204 | type NoEscapeJSONSerializer struct{} 205 | 206 | var _ echo.JSONSerializer = &NoEscapeJSONSerializer{} 207 | 208 | func (d *NoEscapeJSONSerializer) Serialize(c echo.Context, i interface{}, indent string) error { 209 | enc := json.NewEncoder(c.Response()) 210 | enc.SetEscapeHTML(false) 211 | if indent != "" { 212 | enc.SetIndent("", indent) 213 | } 214 | return enc.Encode(i) 215 | } 216 | 217 | // Deserialize reads a JSON from a request body and converts it into an interface. 218 | func (d *NoEscapeJSONSerializer) Deserialize(c echo.Context, i interface{}) error { 219 | // Does not escape <, >, and ? 220 | err := json.NewDecoder(c.Request().Body).Decode(i) 221 | var ute *json.UnmarshalTypeError 222 | var se *json.SyntaxError 223 | if ok := errors.As(err, &ute); ok { 224 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset)).SetInternal(err) 225 | } else if ok := errors.As(err, &se); ok { 226 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())).SetInternal(err) 227 | } 228 | return err 229 | } 230 | 231 | func Deref[T any](ref *T, fallback T) T { 232 | if ref == nil { 233 | return fallback 234 | } 235 | return *ref 236 | } 237 | 238 | func ArrayOrEmpty[T any](ref []T) []T { 239 | if ref == nil { 240 | return make([]T, 0) 241 | } 242 | return ref 243 | } 244 | 245 | var emptyJSON = pgtype.JSONB{Bytes: []byte("{}"), Status: pgtype.Present} 246 | 247 | func OrEmptyJSON(data pgtype.JSONB) pgtype.JSONB { 248 | if data.Status == pgtype.Null { 249 | data = emptyJSON 250 | } 251 | return data 252 | } 253 | 254 | func IfElse[T any](check bool, a T, b T) T { 255 | if check { 256 | return a 257 | } 258 | return b 259 | } 260 | 261 | func OrEmptyArray[T any](a []T) []T { 262 | if a == nil { 263 | return make([]T, 0) 264 | } 265 | return a 266 | } 267 | 268 | func FirstOr[T any](a []T, def T) T { 269 | if len(a) == 0 { 270 | return def 271 | } 272 | return a[0] 273 | } 274 | 275 | var ErrVersionBadFormat = PermError("bad version format") 276 | 277 | // VersionToInt converts a simple semantic version string (e.e. 18.02.66) 278 | func VersionToInt(v string) (int64, error) { 279 | sParts := strings.Split(v, ".") 280 | if len(sParts) > 3 { 281 | return -1, ErrVersionBadFormat 282 | } 283 | var iParts = make([]int64, 3) 284 | for i := range sParts { 285 | vp, err := strconv.ParseInt(sParts[i], 10, 64) 286 | if err != nil { 287 | return -1, fmt.Errorf("error in ParseInt: %s %w", err.Error(), ErrVersionBadFormat) 288 | } 289 | iParts[i] = vp 290 | } 291 | return iParts[0]*10_000*10_000 + iParts[1]*10_000 + iParts[2], nil 292 | } 293 | 294 | // FuncNameFQ returns the fully qualified name of the function. 295 | func FuncNameFQ(f any) string { 296 | return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 297 | } 298 | 299 | // FuncName returns the name of the function, without the package. 300 | func FuncName(f any) string { 301 | fqName := FuncNameFQ(f) 302 | return fqName[strings.LastIndexByte(fqName, '.')+1:] 303 | } 304 | 305 | func AsErr[T error](err error) (te T, ok bool) { 306 | if err == nil { 307 | return te, false 308 | } 309 | return te, errors.As(err, &te) 310 | } 311 | 312 | // IsErr is useful for check for a class of errors (e.g. *serviceerror.WorkflowExecutionAlreadyStarted) instead of a specific error. 313 | // E.g. Temporal doesn't even expose some errors, only their types 314 | func IsErr[T error](err error) bool { 315 | _, ok := AsErr[T](err) 316 | return ok 317 | } 318 | 319 | func MustMarshal(v any) []byte { 320 | b, err := json.Marshal(v) 321 | if err != nil { 322 | panic(err) 323 | } 324 | return b 325 | } 326 | 327 | func ReadWithContext[T any](ctx context.Context, ch chan T) (res T, err error) { 328 | select { 329 | case val := <-ch: 330 | return val, nil 331 | case <-ctx.Done(): 332 | err = ctx.Err() 333 | return 334 | } 335 | } 336 | 337 | func WriteWithContext[T any](ctx context.Context, ch chan<- T, val T) error { 338 | select { 339 | case ch <- val: 340 | return nil 341 | case <-ctx.Done(): 342 | return ctx.Err() 343 | } 344 | } 345 | --------------------------------------------------------------------------------