Repository: tidwall/tile38
Branch: master
Commit: 5b8ce586ab16
Files: 152
Total size: 1.0 MB
Directory structure:
gitextract_ljil7_jn/
├── .github/
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd/
│ ├── tile38-benchmark/
│ │ ├── az/
│ │ │ └── az.go
│ │ └── main.go
│ ├── tile38-cli/
│ │ └── main.go
│ ├── tile38-luamemtest/
│ │ └── main.go
│ └── tile38-server/
│ └── main.go
├── core/
│ ├── commands.go
│ ├── commands.json
│ ├── commands_gen.go
│ ├── commands_test.go
│ ├── gen.sh
│ └── version.go
├── go.mod
├── go.sum
├── internal/
│ ├── bing/
│ │ ├── bing.go
│ │ ├── bing_test.go
│ │ ├── ext.go
│ │ └── ext_test.go
│ ├── buffer/
│ │ ├── buffer.go
│ │ └── buffer_test.go
│ ├── clip/
│ │ ├── clip.go
│ │ ├── clip_test.go
│ │ ├── collection.go
│ │ ├── feature.go
│ │ ├── linestring.go
│ │ ├── point.go
│ │ ├── polygon.go
│ │ └── rect.go
│ ├── collection/
│ │ ├── collection.go
│ │ ├── collection_test.go
│ │ ├── geodesic.go
│ │ └── string.go
│ ├── deadline/
│ │ └── deadline.go
│ ├── endpoint/
│ │ ├── amqp.go
│ │ ├── cfqueue.go
│ │ ├── disque.go
│ │ ├── endpoint.go
│ │ ├── eventHub.go
│ │ ├── grpc.go
│ │ ├── http.go
│ │ ├── kafka.go
│ │ ├── local.go
│ │ ├── mqtt.go
│ │ ├── nats.go
│ │ ├── pubsub.go
│ │ ├── redis.go
│ │ ├── scram_client.go
│ │ └── sqs.go
│ ├── field/
│ │ ├── field.go
│ │ ├── field_test.go
│ │ ├── list_binary.go
│ │ ├── list_struct.go
│ │ └── list_test.go
│ ├── glob/
│ │ ├── glob.go
│ │ ├── glob_test.go
│ │ └── match.go
│ ├── hservice/
│ │ ├── gen.sh
│ │ ├── hservice.pb.go
│ │ └── hservice.proto
│ ├── log/
│ │ ├── log.go
│ │ └── log_test.go
│ ├── object/
│ │ ├── object_binary.go
│ │ ├── object_struct.go
│ │ └── object_test.go
│ ├── server/
│ │ ├── aof.go
│ │ ├── aofmigrate.go
│ │ ├── aofshrink.go
│ │ ├── bson.go
│ │ ├── bson_test.go
│ │ ├── checksum.go
│ │ ├── client.go
│ │ ├── config.go
│ │ ├── crud.go
│ │ ├── dev.go
│ │ ├── expire.go
│ │ ├── expr.go
│ │ ├── expression.go
│ │ ├── fence.go
│ │ ├── follow.go
│ │ ├── group.go
│ │ ├── hooks.go
│ │ ├── json.go
│ │ ├── json_test.go
│ │ ├── keys.go
│ │ ├── live.go
│ │ ├── metrics.go
│ │ ├── monitor.go
│ │ ├── must.go
│ │ ├── must_test.go
│ │ ├── mvt.go
│ │ ├── mvt_test.go
│ │ ├── output.go
│ │ ├── pubqueue.go
│ │ ├── pubsub.go
│ │ ├── readonly.go
│ │ ├── respconn.go
│ │ ├── scan.go
│ │ ├── scanner.go
│ │ ├── scanner_test.go
│ │ ├── scripts.go
│ │ ├── search.go
│ │ ├── server.go
│ │ ├── stats.go
│ │ ├── stats_cpu.go
│ │ ├── stats_cpu_darlin.go
│ │ ├── test.go
│ │ ├── token.go
│ │ └── token_test.go
│ ├── sstring/
│ │ ├── sstring.go
│ │ └── sstring_test.go
│ └── viewer/
│ ├── index.html
│ └── viewer.go
├── scripts/
│ ├── RELEASE.md
│ ├── build.sh
│ ├── docker-push.sh
│ ├── package.sh
│ └── test.sh
└── tests/
├── 107/
│ ├── .gitignore
│ ├── LINK
│ └── main.go
├── 616/
│ └── main.go
├── README.md
├── aof_legacy
├── aof_test.go
├── client_test.go
├── fence_roaming_test.go
├── fence_test.go
├── follower_test.go
├── json_test.go
├── keys_search_test.go
├── keys_test.go
├── metrics_test.go
├── mock_io_test.go
├── mock_test.go
├── monitor_test.go
├── proto_test.go
├── scripts_test.go
├── stats_test.go
├── testcmd_test.go
├── tests_test.go
└── timeout_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CONTRIBUTING.md
================================================
## How to contribute to Tile38
Before getting starting with contributing, please know that we currently use [Tile38 Slack](https://tile38.com/slack) channel for casual questions and user chat.
### Did you find a bug?
- **Do not open up a GitHub issue if the bug is a security vulnerability in Tile38**. Sensitive security-related issues should be reported to [security@tile38.com](mailto:security@tile38.com).
- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/tidwall/tile38/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/tidwall/tile38/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
### Did you fix whitespace, format code, or make a purely cosmetic patch?
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Tile38 will generally not be accepted.
### Do you intend to add a new feature or change an existing one?
- New features will probably not be approved without prior discussion. If you need a specialized feature, make sure to express your willingness to fund the work and maintenance.
- Please do not open a pull request without filing an issue and/or discussing it with a maintainer beforehand.
Thanks!
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Logs**
If applicable, provide logs, panics, system messages to help explain your problem.
**Operating System (please complete the following information):**
- OS: [e.g. Linux / Windows / Mac OS]
- CPU: [e.g. amd64 / arm64 / Apple Silicon / Intel]
- Version: [e.g. 1.19.0]
- Container: [e.g. Docker / None]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Community Support
url: https://tile38.com/slack/
about: Please ask and answer questions here.
- name: Documenation Issues
url: https://github.com/tile38/tile38.github.io/issues
about: Please documenation related issues here.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Please do not open a pull request without first filing an issue and/or discussing the feature directly with the project maintainer.
### Please ensure you adhere to every item in this list
- [ ] This PR was pre-approved by the project maintainer
- [ ] I have self-reviewed the code
- [ ] I have added all necessary tests
### Describe your changes
Please provide detailed description of the changes.
### Issue number and link
Pull request require a prior issue with discussion.
Include the issue number of link here.
================================================
FILE: .github/workflows/main.yml
================================================
name: Go
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.25
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Test
run: make test
- name: Package
run: make package
- name: Docker push
env:
DOCKER_LOGIN: tidwall
DOCKER_USER: tile38
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: ./scripts/docker-push.sh
================================================
FILE: .gitignore
================================================
.DS_Store
tile38-*
!cmd/tile38-*
*.test
data*/
coverage.out
packages/
# Ignore IDE folders
.idea/
.vscode/
================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [1.37.0] = 2026-01-06
### Added
- #791: Add regexp to where expressions using '=~' (@TomDeVito)
### Fixed
- #793: Avoid NaN points and rects for insertions and searches (@krkeshav)
- #789: Fix LineString feature encoding to use all points (@metaxasa)
- 45496a0: Ensure strict resp clients for pubsub
- f6e6fae: Ignore -o json flag for HELLO command
### Updated
- 653e7a0: Bumped github.com/eclipse/paho.mqtt.golang
- 2a86b4b: Bumped golang.org/x/crypto
## [1.36.5] = 2025-10-05
### Added
- 5462de9: Add -o flag to auto set client output to json or resp #779 (@huangpeizhi2018)
### Fixed
- 8c27bd7: Fix 'outside' detect not firing when 'cross' is present
## [1.36.4] = 2025-10-03
### Fixed
- 8c9f56c: Fix leader hanging on sigterm #783 (@dobiadi)
## [1.36.3] = 2025-09-26
### Fixed
- 684ad73: Fix panic, move pubq init cond #782 (@ayaIbrahimm)
- cde0ef7: Do not throw Lua nil access error during scans
## [1.36.2] = 2025-09-04
### Fixed
- 653aea6: Add GeoJSON "properties" member for Lua filtering
- 70c244f: Make the String.match() function case insensitive
## [1.36.1] = 2025-08-28
### Updated
- 53bed30: Update lock strategy for faster writes
- 40f58b0: Kafka Endpoint Improvement #778 (@ifiok)
- 82f4d24: Smaller vector tile sizes for polygons
- 5b21c24: Auto clip geometries at low zoom levels
## [1.36.0] = 2025-07-14
### Added
- 35bde95: Support for Cloudflare Queues endpoint #773 (@tobilg)
- a2afb21: Vector tile support (Mapbox vector tiles) #775
### Updated
- 56c70a1: Bumped github.com/golang-jwt/jwt/v4
- 86b698d: Updated Go dependencies
- f95fcab: Go 1.25
## [1.35.0] = 2025-06-16
### Added
- 4638279: Added NATS Jetstream acks, user credentials, and tls #770 (@VeryStrongFingers)
### Updated
- 3fe57a7; Use atomics for HEALTHZ command to remove contention.
- 3085316: Expose the 'this' property to WHERE clauses.
## [1.34.4] = 2025-05-08
### Fixed
- 977bf25: Fix issue with some startup flags not being read (@salilgupta1)
### Updated
- 86333cd: Bump golang.org/x/net #767
## [1.34.3] = 2025-04-16
### Fixed
- dd98481: Fix channel message delay #763 (@txtsd)
## [1.34.2] = 2025-04-01
### Updated
- a80eaf2: Upgrade to Go 1.24
### Fixed
- 556390e: Fix equality tests with WHERE clause for nested values
- 42e17a1: Fix channel test that sometimes stalls
### Security
- 0ecf097: Bump golang.org/x/net #765
## [1.34.1] = 2025-01-13
### Security
- 927f382: CVE-2024-45338 golang.org/x/net #762 (@tduong2049)
- 07389d8: CVE-2024-45337 golang.org/x/crypto #760
## [1.34.0] = 2024-12-09
### Added
- 459b3e6: Added fifo support for SQS webhooks #759 (@crankycookie)
### Updated
- bed590b: Upgrade to Alpine 3.20 #757 (@tduong2049)
- 2b09508: Upgrade to Go 1.23
## [1.33.4] = 2024-11-05
### Fixed
- aa1caa6: Use zero for undefined fields in where expressions #754 (@unendingblue)
## [1.33.3] = 2024-09-29
### Fixed
- 2b080f4: Include field to INFO replication command output #752 (@Kilowhisky)
## [1.33.2] = 2024-08-02
### Fixed
- 2e3eaa7: Remove extra quote in ROLE command with JSON output #749 (@Kilowhisky)
## [1.33.1] = 2024-07-02
### Fixed
- 193bce1: Fix followers not receiving channel messages #468 (@hibooboo2, @trendstate, @DoisKoh)
## [1.33.0] = 2024-05-03
## Added
- #726: Add EXIST and FEXIST command (@Kilowhisky)
## Fixed
- #738: Add support for CORS in http requests (@Kilowhisky)
- #741: FSET transforms field names to lowercase (@unendingblue, @iwpnd)
- #736: Fix field floating point parsing misrepresentation (@Kilowhisky)
## Updated
- #733: golang.org/x/net
- #724: google.golang.org/protobuf
## [1.32.2] = 2024-02-14
### Fixed
- #714: Fix crash when mixing z-coord dimensionality in a geometry (@prathik)
- #717: Metric expired_keys never incremented (@undeadcat)
- Updated Go runtime to 1.22
## [1.32.1] = 2023-11-20
### Fixed
- #711: Updated dependencies to address security vulnerabilities (@hcmf-wice)
- #706: Add support for 'none' authentication for kafka while still allowing SSL (@Kilowhisky)
- #702: Fix AWS SQS base domain parsing for China region (@LLluma)
## [1.32.0] = 2023-07-31
### Added
- #686: Support the ROLE command (@Kilowhisky)
### Fixed
- #698: Allow AUTH while loading data (@Kilowhisky)
- #694: Allow PING in pubsub (@Kilowhisky)
- #692: Properly support replica_announce properties (@Kilowhisky)
- #691: HEALTHZ should not be AUTH protected (@Kilowhisky)
- #685: Heap size not coming down after objects are removed (@Mukund2900, @iwpnd)
- 0144ca6: Fix missing lock
## [1.31.0] = 2023-05-09
### Added
- #682: Enables cross platform building and pushing of docker images (arm64/amd64) (@eelcocramer)
- #680: Add hostname, port, output, and password env variables to tile38-cli (@ptsilva)
### Fixed
- #606: Only create AMQP queue and bindings for non-topic exchanges (@pacaj2am, @uwer)
- #672: Add graceful shutdown on SIGTERM (@dmitri-zganiaiko)
### Updated
- e9a0500: Upgrade to Go 1.20
- 05b2fb9: Security updates
## [1.30.2] = 2022-12-29
### Fixed
- #668: Fixed fields not persisting (@DucPhan2997)
## [1.30.1] = 2022-12-14
### Fixed
- a8c92a0: Speed up leader/follower replication
- e60ea70: Fix field names converting to lowercase
## [1.30.0] = 2022-11-22
### Added
- bdc80a7: Add WHERE expressions ([more info](https://tile38.com/topics/filter-expressions))
- f24c251: Allow for multiple MATCH patterns
- #652: Allow WHERE for geofence detection
- #657: Add distance to NEARBY IDS response (@iwpnd)
- #663: Lua Sanitization (@program--)
### Fixed
- 023433a: Fix server hang on shared address
- #655: fix: allow host ca sets for SASL and TLS connections (@iwpnd)
### Updated
- 7f2ce23: Upgrade to Go 1.19
- cbfb271: Updated data structures to use Go generics
## [1.29.2] = 2022-11-11
### Fixed
- #664: Fix bad line in inner ring response
## [1.29.1] = 2022-09-21
### Fixed
- fe180dc: Fix follower not authenticating after aofshink
## [1.29.0] = 2022-07-14
### Added
- b883f35: Add pending_events stat
- #643: Expose config and INFO response for replia-priorty (@rave-eserating)
### Fixed
- 8e61f81: Fixed test on Apple silicon
## [1.28.0] = 2022-04-12
### Added
- 10f8564: Added option to "not found" for DEL
- #633: Added "clear" command in the tile38-cli (@CaioDallaqua)
- #634: Added -x flag to tile38-cli (@sign0)
### Fixed
- #636: Workaround for the RESP3 Java lettuce client (@rave-eserating)
- a1cc8e6: Fix eof error for incomplete commands (Theresa D)
### Updated
- fcdb469: Security updates
- #638: Upgrade alpine in Dockerfile (@bb)
- a124738: Upgrade to Go 1.18
- 38ea913: Upgrade prometheous client
- 45fde6a: Upgraded nats dependencies
## [1.27.1] = 2021-01-04
### Fix
- b6833a2: Auto assign server_id for bootstrapped config files
## [1.27.0] = 2021-12-28
### Added
- #629: JSON logging (@iwpnd)
- 241117c: BUFFER option for WITHIN and INTERSECTS, see #79
## [1.26.4] = 2021-10-25
### Hotfix
- a7592f7: Bump version to match changelog
## [1.26.3] = 2021-10-25
### Updated
- a47443a: Upgrade tidwall modules
## [1.26.2] = 2021-10-22
### Added
- #625: Azure EventHub hook support
### Changed
- 11cea4d: Removed vendor directory
## [1.26.1] = 2021-10-01
### Updated
- 9e552c3: Allow some basic client commands before AOF data loads
## [1.26.0] = 2021-09-29
### Added
- #623: Added SECTOR type to spatial searches (@iwpnd, @gmonk)
### Fixed
- #624: AOFSHRINK causes panic on server (@saques)
## [1.25.5] = 2021-09-26
### Fixed
- 8ebcbeb: Fixed Z not matching on where clause for Feature/Point. (@tomquas)
## [1.25.4] = 2021-09-14
### Added
- a737a78: Add unix socket support
### Updated
- 8829b8f: Change hooks collection type from hashmap to btree
- 83094b2: Update hook expiration logic
- c686b87: Return hook ttl with HOOKS request
- 06a92d8: Increase the precision of TIMEOUT
- Upgrade to Go 1.17.1
## [1.25.3] = 2021-08-23
### Fixed
- #621: Fixed a memory leak (@Morgiflute)
### Updated
- Update B-tree library
- Upgrade to Go 1.17
## [1.25.2] = 2021-08-10
### Fixed
- #620: Fixed kafka authentication methods
### Updated
- Upgraded various dependencies
## [1.25.1] = 2021-07-22
### Fixed
- #618: Fixed NEARBY with SPARSE returning too many results. (@nesjett)
## [1.25.0] = 2021-07-12
### Added
- #504: Added TLS support for Nats webhook provider.
- #552: Add CLIPBY subcommand to INTERSECTS/WITHIN. (@rshura)
- #561: Added geofence webhook for GCP Pubsub. (@mscno)
- #615: Add SASL to Kafka provider. (@mathieux51, @iwpnd)
### Updated
- #551: Optimize field value access. (@mpoindexter)
- #554: Improved kNN using geodesic algorithm for NEARBY command. (@mpoindexter)
### Fixed
- #611: Close follower files before finishing aofshrink. (@mzbrau)
- #613: Fix Memory Leak in Kafka Producer. (@iwpnd)
- #616: Fixed expiration logic issue. (@Neuintown)
## [1.24.3] = 2021-06-09
### Fixed
- af43d5a: Hotfix. Fixed invalid healthz output.
## [1.24.2] = 2021-06-07
### Updated
- b610633: Update Go to 1.16
## [1.24.1] = 2021-06-07
### Added
- #609: Added HEALTHZ command (@iwpnd, @stevelacy)
## [1.24.0] = 2021-05-19
### Added
- #604: Added Prometheus metrics (@oliver006)
### Fixed
- #605: Remove deprecated threads flag (@cep-ter)
## [1.23.0] = 2021-04-01
### Updated
- #598: Added TLS Config to Kafka (@iwpnd)
- #599: Include "distance" to output when user specifically requests (@iwpnd)
- #597: Allow for all command types for roaming event (@johnpmayer)
- 31a0fbd: Upgraded dependencies and moved to Go 1.16
### Fixed
- #600: Fix invalid queue.db error (@lokisisland)
- #603: Fix tile38-cli output showing protocol size when piping (@bb)
## [1.22.6] = 2021-02-07
### Updated
- 72dfaae: Updated various dependencies
- 016f397: Updated btree library, optimization
- 4f8bc05: Updated rtree library, optimization
### Fixed
- 6092f73: Better handle connection errors in tile38-cli
## [1.22.5] = 2020-11-09
### Fixed
- 9ce2033: Fixed fields being shuffled after AOFSHRINK
## [1.22.4] = 2020-11-07
### Updated
- 1a7d8d6: Added ENV var for 500 http errors
## [1.22.3] = 2020-10-28
### Updated
- #583: Optimization for non-"cross" based geofence detection (@cliedeman)
- 79bee85: Replaced the underlying B-tree structure.
## [1.22.2] = 2020-10-07
### Fixed
- #230: Fix trailing zeros in AOF at startup
## [1.22.1] = 2020-09-22
### Updated
- 9a34a37: Updated Go version to 1.15
- b1dc463: Updated outdated dependencies (40 in total)
### Added
- #578 Fix "cross" detection not firing in some cases (@feichler-or)
## [1.22.0] = 2020-08-12
### Added
- #571 Added MONITOR command (@tomquas)
### Fixed
- #566: Fixed crash in fenceMatchRoam causing an index out of range panic (@larsw)
- #569: Fixed wrong order for fields with SCAN (@ipsusila)
- #573: Fixed crash with geohash precision above 12 (@superloach)
- 68e2b6d: Updated Kafka client to support (@LeonardoBonacci)
## [1.21.1] = 2020-06-04
### Fixed
- #564: Fix OUTPUT client command requiring authentication. (@LeonardoBonacci)
## [1.20.0] = 2020-05-20
### Updated
- #534: Avoid sorting fields for each written object. (@rshura)
- #544: Match geometry indexing to server config
- b3dc025: Optimize point in ring
- 3718cd7: Added priority option for AMQP endpoints
### Fixed
- #538: DEL geofence notifications are missing the "key" field
- #539: Fixed issue with some features not working with WITHIN (@rshura)
- #540: Fix a concurrent write/read on the server conn map (@mpoindexter)
- #543: Fix clipping empty rings (@rshura)
- #558: Fixed clip test (@mmcloughlin)
- #562: Crashes under go1.14 runtime
- ff48054: Fixed a missing faraway event for roaming geofences
- 5162ac5: Stable sort roam notifications
## [1.19.5] = 2020-02-11
### Fixed
- c567512: Fix packages not vendoring on build
## [1.19.4] = 2020-02-10
### Fixed
- #529: Fix linestring features behave diffrent with CIRCLE (@spierepf)
## [1.19.3] = 2019-12-11
### Fixed
- #513: Fix tile38-cli from freezing with non-quoted geojson (@duartejc)
## [1.19.2] = 2019-11-28
### Fixed
- 6f3716a: Fix false negative for intersecting rings (@thomascoquet)
## [1.19.1] = 2019-11-18
### Updated
- cfc65a1: Refactored repo, moved to Go modules, updated vendor dependencies.
### Fixed
- 9d27533: Fix infinite loop on tile38-cli connection failure.
- #509: Fixed panic on AOFSHRINK. (@jordanferenz)
## [1.19.0] = 2019-11-02
### Added
- #464: Add area expressions TEST command. (@rshura)
### Fixed
- #493: Fix invalid JSON when JSET strings that look like numbers. (@spierepf, @JordanArmstrong)
- #499: Fix invalid PubSub format when output is set to JSON. (@dmvass)
- #500: Fix Tile38-cli not propertly handling quotes. (@vthorsell)
- #502: Fix excessive memory usage for objects with TTLs. commit 23b016d. (@FreakyBytes)
- #503: Fix fprintf type error in stats_cpu.go for non-linux/darwin builds. (@JordanArmstrong)
### Changed
- #505: Update Travi-ci to use Go 1.13.x
## [1.18.0] = 2019-10-09
### Updated
- 639f6e2: Updated the spatial index (R-tree) implementation.
### Fixed
- b092cea: Fixed MQTT blocking on publish/wait.
- #496: Fixed MQTT client ID uniqueness. (@neterror)
- #497: Fixed data race on webhook map with TTLs. (@belek)
- #498: Fixed JSET cancels objects TTL expiry value. (@belek)
## [1.17.6] - 2019-08-22
### Fixed
- 3d96b17: Fixed periodic stop-the-world pauses for systems with large heaps.
## [1.17.5] - 2019-08-22
### Fixed
- #489: Fixed nearby count always one (@jkarjala)
## [1.17.4] - 2019-08-09
### Fixed
- #486: Fixed data condition on connections map (@saltatory)
## [1.17.3] - 2019-08-03
### Fixed
- #483: Fixed lua pool pruning (@rshura)
- f7888c1: Fixed malformed json for chans command
## [1.17.2] - 2019-06-28
### Fixed
- #422: Fixes NEARBY command distance normalization issue (@TrivikrAm-Pamarthi, @melbania)
## [1.17.1] - 2019-05-04
### Fixed
- #448: Fixed missing commands for unsubscribing from active channel (@githubfr)
- #454: Fixed colored output for fatalf (@olevole)
- #453: Fixed nearby json field results showing wrong data (@melbania)
## [1.17.0] - 2019-04-26
### Added
- #446: Added timeouts to allow prepending commands with a TIMEOUT option. (@rshura)
### Fixed
- #440: Fixed crash with fence ROAM (@githubfr)
### Changed
- 3ae5927: Removed experimental evio option
## [1.16.4] - 2019-03-19
### Fixed
- e1a7145: Hotfix. Do not ignore SIGHUP. Use the `--nohup` flag or `nohup` command.
## [1.16.3] - 2019-03-19
### Fixed
- #437: Fixed clients blocking while webook sending. (@tesujiro)
### Added
- #430: Support more SQS credential providers. (@tobilg)
- #435: Added pprof flags for optional memory and cpu diagnostics.
- e47540b: Added auth flag to tile38-benchmark.
- 5335aec: Allow for standard SQS URLs. (@tobilg)
## [1.16.2] - 2019-03-12
### Fixed
- #432: Ignore SIGHUP signals. (@abhit011)
- #433: Fixed nearby inaccuracy with geofence. (@stcktrce)
- #429: Memory optimization, recycle AOF buffer.
- 95a5556: Added periodic yielding to iterators. (@rshura)
## [1.16.1] - 2019-03-01
### Fixed
- #421: Nearby with MATCH is returning invalid results (@nithinkota)
## [1.16.0] - 2019-02-25
### Fixed
- #415: Fixed overlapping geofences sending notifcation to wrong endpoint. (@belek, @s32x)
- #412: Allow SERVER command for Lua scripts. (@1995parham)
- #410: Allow slashes in MQTT Topics (@pstuifzand)
- #409: Fixed bug in polygon clipping. (@rshura)
- 30f903b: Require properties member for geojson features. (@rshura)
### Added
- #409: Added TEST command for executing WITHIN and INTERSECTS on two objects. (@rshura)
- #407: Allow 201 & 202 status code on webhooks. (@s32x)
- #404: Adding more replication data to INFO response. (@s32x)
## [1.15.0] - 2019-01-16
### Fixed
- #403: JSON Output for INFO and CLIENT (@s32x)
- #401: Fixing KEYS command (@s32x)
- #398: Ensuring channel publish order (@s32x)
- d7d0baa: Fix roam fence missing
### Added
- #402: Adding ARM and ARM64 packages (@s32x)
- #399: Add RequireValid and update geojson dependency (@stevelacy)
- #396: Add distance_to function to the tile38 namespace in lua. (@rshura)
- #395: Add RENAME and RENAMENX commands. (@rshura)
## [1.14.4] - 2018-12-03
### Fixed
- #394: Hotfix MultiPolygon intersect failure. (@contra)
- #392: Fix TLS certs missing in Docker. (@vziukas, @s32x)
### Added
- Add extended server stats with SERVER EXT. (@s32x)
- Add Kafka key to match notication key. (@Joey92)
- Add optimized spatial index for fences
## [1.14.3] - 2018-11-20
### Fixed
- Hotfix SCRIPT LOAD not executing from cli. (@rshura)
## [1.14.2] - 2018-11-15
### Fixed
- #386: Fix version not being set at build. (@stevelacy)
## [1.14.1] - 2018-11-15
### Fixed
- #385: Add `version` to SERVER command response (@stevelacy)
- Hotfix replica sync needs flushing (@rshura)
- Fixed a bug where some AOF commands where corrupted during reload
## [1.14.0] - 2018-11-11
### Added
- INTERSECT/WITHIN optimization that may drastically improve searching polygons that have lots of points.
- Faster responses for write operations such as SET/DEL
- NEARBY now always returns objects from nearest to farthest (@rshura)
- kNN haversine distance optimization (@rshura)
- Evio networking beta using the "-evio yes" and "-threads num" flags
### Fixed
- #369: Fix poly in hole query
## [1.13.0] - 2018-08-29
### Added
- eef5f3c: Add geofence notifications over pub/sub channels
- 3a6f366: Add NODWELL keyword to roaming geofences
- #343: Add Nats endpoints (@lennycampino)
- #340: Add MQTT tls/cert options (@tobilg)
- #314: Add CLIP subcommand to INTERSECTS (@rshura)
### Changed
- 3ae26e3: Updated B-tree implementation
- 1d78a41: Updated R-tree implementation
## [1.12.3] - 2018-06-16
### Fixed
- #316: Fix AMQP and AMQPS webhook endpoints to support namespaces (@DeadWisdom)
- #318: Fix aofshrink crash on windows (@abhit011)
- #326: Fix sporadic kNN results when TTL is used (@pmaseberg)
## [1.12.2] - 2018-05-10
### Fixed
- #313: Hotfix intersect returning incorrect results (@stevelacy)
## [1.12.1] - 2018-04-30
### Fixed
- #300: Fix pdelhooks not persisting (@tobilg)
- #293: Fix kafka lockup issue (@Joey92)
- #301: Fix AMQP uri custom params not working (@tobilg)
- #302: Fix tile with zoom level over 63 panics (@rshura)
- b99cd39: Fix Sync hook msg ttl with server time
## [1.12.0] - 2018-04-12
### Added
- 11b42c0: Option to disable AOF or to use a custom path: #220 #223 #297 (@sign0, @umpc, @fmr683, @zhangfeng158)
- #296: Add Meta data to hooks command (@tobilg)
### Changed
- 11b42c0: Updated help menu and show more options
### Fixed
- #295: Intersects returning nothing in some cases (@fils)
- #294: HTTP requests stopped working (@zhangfeng158)
- 0aa04a1: Lotsa package not vendored
## [1.11.1] - 2018-03-16
### Added
- #272: Preserve Docker image tag history (@gechr)
- 9428b84: Added cpu and threads to SERVER stats
### Fixed
- #281: Linestring intersection failure (@contra)
- #280: Filter id match before kNN results (@sweco-semtne)
- #269: Safe atomic ints for arm32 (@gmonk63)
- #267: Optimization for multiploygons intersect queries (@contra)
## [1.11.0] - 2018-03-05
### Added
- #221: Add WHEREEVAL clause to scan/search commands (@rshura)
### Fixed
- #254: Add maxmemory protection to FSET (@rshura)
- #258: Clear expires on reset (@zycbobby)
- #268: Avoid bbox intersect for non-bbox objects (@contra)
## [1.10.1] - 2018-01-17
### Fixed
- #244: Fix issue with points not being detected inside MultiPolygons (@fazlul3003)
- #245: Precalculate and store bboxes for complex objects (@huangpeizhi)
- #246: Fix server crash when receiving zero arg commands (@behrad)
## [1.10.0] - 2017-12-18
### Added
- #221: Sqs endpoint (@lennycampino)
- #226: Lua scripting (@rshura)
- #231: Allow setting multiple fields in a single fset command (@rshura)
- #235: Add json library (encode/decode methods) to lua. (@rshura)
- 26d0083: Update vendoring to use golang/dep
- c8ed7ca: Add WHEREIN command (@rshura)
- d817814: Optimized network pipelining
### Fixed
- #237: Flush to file periodically (@rshura)
- #241: Point match on interior hole (@genesor)
- 920dc3a: Use atomic ints/bools
- 730502d: Set keepalive default to 300 seconds
- 1084c60: Apply limit on top of cursor (@rshura)
## [1.9.1] - 2017-08-16
### Added
- cd05708: Spatial index optimizations
- #208: Debug message for failed webhook notifications (@karnivas)
- #201: New ECHO command (@yorkxiao)
- #183: Include tile38-cli in Docker image (@jchamberlain)
- #121: Allow reads for disconnected followers (@octete)
### Fixed
- 3fae3f7: Allow cursors for kNN queries
- #211: Crash when shrinking AOF on Windows (@icewukong)
- #203: Lifted LIMIT restriction all queries and COUNT keyword (@yorkxiao, @FX-HAO)
- #207: Send empty results for queries on nonexistent keys (@FX-HAO)
- #195: Added kNN overscan ordering (@rshura)
- #199: Apply LIMIT after WHERE clause (@rshura)
- #199: Require Go 1.7 (@rshura)
- #198: Omit fields for Resp when NOFIELDS is used (@rshura)
## [1.9.0] - 2017-04-13
### Added
- #159: AMQP/RabbitMQ webhook support (@m1ome, @paavalan)
- #152: Kafka webhook support (@m1ome)
- #141: Add distances to Geofence notifications
- #54: New benchmark tool (@literadix, @Lars-Meijer, @m1ome)
- #20: Ability to specify pidfile via args (@olevole)
### Fixed
- b1c76d7: tile38-cli auto doesn't auto reconnect
- #156: Use redis-style TTL implementation (@Lars-Meijer, @m1ome)
- #150: Live "inside" fence event not triggering for new object (@phulst)
## [1.8.0] - 2017-02-21
### Added
- #145: TCP Keepalives option (@UriHendler)
- #136: K nearest neighbors for NEARBY command (@m1ome, @tomquas, @joernroeder)
- #139: Added CLIENT command (@UriHendler)
- #133: AutoGC config option (@m1ome, @amorskoy)
### Fixed
- #147: Leaking http hook connections (@mkabischev)
- #143: Duplicate data in hook data (@mkabischev)
## [1.7.5] - 2017-01-13
### Added
- Performance bump for all SET commands, ~10% faster
- Lower memory footprint for large datasets
- #112: Added distance to NEARBY command (@m1ome, @auselen)
- #123: Redis endpoint for webhooks (@m1ome)
- #128: Allow disabling HTTP & WebSocket transport (@m1ome)
### Fixed
- #116: Missing response in TTL json command (@phulst)
- #117: Error in command documentation (@juanpabloaj)
- #118: Unexpected EOF bug with websockets (@m1ome)
- #122: Disque typo timeout handling (@m1ome)
- #127: 3d object searches with 2d geojson area (@damariei)
## [1.7.0] - 2016-12-29
### Added
- #104: PDEL command - Selete objects that match a pattern (@GameFreedom)
- #99: COMMAND keyword for masking geofences by command type (@amorskoy)
- #96: SCAN keyword for roaming geofences
- fba34a9: JSET, JGET, JDEL commands
### Fixed
- #107: Memory leak (@amorskoy)
- #98: Output json fix
## [1.6.0] - 2016-12-11
### Added
- #87: Fencing event grouping (@huangpeizhi)
### Fixed
- #91: Wrong winding order for CirclePolygon function (@antonioromano)
- #73: Corruption for AOFSHRINK (@huangpeizhi)
- #71: Lower memory usage. About 25% savings (@thisisaaronland, @umpc)
- Polygon raycast bug. tidwall/poly#1 (@drewlesueur)
- Added black-box testing
## [1.5.4] - 2016-11-17
### Fixed
- #84: Hotfix - roaming fence deadlock (@tomquas)
## [1.5.3] - 2016-11-16
### Added
- #4: Official docker support (@gordysc)
### Fixed
- #77: NX/XX bug (@damariei)
- #76: Match on prefix star (@GameFreedom, @icewukong)
- #82: Allow for precise search for strings (@GameFreedom)
- #83: Faster congruent modulo for points (@icewukong, @umpc)
## [1.5.2] - 2016-10-20
### Fixed
- #70: Invalid results for INTERSECTS query (@thisisaaronland)
## [1.5.1] - 2016-10-19
### Fixed
- #67: Call the EXPIRE command hangs the server (@PapaStifflera)
- #64: Missing points in 'Nearby' queries (@umpc)
## [1.5.0] - 2016-10-03
### Added
- #61: Optimized queries on 3d objects (@damariei)
- #60: Added [NX|XX] keywords to SET command (@damariei)
- #29: Generalized hook interface (@jeremytregunna)
- GRPC geofence hook support
### Fixed
- #62: Potential Replace Bug Corrupting the Index (@umpc)
- #57: CRLF codes in info after bump from 1.3.0 to 1.4.2 (@olevole)
## [1.4.2] - 2016-08-26
### Fixed
- #49. Allow fragmented pipeline requests (@owaaa)
- #51: Allow multispace delim in native proto (@huangpeizhi)
- #50: MATCH with slashes (@huangpeizhi)
- #43: Linestring nearby search correction (@owaaa)
## [1.4.1] - 2016-08-26
### Added
- #34: Added "BOUNDS key" command (@icewukong)
### Fixed
- #38: Allow for nginx support (@GameFreedom)
- #39: Reset requirepass (@GameFreedom)
## [1.3.0] - 2016-07-22
### Added
- New EXPIRE, PERSISTS, TTL commands. New EX keyword to SET command
- Support for plain strings using `SET ... STRING value.` syntax
- New SEARCH command for finding strings
- Scans can now order descending
### Fixed
- #28: fix windows cli issue (@zhangkaizhao)
## [1.2.0] - 2016-05-24
### Added
- #17: Roaming Geofences for NEARBY command (@ElectroCamel, @davidxv)
- #15: maxmemory config setting (@jrots)
## [1.1.4] - 2016-04-19
### Fixed
- #12: Issue where a newline was being added to HTTP POST requests (@davidxv)
- #13: OBJECT keyword not accepted for WITHIN command (@ray93)
- Panic on missing key for search requests
## [1.1.2] - 2016-04-12
### Fixed
- A glob suffix wildcard can result in extra hits
- The native live geofence sometimes fails connections
## [1.1.0] - 2016-04-02
### Added
- Resp client support. All major programming languages now supported
- Added WITHFIELDS option to GET
- Added OUTPUT command to allow for outputing JSON when using RESP
- Added DETECT option to geofences
### Changed
- New AOF file structure.
- Quicker and safer AOFSHRINK.
### Deprecation Warning
- Native protocol support is being deprecated in a future release in favor of RESP
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thanks for your interest in improving Tile38!
## Issues
- **Found a bug?** Please open an issue with a clear description and precise steps to reproduce.
## Pull Requests
- **Scope.** PRs are welcome for bug fixes and small, low-risk changes.
- **Please coordinate first.** For large changes, such as new features, please file an issue and/or discuss it with a maintainer beforehand.
- **Quality.** Keep PRs focused, include tests (where applicable), and update docs.
## When Not to use a Pull Request
Do NOT submit a Pull Request for:
- Security-related changes.
- Works in progress.
- Changes that require design discussion, or are likely to be controversial.
- Changes needing specialized or cross-subsystem review.
- Large refactors or mechanical tree-wide changes.
- Changes generated by AI tools.
## AI Assisted Contributions
We are not accepting AI assisted code into Tile38 at this time.
This policy may change in the future.
Please reach out if you have any questions or concerns regarding AI and Tile38.
### Contributor Responsibilities
- Monitor your Pull Request and respond to review feedback promptly.
- Pull Requests may be closed if there is no response for **one month**.
By submitting a contribution, you confirm you have the right to do so and grant Tile38 LLC the rights needed to use, modify, and redistribute your contribution as part of the project.
If you have any questions or concerns regarding licensing, please contact us at licensing@tile38.com.
For all other questions, contributing@tile38.com.
================================================
FILE: Dockerfile
================================================
FROM alpine:3.20
ARG VERSION
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache ca-certificates
ADD packages/tile38-$VERSION-$TARGETOS-$TARGETARCH/tile38-server /usr/local/bin
ADD packages/tile38-$VERSION-$TARGETOS-$TARGETARCH/tile38-cli /usr/local/bin
ADD packages/tile38-$VERSION-$TARGETOS-$TARGETARCH/tile38-benchmark /usr/local/bin
RUN addgroup -S tile38 && \
adduser -S -G tile38 tile38 && \
mkdir /data && chown tile38:tile38 /data
VOLUME /data
EXPOSE 9851
CMD ["tile38-server", "-d", "/data"]
================================================
FILE: LICENSE
================================================
Copyright (c) 2016 Josh Baker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Makefile
================================================
all: tile38-server tile38-cli tile38-benchmark tile38-luamemtest
.PHONY: tile38-server
tile38-server:
@./scripts/build.sh tile38-server
.PHONY: tile38-cli
tile38-cli:
@./scripts/build.sh tile38-cli
.PHONY: tile38-benchmark
tile38-benchmark:
@./scripts/build.sh tile38-benchmark
.PHONY: tile38-luamemtest
tile38-luamemtest:
@./scripts/build.sh tile38-luamemtest
test: all
@./scripts/test.sh
package:
@rm -rf packages/
@scripts/package.sh Windows windows amd64
@scripts/package.sh Mac darwin amd64
@scripts/package.sh Linux linux amd64
@scripts/package.sh FreeBSD freebsd amd64
@scripts/package.sh ARM linux arm
@scripts/package.sh ARM64 linux arm64
clean:
rm -rf tile38-server tile38-cli tile38-benchmark tile38-luamemtest
distclean: clean
rm -rf packages/
install: all
cp tile38-server /usr/local/bin
cp tile38-cli /usr/local/bin
cp tile38-benchmark /usr/local/bin
uninstall:
rm -f /usr/local/bin/tile38-server
rm -f /usr/local/bin/tile38-cli
rm -f /usr/local/bin/tile38-benchmark
================================================
FILE: README.md
================================================
Tile38 is an open source (MIT licensed), in-memory geolocation data store, spatial index, and realtime geofencing server.
It supports a variety of object types including lat/lon points, bounding boxes, XYZ tiles, Geohashes, and GeoJSON.
This README is quick start document. You can find detailed documentation at https://tile38.com.
## Features
- Spatial index with [search](#searching) methods such as Nearby, Within, and Intersects.
- Realtime [geofencing](#geofencing) through [webhooks](https://tile38.com/commands/sethook) or [pub/sub channels](#pubsub-channels).
- Object types of [lat/lon](#latlon-point), [bbox](#bounding-box), [Geohash](#geohash), [GeoJSON](#geojson), [QuadKey](#quadkey), and [XYZ tile](#xyz-tile).
- Support for lots of [Clients Libraries](#tile38-client-libraries) written in many different languages.
- Variety of protocols, including [http](#http) (curl), [websockets](#websockets), [telnet](#telnet), and the [Redis RESP](https://redis.io/topics/protocol).
- Server responses are [RESP](https://redis.io/topics/protocol) or [JSON](https://www.json.org).
- Full [command line interface](#cli).
- Leader / follower [replication](#replication).
- In-memory database that persists on disk.
## Components
- tile38-server: The server
- tile38-cli: Command line interface tool
- tile38-benchmark: Server benchmark tool
## Getting Started
### Getting Tile38
Perhaps the easiest way to get the latest Tile38 is to use one of the pre-built release binaries which are available for OSX, Linux, FreeBSD, and Windows. Instructions for using these binaries are on the GitHub [releases page](https://github.com/tidwall/tile38/releases).
### Docker
To run the latest stable version of Tile38:
```
docker pull tile38/tile38
docker run -p 9851:9851 tile38/tile38
```
Visit the [Tile38 hub page](https://hub.docker.com/r/tile38/tile38/) for more information.
### Homebrew (macOS)
Install Tile38 using [Homebrew](https://brew.sh/)
```sh
brew install tile38
tile38-server
```
### Building Tile38
Tile38 can be compiled and used on Linux, OSX, Windows, FreeBSD, and probably others since the codebase is 100% Go. We support both 32 bit and 64 bit systems. [Go](https://golang.org/dl/) must be installed on the build machine.
To build everything simply:
```
$ make
```
To test:
```
$ make test
```
### Running
For command line options invoke:
```
$ ./tile38-server -h
```
To run a single server:
```
$ ./tile38-server
# The tile38 shell connects to localhost:9851
$ ./tile38-cli
> help
```
#### Prometheus Metrics
Tile38 can natively export Prometheus metrics by setting the `--metrics-addr` command line flag (disabled by default). This example exposes the HTTP metrics server on port 4321:
```
# start server and enable Prometheus metrics, listen on local interface only
./tile38-server --metrics-addr=127.0.0.1:4321
# access metrics
curl http://127.0.0.1:4321/metrics
```
If you need to access the `/metrics` endpoint from a different host you'll have to set the flag accordingly, e.g. set it to `0.0.0.0:<>` to listen on all interfaces.
Use the [redis_exporter](https://github.com/oliver006/redis_exporter) for more advanced use cases like extracting key values or running a lua script.
## Playing with Tile38
Basic operations:
```
$ ./tile38-cli
# add a couple of points named 'truck1' and 'truck2' to a collection named 'fleet'.
> set fleet truck1 point 33.5123 -112.2693 # on the Loop 101 in Phoenix
> set fleet truck2 point 33.4626 -112.1695 # on the I-10 in Phoenix
# search the 'fleet' collection.
> scan fleet # returns both trucks in 'fleet'
> nearby fleet point 33.462 -112.268 6000 # search 6 kilometers around a point. returns one truck.
# key value operations
> get fleet truck1 # returns 'truck1'
> del fleet truck2 # deletes 'truck2'
> drop fleet # removes all
```
Tile38 has a ton of [great commands](https://tile38.com/commands).
## Fields
Fields are extra data that belongs to an object. A field is always a double precision floating point. There is no limit to the number of fields that an object can have.
To set a field when setting an object:
```
> set fleet truck1 field speed 90 point 33.5123 -112.2693
> set fleet truck1 field speed 90 field age 21 point 33.5123 -112.2693
```
To set a field when an object already exists:
```
> fset fleet truck1 speed 90
```
To get a field when an object already exists:
```
> fget fleet truck1 speed
```
## Searching
Tile38 has support to search for objects and points that are within or intersects other objects. All object types can be searched including Polygons, MultiPolygons, GeometryCollections, etc.
#### Within
WITHIN searches a collection for objects that are fully contained inside a specified bounding area.
#### Intersects
INTERSECTS searches a collection for objects that intersect a specified bounding area.
#### Nearby
NEARBY searches a collection for objects that intersect a specified radius.
### Search options
**WHERE** - This option allows for filtering out results based on [field](#fields) values. For example ```nearby fleet where speed 70 +inf point 33.462 -112.268 6000``` will return only the objects in the 'fleet' collection that are within the 6 km radius **and** have a field named `speed` that is greater than `70`.
Multiple WHEREs are concatenated as **and** clauses. ```WHERE speed 70 +inf WHERE age -inf 24``` would be interpreted as *speed is over 70 and age is less than 24.*
The default value for a field is always `0`. Thus if you do a WHERE on the field `speed` and an object does not have that field set, the server will pretend that the object does and that the value is Zero.
WHERE expressions support the `=~` operator for regex matching. It uses Go's re2 regular expression engine to match string values in fields or GeoJSON properties within objects. For example, `WHERE properties.name =~ 'truck.*'` filters objects where the 'name' property matches the pattern, and `WHERE field_name =~ 'value.*'` works similarly for fields.
**MATCH** - MATCH is similar to WHERE except that it works on the object id instead of fields. ```nearby fleet match truck* point 33.462 -112.268 6000``` will return only the objects in the 'fleet' collection that are within the 6 km radius **and** have an object id that starts with `truck`. There can be multiple MATCH options in a single search. The MATCH value is a simple [glob pattern](https://en.wikipedia.org/wiki/Glob_(programming)).
**CURSOR** - CURSOR is used to iterate though many objects from the search results. An iteration begins when the CURSOR is set to Zero or not included with the request, and completes when the cursor returned by the server is Zero.
**NOFIELDS** - NOFIELDS tells the server that you do not want field values returned with the search results.
**LIMIT** - LIMIT can be used to limit the number of objects returned for a single search request.
## Geofencing
A geofence is a virtual boundary that can detect when an object enters or exits the area. This boundary can be a radius, bounding box, or a polygon. Tile38 can turn any standard search into a geofence monitor by adding the FENCE keyword to the search.
*Tile38 also allows for [Webhooks](https://tile38.com/commands/sethook) to be assigned to Geofences.*
A simple example:
```
> nearby fleet fence point 33.462 -112.268 6000
```
This command opens a geofence that monitors the 'fleet' collection. The server will respond with:
```
{"ok":true,"live":true}
```
And the connection will be kept open. If any object enters or exits the 6 km radius around `33.462,-112.268` the server will respond in realtime with a message such as:
```
{"command":"set","detect":"enter","id":"truck02","object":{"type":"Point","coordinates":[-112.2695,33.4626]}}
```
The server will notify the client if the `command` is `del | set | drop`.
- `del` notifies the client that an object has been deleted from the collection that is being fenced.
- `drop` notifies the client that the entire collection is dropped.
- `set` notifies the client that an object has been added or updated, and when it's position is detected by the fence.
The `detect` may be one of the following values.
- `inside` is when an object is inside the specified area.
- `outside` is when an object is outside the specified area.
- `enter` is when an object that **was not** previously in the fence has entered the area.
- `exit` is when an object that **was** previously in the fence has exited the area.
- `cross` is when an object that **was not** previously in the fence has entered **and** exited the area.
These can be used when establishing a geofence, to pre-filter responses. For instance, to limit responses to `enter` and `exit` detections:
```
> nearby fleet fence detect enter,exit point 33.462 -112.268 6000
```
### Pub/sub channels
Tile38 supports delivering geofence notifications over pub/sub channels.
To create a static geofence that sends notifications when a bus is within 200 meters of a point and sends to the `busstop` channel:
```
> setchan busstop nearby buses fence point 33.5123 -112.2693 200
```
Subscribe on the `busstop` channel:
```
> subscribe busstop
```
## Object types
All object types except for XYZ Tiles and QuadKeys can be stored in a collection. XYZ Tiles and QuadKeys are reserved for the SEARCH keyword only.
#### Lat/lon point
The most basic object type is a point that is composed of a latitude and a longitude. There is an optional `z` member that may be used for auxiliary data such as elevation or a timestamp.
```
set fleet truck1 point 33.5123 -112.2693 # plain lat/lon
set fleet truck1 point 33.5123 -112.2693 225 # lat/lon with z member
```
#### Bounding box
A bounding box consists of two points. The first being the southwestern most point and the second is the northeastern most point.
```
set fleet truck1 bounds 30 -110 40 -100
```
#### Geohash
A [geohash](https://en.wikipedia.org/wiki/Geohash) is a string representation of a point. With the length of the string indicating the precision of the point.
```
set fleet truck1 hash 9tbnthxzr # this would be equivalent to 'point 33.5123 -112.2693'
```
#### GeoJSON
[GeoJSON](https://tools.ietf.org/html/rfc7946) is an industry standard format for representing a variety of object types including a point, multipoint, linestring, multilinestring, polygon, multipolygon, geometrycollection, feature, and featurecollection.
* All ignored members will not persist.
**Important to note that all coordinates are in Longitude, Latitude order.**
```
set city tempe object {"type":"Polygon","coordinates":[[[0,0],[10,10],[10,0],[0,0]]]}
```
#### XYZ Tile
An XYZ tile is rectangle bounding area on earth that is represented by an X, Y coordinate and a Z (zoom) level.
Check out [maptiler.org](http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/) for an interactive example.
#### QuadKey
A QuadKey used the same coordinate system as an XYZ tile except that the string representation is a string characters composed of 0, 1, 2, or 3. For a detailed explanation checkout [The Bing Maps Tile System](https://msdn.microsoft.com/en-us/library/bb259689.aspx).
## Network protocols
It's recommended to use a [client library](#tile38-client-libraries) or the [Tile38 CLI](#running), but there are times when only HTTP is available or when you need to test from a remote terminal. In those cases we provide an HTTP and telnet options.
#### HTTP
One of the simplest ways to call a tile38 command is to use HTTP. From the command line you can use [curl](https://curl.haxx.se/). For example:
```
# call with request in the body
curl --data "set fleet truck3 point 33.4762 -112.10923" localhost:9851
# call with request in the url path
curl localhost:9851/set+fleet+truck3+point+33.4762+-112.10923
```
#### Websockets
Websockets can be used when you need to Geofence and keep the connection alive. It works just like the HTTP example above, with the exception that the connection stays alive and the data is sent from the server as text websocket messages.
#### Telnet
There is the option to use a plain telnet connection. The default output through telnet is [RESP](https://redis.io/topics/protocol).
```
telnet localhost 9851
set fleet truck3 point 33.4762 -112.10923
+OK
```
The server will respond in [JSON](https://json.org) or [RESP](https://redis.io/topics/protocol) depending on which protocol is used when initiating the first command.
- HTTP and Websockets use JSON.
- Telnet and RESP clients use RESP.
## Tile38 Client Libraries
The following clients are built specifically for Tile38.
Clients that support most Tile38 features are marked with a ⭐️.
- ⭐️ Go: [xjem/t38c](https://github.com/xjem/t38c)
- ⭐️ Node.js: [node-tile38](https://github.com/phulst/node-tile38) ([example code](https://github.com/tidwall/tile38/wiki/Node.js-example-(node-tile38)))
- ⭐️ Python: [pyle38](https://github.com/iwpnd/pyle38)
- ⭐️ TypeScript: [tile38-ts](https://github.com/iwpnd/tile38-ts)
- Go: [cjkreklow/t38c](https://github.com/cjkreklow/t38c)
- Python: [pytile38](https://github.com/mitghi/pytile38)
- Rust: [nazar](https://github.com/younisshah/nazar)
- Swift: [Talon](https://github.com/mikekinney/Talon)
- Java: [tile38-client-java](https://github.com/jamshidrostami/tile38-client-java)
- Java: [tile38-client](https://github.com/HkMoyun/tile38-client)
## Redis Client Libraries
Tile38 uses the [Redis RESP](https://redis.io/topics/protocol) protocol natively.
Therefore most clients that support basic Redis commands will also support Tile38.
- C: [hiredis](https://github.com/redis/hiredis)
- C#: [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis)
- C++: [redox](https://github.com/hmartiro/redox)
- Clojure: [carmine](https://github.com/ptaoussanis/carmine)
- Common Lisp: [CL-Redis](https://github.com/vseloved/cl-redis)
- Erlang: [Eredis](https://github.com/wooga/eredis)
- Go: [go-redis](https://github.com/go-redis/redis) ([example code](https://github.com/tidwall/tile38/wiki/Go-example-(go-redis)))
- Go: [redigo](https://github.com/gomodule/redigo) ([example code](https://github.com/tidwall/tile38/wiki/Go-example-(redigo)))
- Haskell: [hedis](https://github.com/informatikr/hedis)
- Java: [lettuce](https://github.com/mp911de/lettuce) ([example code](https://github.com/tidwall/tile38/wiki/Java-example-(lettuce)))
- Node.js: [node_redis](https://github.com/NodeRedis/node_redis) ([example code](https://github.com/tidwall/tile38/wiki/Node.js-example-(node-redis)))
- Perl: [perl-redis](https://github.com/PerlRedis/perl-redis)
- PHP: [tinyredisclient](https://github.com/ptrofimov/tinyredisclient) ([example code](https://github.com/tidwall/tile38/wiki/PHP-example-(tinyredisclient)))
- PHP: [phpredis](https://github.com/phpredis/phpredis)
- Python: [redis-py](https://github.com/andymccurdy/redis-py) ([example code](https://github.com/tidwall/tile38/wiki/Python-example))
- Ruby: [redic](https://github.com/amakawa/redic) ([example code](https://github.com/tidwall/tile38/wiki/Ruby-example-(redic)))
- Ruby: [redis-rb](https://github.com/redis/redis-rb) ([example code](https://github.com/tidwall/tile38/wiki/Ruby-example-(redis-rb)))
- Rust: [redis-rs](https://github.com/mitsuhiko/redis-rs)
- Scala: [scala-redis](https://github.com/debasishg/scala-redis)
- Swift: [Redbird](https://github.com/czechboy0/Redbird)
## Contact
Josh Baker [@tidwall](https://twitter.com/tidwall)
## License
Tile38 source code is available under the MIT [License](/LICENSE).
================================================
FILE: cmd/tile38-benchmark/az/az.go
================================================
package az
// JSON is GeoJSON
var JSON = `
{ "type": "MultiPolygon", "coordinates": [ [ [ [ -114.635458, 34.876902 ], [ -114.636768000000103, 34.885705 ], [ -114.636725, 34.889107 ], [ -114.635425, 34.895192 ], [ -114.63185, 34.903942 ], [ -114.630877, 34.907263 ], [ -114.630552, 34.911852 ], [ -114.631537, 34.916153 ], [ -114.633237, 34.92123 ], [ -114.633253, 34.924608 ], [ -114.632196, 34.930628 ], [ -114.629753, 34.938684 ], [ -114.629811, 34.94481 ], [ -114.631681, 34.95131 ], [ -114.634274, 34.956662 ], [ -114.634953, 34.958918 ], [ -114.635237000000103, 34.965149 ], [ -114.634607, 34.96906 ], [ -114.629907, 34.980791 ], [ -114.629129, 34.986132 ], [ -114.629443, 34.991825 ], [ -114.630244000000104, 34.99464 ], [ -114.631807, 34.998632 ], [ -114.632665, 34.999806 ], [ -114.635570000000101, 35.005933 ], [ -114.637071, 35.010371 ], [ -114.637769, 35.014948 ], [ -114.63819, 35.022069 ], [ -114.637524, 35.027053 ], [ -114.633715, 35.035602 ], [ -114.629027, 35.042531 ], [ -114.625799, 35.045834 ], [ -114.615902, 35.05272 ], [ -114.610701000000105, 35.055458 ], [ -114.606694, 35.058941 ], [ -114.604715, 35.061744 ], [ -114.603619, 35.064226 ], [ -114.602908, 35.068588 ], [ -114.603175, 35.070445 ], [ -114.604736, 35.07483 ], [ -114.607701, 35.078533 ], [ -114.613132, 35.083097 ], [ -114.61842, 35.086539 ], [ -114.622517, 35.088703 ], [ -114.632053, 35.092559 ], [ -114.63937, 35.094733 ], [ -114.642831, 35.096503 ], [ -114.646579, 35.10082 ], [ -114.646764, 35.101868 ], [ -114.645152, 35.104995 ], [ -114.644354, 35.105903 ], [ -114.641116, 35.108401 ], [ -114.637432000000103, 35.112489 ], [ -114.632282, 35.117088 ], [ -114.628427, 35.118943 ], [ -114.623761, 35.120602 ], [ -114.628993, 35.119411 ], [ -114.624954000000102, 35.120742 ], [ -114.618697, 35.121749 ], [ -114.604007, 35.121252 ], [ -114.60274, 35.121666 ], [ -114.597794, 35.121735 ], [ -114.589787, 35.123522 ], [ -114.584877, 35.125194 ], [ -114.579882, 35.127506 ], [ -114.578263, 35.12881 ], [ -114.577146, 35.130982 ], [ -114.574411, 35.13495 ], [ -114.572597, 35.139557 ], [ -114.573706, 35.142698 ], [ -114.573879, 35.145351 ], [ -114.569529, 35.162317 ], [ -114.56876, 35.172195 ], [ -114.569214, 35.17289 ], [ -114.568989, 35.175085 ], [ -114.569258, 35.183424 ], [ -114.569653, 35.186267 ], [ -114.571404, 35.191026 ], [ -114.572084, 35.200794 ], [ -114.574037, 35.20379 ], [ -114.574233, 35.205481 ], [ -114.574958, 35.206714 ], [ -114.578581, 35.208113 ], [ -114.579535, 35.208911 ], [ -114.579897000000102, 35.21097 ], [ -114.580312, 35.220095 ], [ -114.583523, 35.230348 ], [ -114.58248, 35.233173 ], [ -114.582842, 35.238703 ], [ -114.584993, 35.242717 ], [ -114.586053, 35.248891 ], [ -114.585714, 35.253145 ], [ -114.585768, 35.257743 ], [ -114.586604, 35.262386 ], [ -114.587497, 35.265473 ], [ -114.590513, 35.272334 ], [ -114.593247, 35.284361 ], [ -114.595705, 35.289939 ], [ -114.596682, 35.294557 ], [ -114.597268, 35.299565 ], [ -114.59721, 35.303223 ], [ -114.595163, 35.321883 ], [ -114.595553, 35.326547 ], [ -114.599771, 35.34111 ], [ -114.604607, 35.355239 ], [ -114.606173, 35.359651 ], [ -114.611206, 35.370119 ], [ -114.617698, 35.380131 ], [ -114.618257, 35.382646 ], [ -114.618984, 35.389391 ], [ -114.620887, 35.396867 ], [ -114.621783, 35.39945 ], [ -114.625702, 35.407976 ], [ -114.626765, 35.409644 ], [ -114.629061000000107, 35.411175 ], [ -114.65208, 35.430134 ], [ -114.653817, 35.432853 ], [ -114.654295, 35.436854 ], [ -114.658105, 35.441835 ], [ -114.661747, 35.444735 ], [ -114.662896, 35.446449 ], [ -114.663934, 35.449466 ], [ -114.664215, 35.451707 ], [ -114.663880000000105, 35.454657 ], [ -114.664217, 35.455845 ], [ -114.665142, 35.457331 ], [ -114.666151, 35.458198 ], [ -114.667217, 35.46037 ], [ -114.666769000000102, 35.462085 ], [ -114.665790000000101, 35.463915 ], [ -114.665651, 35.466911 ], [ -114.665988, 35.467985 ], [ -114.667389, 35.469904 ], [ -114.67235, 35.47374 ], [ -114.673164, 35.474814 ], [ -114.673585, 35.475843 ], [ -114.673473000000101, 35.476849 ], [ -114.672074, 35.479709 ], [ -114.671794, 35.480806 ], [ -114.671907, 35.482087 ], [ -114.673534, 35.485675 ], [ -114.676815000000104, 35.489787 ], [ -114.6767040000001, 35.491845 ], [ -114.676257, 35.493103 ], [ -114.677743, 35.495182 ], [ -114.678642, 35.497628 ], [ -114.678587, 35.499846 ], [ -114.678892000000104, 35.501276 ], [ -114.67748, 35.510948 ], [ -114.677143, 35.512945 ], [ -114.675685, 35.51563 ], [ -114.672767, 35.518428 ], [ -114.66954, 35.52079 ], [ -114.668586, 35.521225 ], [ -114.666565, 35.520993 ], [ -114.664601000000104, 35.521519 ], [ -114.663983000000101, 35.522161 ], [ -114.661682, 35.526682 ], [ -114.659886, 35.527919 ], [ -114.657753, 35.530741 ], [ -114.657163, 35.532301 ], [ -114.65677, 35.534964 ], [ -114.657809, 35.536963 ], [ -114.660335, 35.540433 ], [ -114.661457, 35.544062 ], [ -114.66157, 35.545692 ], [ -114.66112, 35.549021 ], [ -114.661963, 35.552604 ], [ -114.661963, 35.555887 ], [ -114.663451, 35.559884 ], [ -114.663535, 35.560963 ], [ -114.662805, 35.564268 ], [ -114.6639, 35.56629 ], [ -114.664433000000102, 35.568426 ], [ -114.666231, 35.571642 ], [ -114.668393, 35.574331 ], [ -114.670022, 35.575596 ], [ -114.671567, 35.576217 ], [ -114.674881, 35.578379 ], [ -114.675751, 35.579459 ], [ -114.675667, 35.580033 ], [ -114.670191, 35.583471 ], [ -114.664209, 35.585944 ], [ -114.660558, 35.586583 ], [ -114.659238, 35.587477 ], [ -114.654518, 35.596609 ], [ -114.653900000000107, 35.598491 ], [ -114.653731, 35.600373 ], [ -114.654489, 35.605173 ], [ -114.653618, 35.607192 ], [ -114.653534, 35.609672 ], [ -114.653927, 35.611739 ], [ -114.655219, 35.614059 ], [ -114.657241, 35.617046 ], [ -114.659461, 35.619552 ], [ -114.660641, 35.620334 ], [ -114.663647, 35.620773 ], [ -114.665389, 35.621556 ], [ -114.666682, 35.623073 ], [ -114.668087, 35.627115 ], [ -114.669015000000101, 35.628861 ], [ -114.672134, 35.633365 ], [ -114.675001, 35.638304 ], [ -114.677615, 35.641774 ], [ -114.679415000000105, 35.643429 ], [ -114.686133, 35.647522 ], [ -114.689001, 35.65028 ], [ -114.689507, 35.651429 ], [ -114.689226, 35.652898 ], [ -114.690494000000101, 35.662657 ], [ -114.690214, 35.665159 ], [ -114.686055, 35.670642 ], [ -114.682317, 35.677825 ], [ -114.680827, 35.682255 ], [ -114.680631000000105, 35.684046 ], [ -114.680997000000104, 35.685929 ], [ -114.682657000000106, 35.688571 ], [ -114.691263, 35.693125 ], [ -114.696214, 35.69655 ], [ -114.701416, 35.701084 ], [ -114.703608, 35.703922 ], [ -114.704501, 35.705993 ], [ -114.704959, 35.706366 ], [ -114.704842, 35.706744 ], [ -114.705597, 35.708274 ], [ -114.705347000000103, 35.708344 ], [ -114.705447, 35.711757 ], [ -114.699405, 35.726929 ], [ -114.697859, 35.731657 ], [ -114.69654, 35.738934 ], [ -114.6964, 35.742653 ], [ -114.696655, 35.746143 ], [ -114.697585, 35.748417 ], [ -114.697726, 35.750966 ], [ -114.696854, 35.752756 ], [ -114.696546, 35.754638 ], [ -114.694267, 35.756633 ], [ -114.694717, 35.757897 ], [ -114.69742, 35.760677 ], [ -114.700266, 35.766879 ], [ -114.701027, 35.76968 ], [ -114.70117, 35.774112 ], [ -114.699036000000106, 35.788046 ], [ -114.699318000000105, 35.79048 ], [ -114.703178, 35.794685 ], [ -114.705827, 35.798889 ], [ -114.71149, 35.80438 ], [ -114.712026, 35.805529 ], [ -114.710534, 35.807525 ], [ -114.709324, 35.81005 ], [ -114.70634, 35.812022 ], [ -114.703665, 35.814614 ], [ -114.700654, 35.822004 ], [ -114.697276, 35.826776 ], [ -114.69553, 35.829897 ], [ -114.695277000000104, 35.831091 ], [ -114.695249, 35.832285 ], [ -114.695757, 35.833387 ], [ -114.701478, 35.839316 ], [ -114.702293, 35.840792 ], [ -114.702339, 35.842151 ], [ -114.703527, 35.841845 ], [ -114.704173, 35.842669 ], [ -114.704203000000106, 35.844274 ], [ -114.706288, 35.846218 ], [ -114.706532, 35.849027 ], [ -114.705856, 35.850508 ], [ -114.703599, 35.852595 ], [ -114.701904, 35.853223 ], [ -114.696581, 35.853727 ], [ -114.69437, 35.854463 ], [ -114.693446, 35.855125 ], [ -114.691456, 35.858661 ], [ -114.6877980000001, 35.860728 ], [ -114.68205, 35.86295 ], [ -114.678186, 35.863311 ], [ -114.672289, 35.865011 ], [ -114.66968, 35.865036 ], [ -114.667471000000106, 35.867061 ], [ -114.662623, 35.869213 ], [ -114.661636, 35.870545 ], [ -114.661636, 35.871233 ], [ -114.663214, 35.873692 ], [ -114.668145, 35.875201 ], [ -114.672009, 35.878018 ], [ -114.678972, 35.88551 ], [ -114.693602, 35.895311 ], [ -114.69454, 35.896587 ], [ -114.696064, 35.896464 ], [ -114.694928, 35.897594 ], [ -114.696132000000105, 35.898662 ], [ -114.697558, 35.89936 ], [ -114.700258000000105, 35.901757 ], [ -114.700769, 35.903064 ], [ -114.703538, 35.906707 ], [ -114.705119, 35.907637 ], [ -114.705991, 35.908598 ], [ -114.7057140000001, 35.909316 ], [ -114.706767, 35.90895 ], [ -114.708112, 35.909933 ], [ -114.709187, 35.916827 ], [ -114.707784, 35.916993 ], [ -114.707398, 35.918057 ], [ -114.70788, 35.919207 ], [ -114.707329000000101, 35.926177 ], [ -114.707603, 35.92795 ], [ -114.712965, 35.932159 ], [ -114.712756, 35.932639 ], [ -114.713413, 35.9319 ], [ -114.713312, 35.933844 ], [ -114.729762, 35.959895 ], [ -114.728496000000106, 35.960395 ], [ -114.728666, 35.961757 ], [ -114.730090000000104, 35.962691 ], [ -114.732456, 35.965891 ], [ -114.736195, 35.969421 ], [ -114.740536, 35.975545 ], [ -114.743494, 35.983553 ], [ -114.743638, 35.985785 ], [ -114.743117, 35.987387 ], [ -114.740043, 35.990534 ], [ -114.739318, 35.991804 ], [ -114.740544, 35.994853 ], [ -114.740815, 35.997464 ], [ -114.741536, 35.99969 ], [ -114.741679, 36.002283 ], [ -114.743163, 36.006722 ], [ -114.743005, 36.00845 ], [ -114.740866, 36.012928 ], [ -114.738555, 36.015223 ], [ -114.728874000000104, 36.021387 ], [ -114.723324, 36.026588 ], [ -114.722214, 36.027964 ], [ -114.722096, 36.028952 ], [ -114.722742, 36.030286 ], [ -114.723673, 36.03123 ], [ -114.727602, 36.033099 ], [ -114.730563, 36.036207 ], [ -114.733417000000102, 36.037913 ], [ -114.735739, 36.038033 ], [ -114.740018, 36.037467 ], [ -114.741262000000106, 36.038044 ], [ -114.742105, 36.039792 ], [ -114.742661, 36.042573 ], [ -114.742479, 36.045697 ], [ -114.741677, 36.047877 ], [ -114.735701, 36.053393 ], [ -114.73508, 36.054435 ], [ -114.735285, 36.056648 ], [ -114.736023000000102, 36.059063 ], [ -114.74006, 36.062437 ], [ -114.7422, 36.067833 ], [ -114.742138, 36.068676 ], [ -114.743542, 36.071037 ], [ -114.748891, 36.074981 ], [ -114.75057, 36.08033 ], [ -114.754032, 36.083093 ], [ -114.754681, 36.085052 ], [ -114.754508, 36.086171 ], [ -114.752836, 36.089393 ], [ -114.750095, 36.092275 ], [ -114.748913000000101, 36.095183 ], [ -114.737497000000104, 36.103102 ], [ -114.734857, 36.104426 ], [ -114.718257, 36.107164 ], [ -114.709269, 36.107396 ], [ -114.706091, 36.108239 ], [ -114.703737, 36.108348 ], [ -114.696981, 36.110297 ], [ -114.693655000000106, 36.112482 ], [ -114.691631, 36.112535 ], [ -114.6880740000001, 36.111457 ], [ -114.684426000000101, 36.109472 ], [ -114.681847000000104, 36.109192 ], [ -114.679775000000106, 36.109874 ], [ -114.678375, 36.110815 ], [ -114.675106, 36.114111 ], [ -114.671867, 36.115964 ], [ -114.664343000000102, 36.1163 ], [ -114.662144, 36.117742 ], [ -114.660448, 36.119999 ], [ -114.658131, 36.124127 ], [ -114.655512, 36.126187 ], [ -114.645728, 36.131995 ], [ -114.641976, 36.13373 ], [ -114.640125, 36.135126 ], [ -114.636862, 36.135552 ], [ -114.635809, 36.13617 ], [ -114.630474, 36.142218 ], [ -114.628462, 36.141822 ], [ -114.627079, 36.140761 ], [ -114.623837, 36.137144 ], [ -114.620605, 36.131759 ], [ -114.618429, 36.130328 ], [ -114.615455, 36.129653 ], [ -114.61324, 36.130266 ], [ -114.609288, 36.132229 ], [ -114.596474, 36.141537 ], [ -114.5930350000001, 36.142674 ], [ -114.589828, 36.143192 ], [ -114.583716, 36.14556 ], [ -114.580707, 36.145987 ], [ -114.578828, 36.147175 ], [ -114.57706, 36.148845 ], [ -114.57109, 36.151099 ], [ -114.561173, 36.150921 ], [ -114.556162, 36.15247 ], [ -114.548742000000104, 36.150697 ], [ -114.543232, 36.151871 ], [ -114.539233, 36.151764 ], [ -114.534478, 36.15023 ], [ -114.532924, 36.149282 ], [ -114.532308, 36.14804 ], [ -114.531091, 36.147644 ], [ -114.52621, 36.148177 ], [ -114.51428, 36.150795 ], [ -114.511218, 36.150576 ], [ -114.508104000000102, 36.149713 ], [ -114.501049, 36.144516 ], [ -114.500236, 36.143226 ], [ -114.499992, 36.141594 ], [ -114.500339, 36.1407 ], [ -114.50108, 36.14006 ], [ -114.50515, 36.138078 ], [ -114.507175, 36.13634 ], [ -114.50921, 36.133247 ], [ -114.508467, 36.129913 ], [ -114.507201, 36.128484 ], [ -114.504715, 36.127188 ], [ -114.501798, 36.126556 ], [ -114.498849, 36.126612 ], [ -114.487635, 36.128656 ], [ -114.483827, 36.12972 ], [ -114.478248, 36.132683 ], [ -114.468674, 36.138889 ], [ -114.465579, 36.139496 ], [ -114.4626, 36.139644 ], [ -114.458945, 36.139214 ], [ -114.456487, 36.138032 ], [ -114.45511, 36.136372 ], [ -114.453798, 36.133586 ], [ -114.451331, 36.129831 ], [ -114.447135, 36.126022 ], [ -114.445042, 36.125346 ], [ -114.443736, 36.125593 ], [ -114.435507, 36.130057 ], [ -114.423114, 36.13735 ], [ -114.418193, 36.142771 ], [ -114.415253, 36.145123 ], [ -114.412491000000102, 36.146511 ], [ -114.40914, 36.147 ], [ -114.405624, 36.146983 ], [ -114.398373, 36.145799 ], [ -114.381479, 36.141349 ], [ -114.379976, 36.141388 ], [ -114.375278, 36.143592 ], [ -114.373745, 36.143722 ], [ -114.370181, 36.142624 ], [ -114.368551, 36.140892 ], [ -114.367381, 36.13852 ], [ -114.365529, 36.136306 ], [ -114.364499, 36.134072 ], [ -114.358968, 36.127795 ], [ -114.348592, 36.121147 ], [ -114.3451, 36.118556 ], [ -114.342601, 36.115878 ], [ -114.34095, 36.113457 ], [ -114.338815, 36.111309 ], [ -114.337264, 36.110428 ], [ -114.334632, 36.106784 ], [ -114.333587, 36.106342 ], [ -114.328801, 36.105902 ], [ -114.325814, 36.103933 ], [ -114.325539, 36.102989 ], [ -114.323458000000102, 36.101186 ], [ -114.320866, 36.096463 ], [ -114.316983, 36.093409 ], [ -114.313086, 36.088816 ], [ -114.306939, 36.082487 ], [ -114.304171, 36.07558 ], [ -114.304384, 36.074019 ], [ -114.305853, 36.071478 ], [ -114.307485, 36.069672 ], [ -114.31242, 36.066117 ], [ -114.3136, 36.064148 ], [ -114.314328, 36.062016 ], [ -114.314427, 36.060523 ], [ -114.313591, 36.059048 ], [ -114.311904, 36.057661 ], [ -114.308624, 36.056976 ], [ -114.300971000000104, 36.05746 ], [ -114.298593, 36.057263 ], [ -114.295941, 36.056168 ], [ -114.293435000000102, 36.0545 ], [ -114.290867, 36.050511 ], [ -114.287992, 36.04907 ], [ -114.284006, 36.048242 ], [ -114.279637, 36.046103 ], [ -114.278166, 36.045819 ], [ -114.273911, 36.046529 ], [ -114.272299, 36.046289 ], [ -114.270862, 36.045523 ], [ -114.269548, 36.043769 ], [ -114.268896, 36.04094 ], [ -114.26922, 36.036807 ], [ -114.268586, 36.035034 ], [ -114.26438, 36.027911 ], [ -114.262388, 36.026107 ], [ -114.259518, 36.024206 ], [ -114.251633, 36.019886 ], [ -114.248419, 36.018556 ], [ -114.246111, 36.017164 ], [ -114.243865, 36.015266 ], [ -114.240439, 36.015245 ], [ -114.238154, 36.014473 ], [ -114.236892, 36.013247 ], [ -114.233443, 36.012835 ], [ -114.231854, 36.013147 ], [ -114.228015, 36.014731 ], [ -114.226459, 36.014606 ], [ -114.224798, 36.013699 ], [ -114.218759, 36.014511 ], [ -114.216609, 36.014336 ], [ -114.214679, 36.014806 ], [ -114.213549, 36.014615 ], [ -114.211932, 36.014834 ], [ -114.206052, 36.016634 ], [ -114.204156, 36.016575 ], [ -114.201227, 36.017751 ], [ -114.200066, 36.017743 ], [ -114.191221, 36.020019 ], [ -114.185860000000105, 36.022266 ], [ -114.179438, 36.024313 ], [ -114.176304, 36.026129 ], [ -114.174683, 36.02667 ], [ -114.164402, 36.026852 ], [ -114.161237, 36.026279 ], [ -114.157344, 36.024966 ], [ -114.1534, 36.02317 ], [ -114.15139, 36.023133 ], [ -114.150225, 36.023515 ], [ -114.145907, 36.027229 ], [ -114.145637, 36.028559 ], [ -114.145672, 36.03297 ], [ -114.144666, 36.034272 ], [ -114.143153, 36.035295 ], [ -114.13826, 36.03719 ], [ -114.137112, 36.038491 ], [ -114.135721, 36.041238 ], [ -114.134841, 36.043873 ], [ -114.134824, 36.045343 ], [ -114.135927, 36.050358 ], [ -114.136206, 36.053232 ], [ -114.1352, 36.056946 ], [ -114.133389, 36.061665 ], [ -114.129768, 36.068484 ], [ -114.125891, 36.072935 ], [ -114.124019, 36.075563 ], [ -114.121186, 36.082755 ], [ -114.119648, 36.085822 ], [ -114.112297, 36.09405 ], [ -114.111998, 36.09491 ], [ -114.1119, 36.095845 ], [ -114.115208, 36.099878 ], [ -114.11707, 36.101177 ], [ -114.119329, 36.10193 ], [ -114.121033, 36.103885 ], [ -114.121779, 36.105699 ], [ -114.12167, 36.108294 ], [ -114.120865, 36.11085 ], [ -114.118497, 36.1139 ], [ -114.116061, 36.115471 ], [ -114.108381, 36.119154 ], [ -114.107419, 36.119401 ], [ -114.100433, 36.119359 ], [ -114.097707, 36.120213 ], [ -114.096994, 36.120823 ], [ -114.092753, 36.132356 ], [ -114.092366, 36.135331 ], [ -114.091701, 36.137303 ], [ -114.089279, 36.140326 ], [ -114.087899, 36.142923 ], [ -114.081234, 36.150208 ], [ -114.07945, 36.154625 ], [ -114.078832, 36.157434 ], [ -114.075641, 36.162523 ], [ -114.071652, 36.170921 ], [ -114.066798, 36.179087 ], [ -114.058662, 36.187835 ], [ -114.052743, 36.190919 ], [ -114.049484, 36.192134 ], [ -114.043944, 36.19335 ], [ -114.043849, 36.245114 ], [ -114.045518, 36.27439 ], [ -114.045559, 36.288837 ], [ -114.045033, 36.30305 ], [ -114.044345, 36.310234 ], [ -114.044051, 36.317628 ], [ -114.044776, 36.331969 ], [ -114.044702, 36.346298 ], [ -114.043034, 36.38587 ], [ -114.042843000000104, 36.448175 ], [ -114.043133, 36.469716 ], [ -114.044816, 36.491343 ], [ -114.045647, 36.521095 ], [ -114.04632, 36.564615 ], [ -114.049935, 36.709521 ], [ -114.049973, 36.738672 ], [ -114.050327, 36.752899 ], [ -114.049879, 36.781909 ], [ -114.050502, 36.895232 ], [ -114.049995, 36.957769 ], [ -114.050600000000102, 37.000396 ], [ -114.0008, 37.000448 ], [ -113.96266, 36.999973 ], [ -113.052912, 36.999983 ], [ -112.875756, 37.000533 ], [ -112.538546, 37.000652 ], [ -112.529846, 37.000899 ], [ -112.36102, 37.001114 ], [ -112.36037, 37.000912 ], [ -112.359329, 37.001117 ], [ -112.125741, 37.001237 ], [ -112.000735, 37.000959 ], [ -111.62572, 37.001401 ], [ -111.616249, 37.001647 ], [ -111.406146, 37.001481 ], [ -111.405895, 37.001702 ], [ -111.313211, 37.000894 ], [ -111.312169, 37.001193 ], [ -111.305843, 37.000776 ], [ -111.278221, 37.000467 ], [ -111.254853, 37.001076 ], [ -111.133718, 37.000779 ], [ -111.081493, 37.002261 ], [ -111.052354, 37.00246 ], [ -111.00182, 37.002293 ], [ -110.625691, 37.003725 ], [ -110.625605, 37.003416 ], [ -110.599512, 37.003448 ], [ -110.509004, 37.003985 ], [ -110.50069, 37.00426 ], [ -110.490908, 37.003566 ], [ -110.478446, 36.999996 ], [ -110.47729, 36.999997 ], [ -110.47019, 36.997997 ], [ -110.023043, 36.998601 ], [ -110.000876, 36.998502 ], [ -110.000677, 36.997968 ], [ -109.969958, 36.997949 ], [ -109.938511, 36.998491 ], [ -109.750669, 36.99816 ], [ -109.743284, 36.998453 ], [ -109.625658, 36.998308 ], [ -109.495338, 36.999105 ], [ -109.362565, 36.999304 ], [ -109.125691, 36.999389 ], [ -109.045223, 36.999084 ], [ -109.045554, 36.645013 ], [ -109.04539, 36.503241 ], [ -109.045946, 36.375002 ], [ -109.045637, 36.374625 ], [ -109.045744, 36.257214 ], [ -109.046024, 36.247197 ], [ -109.045877, 36.188719 ], [ -109.046183, 36.181751 ], [ -109.045726, 36.116908 ], [ -109.045767, 36.033679 ], [ -109.046124, 35.990618 ], [ -109.046009, 35.875012 ], [ -109.046423, 35.624911 ], [ -109.046181, 35.614569 ], [ -109.046795, 35.379918 ], [ -109.046084, 35.249986 ], [ -109.046256, 35.125041 ], [ -109.045842, 34.966076 ], [ -109.046136, 34.875006 ], [ -109.046072, 34.828566 ], [ -109.045626, 34.814226 ], [ -109.046104, 34.799981 ], [ -109.045363, 34.785406 ], [ -109.046087, 34.770963 ], [ -109.046175, 34.520102 ], [ -109.046561, 34.379479 ], [ -109.046337, 34.283639 ], [ -109.046664, 34.250046 ], [ -109.04696, 34.068968 ], [ -109.047006, 34.00005 ], [ -109.046426, 33.875052 ], [ -109.046869, 33.844183 ], [ -109.047145, 33.74001 ], [ -109.046662, 33.625055 ], [ -109.046825, 33.469389 ], [ -109.047309, 33.462131 ], [ -109.046928, 33.4428 ], [ -109.047304, 33.439442 ], [ -109.047298, 33.409774 ], [ -109.046564, 33.375059 ], [ -109.047045, 33.36928 ], [ -109.046827, 33.365271 ], [ -109.047104, 33.27046 ], [ -109.04747, 33.250168 ], [ -109.047122, 33.2408 ], [ -109.047324, 33.18408 ], [ -109.047208, 33.107377 ], [ -109.046905, 33.091931 ], [ -109.047513, 33.059137 ], [ -109.047382, 33.000311 ], [ -109.04711, 32.99225 ], [ -109.047117, 32.777569 ], [ -109.047518, 32.749997 ], [ -109.047796, 32.68263 ], [ -109.047912, 32.500261 ], [ -109.047629, 32.413987 ], [ -109.048323, 32.070887 ], [ -109.048731, 32.028174 ], [ -109.048465, 32.000089 ], [ -109.048738, 31.876905 ], [ -109.049048, 31.870689 ], [ -109.049298, 31.796742 ], [ -109.04899, 31.721922 ], [ -109.049311, 31.544932 ], [ -109.050173, 31.480004 ], [ -109.049934, 31.437907 ], [ -109.050044, 31.332502 ], [ -109.1256, 31.332685 ], [ -109.271744, 31.333942 ], [ -109.49449, 31.334125 ], [ -109.500621, 31.333911 ], [ -109.875628, 31.33405 ], [ -110.000613, 31.333145 ], [ -110.140512, 31.333965 ], [ -110.375635, 31.332896 ], [ -110.460172, 31.332827 ], [ -110.68143, 31.33309 ], [ -110.750638, 31.333636 ], [ -110.795467, 31.33363 ], [ -110.94232, 31.332833 ], [ -111.000643, 31.332177 ], [ -111.074825, 31.332239 ], [ -111.125646, 31.348978 ], [ -111.129451, 31.349979 ], [ -111.357436, 31.423346 ], [ -111.500659, 31.468862 ], [ -111.560194, 31.488138 ], [ -111.659998, 31.519448 ], [ -111.738873, 31.544718 ], [ -111.875674, 31.587657 ], [ -111.979304, 31.620648 ], [ -112.200717, 31.690033 ], [ -112.365328, 31.741078 ], [ -112.375759, 31.743987 ], [ -112.399254, 31.751638 ], [ -112.433246, 31.762162 ], [ -112.737399, 31.855527 ], [ -112.800213, 31.87507 ], [ -112.834233, 31.885137 ], [ -112.871505, 31.896838 ], [ -113.125961, 31.97278 ], [ -113.21163, 32.000061 ], [ -113.211365, 32.000061 ], [ -113.217307, 32.002106 ], [ -113.250731, 32.012405 ], [ -113.493196, 32.088943 ], [ -113.750756, 32.169005 ], [ -113.78168, 32.179034 ], [ -114.250775, 32.323909 ], [ -114.625785, 32.43789 ], [ -114.790245, 32.487505 ], [ -114.813613, 32.494276 ], [ -114.813991000000101, 32.497231 ], [ -114.812316, 32.500054 ], [ -114.813402, 32.501764 ], [ -114.813753000000105, 32.50426 ], [ -114.815185, 32.506023 ], [ -114.81651, 32.506963 ], [ -114.816591000000102, 32.507696 ], [ -114.815591, 32.508612 ], [ -114.814321000000106, 32.509023 ], [ -114.812942, 32.509116 ], [ -114.810159, 32.508383 ], [ -114.807726, 32.508726 ], [ -114.804076, 32.510375 ], [ -114.802833, 32.511749 ], [ -114.802211, 32.513191 ], [ -114.802238, 32.515206 ], [ -114.80367, 32.516374 ], [ -114.807753, 32.516925 ], [ -114.809672, 32.517567 ], [ -114.810374, 32.518391 ], [ -114.809969, 32.520291 ], [ -114.810482, 32.521758 ], [ -114.810969, 32.522444 ], [ -114.812888, 32.52359 ], [ -114.813348000000104, 32.524186 ], [ -114.812645, 32.525399 ], [ -114.811293, 32.526429 ], [ -114.810563, 32.527666 ], [ -114.808617, 32.529017 ], [ -114.80640000000011, 32.531191 ], [ -114.804858, 32.533689 ], [ -114.802559, 32.535521 ], [ -114.802181, 32.536414 ], [ -114.802018, 32.53946 ], [ -114.80237, 32.540078 ], [ -114.804776000000103, 32.541659 ], [ -114.805966, 32.545346 ], [ -114.8058300000001, 32.546354 ], [ -114.803883, 32.548001 ], [ -114.795635, 32.550956 ], [ -114.793769, 32.552329 ], [ -114.792065, 32.555009 ], [ -114.791551, 32.557023 ], [ -114.791523, 32.558602 ], [ -114.792955000000106, 32.562085 ], [ -114.792088, 32.568497 ], [ -114.792358000000107, 32.569091 ], [ -114.793224, 32.569459 ], [ -114.794684, 32.568703 ], [ -114.795253, 32.56662 ], [ -114.79766, 32.564444 ], [ -114.801311, 32.562865 ], [ -114.803664, 32.560689 ], [ -114.806830000000105, 32.55888 ], [ -114.808885, 32.558467 ], [ -114.810318, 32.558628 ], [ -114.812914, 32.560049 ], [ -114.813995, 32.562201 ], [ -114.814212, 32.56369 ], [ -114.813968, 32.566209 ], [ -114.812995, 32.568706 ], [ -114.81148, 32.569781 ], [ -114.804421, 32.572941 ], [ -114.803474, 32.573628 ], [ -114.801877, 32.576009 ], [ -114.801471, 32.578255 ], [ -114.80193, 32.579194 ], [ -114.803879, 32.580889 ], [ -114.803987, 32.582652 ], [ -114.802823, 32.585079 ], [ -114.800441, 32.588079 ], [ -114.799737, 32.592177 ], [ -114.799683, 32.593621 ], [ -114.801251, 32.596232 ], [ -114.801548, 32.598591 ], [ -114.802361, 32.59937 ], [ -114.805932, 32.600721 ], [ -114.8069050000001, 32.60143 ], [ -114.808041, 32.603172 ], [ -114.807879, 32.605416 ], [ -114.809042, 32.608806 ], [ -114.808906, 32.612951 ], [ -114.809555, 32.616203 ], [ -114.808662, 32.619157 ], [ -114.80739, 32.621332 ], [ -114.806821, 32.621721 ], [ -114.799302, 32.625115 ], [ -114.797564, 32.624578 ], [ -114.794102, 32.622475 ], [ -114.792640000000105, 32.621948 ], [ -114.791179, 32.621833 ], [ -114.787715, 32.623573 ], [ -114.782573, 32.624304 ], [ -114.781896, 32.624702 ], [ -114.781766, 32.625613 ], [ -114.782518, 32.628625 ], [ -114.782235, 32.630215 ], [ -114.779215, 32.633578 ], [ -114.77457, 32.63593 ], [ -114.771978, 32.637954 ], [ -114.768199, 32.639874 ], [ -114.764382, 32.642666 ], [ -114.76331, 32.644616 ], [ -114.763512, 32.645995 ], [ -114.764917, 32.648079 ], [ -114.76495, 32.649391 ], [ -114.75831, 32.655178 ], [ -114.751079, 32.659789 ], [ -114.749480000000105, 32.66178 ], [ -114.748000000000104, 32.664184 ], [ -114.748183, 32.665098 ], [ -114.747817, 32.667777 ], [ -114.746383, 32.669853 ], [ -114.745344, 32.67219 ], [ -114.7449, 32.677231 ], [ -114.744349, 32.678935 ], [ -114.740541, 32.684196 ], [ -114.739405, 32.686385 ], [ -114.730453, 32.698844 ], [ -114.72981, 32.700282 ], [ -114.72974, 32.703121 ], [ -114.730086, 32.704298 ], [ -114.728408, 32.706648 ], [ -114.726974, 32.707875 ], [ -114.72534, 32.710369 ], [ -114.72241, 32.713597 ], [ -114.719938, 32.71829 ], [ -114.717695, 32.721547 ], [ -114.715788, 32.727758 ], [ -114.714522, 32.73039 ], [ -114.712629, 32.732678 ], [ -114.710615, 32.733936 ], [ -114.709074, 32.735456 ], [ -114.706114, 32.740986 ], [ -114.70294, 32.744793 ], [ -114.701582000000101, 32.745632 ], [ -114.699247, 32.745098 ], [ -114.695387, 32.742244 ], [ -114.691801, 32.740147 ], [ -114.689282, 32.737927 ], [ -114.688230000000104, 32.73753 ], [ -114.682614, 32.737348 ], [ -114.672025, 32.734951 ], [ -114.665921, 32.734028 ], [ -114.654247, 32.73357 ], [ -114.645353, 32.732139 ], [ -114.635006000000104, 32.731372 ], [ -114.629299, 32.729908 ], [ -114.617479, 32.728243 ], [ -114.61567, 32.728454 ], [ -114.61587, 32.729717 ], [ -114.615501, 32.730044 ], [ -114.615504, 32.731449 ], [ -114.614786, 32.732846 ], [ -114.614787000000106, 32.734076 ], [ -114.615112, 32.734515 ], [ -114.581784, 32.734946 ], [ -114.581736, 32.74232 ], [ -114.564508, 32.742274 ], [ -114.564447, 32.749554 ], [ -114.539224, 32.749812 ], [ -114.539092, 32.756949 ], [ -114.526856, 32.757094 ], [ -114.528443, 32.767276 ], [ -114.529264, 32.769484 ], [ -114.531831, 32.774264 ], [ -114.532432, 32.776922 ], [ -114.532426, 32.778644 ], [ -114.531746, 32.782503 ], [ -114.531669, 32.791185 ], [ -114.529633000000103, 32.795477 ], [ -114.522031, 32.801675 ], [ -114.520385, 32.803576 ], [ -114.520363000000103, 32.804385 ], [ -114.519758, 32.805676 ], [ -114.515389, 32.811439 ], [ -114.510327, 32.816488 ], [ -114.494116, 32.823287 ], [ -114.475892, 32.838693 ], [ -114.468971, 32.845155 ], [ -114.465711, 32.873681 ], [ -114.465172, 32.885295 ], [ -114.463307, 32.899116 ], [ -114.462929, 32.907944 ], [ -114.46365, 32.911682 ], [ -114.464448, 32.913128 ], [ -114.473713, 32.920594 ], [ -114.47664, 32.923628 ], [ -114.477952000000101, 32.925706 ], [ -114.479005, 32.928291 ], [ -114.480783, 32.933678 ], [ -114.480925, 32.936276 ], [ -114.48074, 32.937027 ], [ -114.478456, 32.940555 ], [ -114.474042, 32.94515 ], [ -114.470768, 32.949424 ], [ -114.468536, 32.953922 ], [ -114.467624, 32.956663 ], [ -114.467274, 32.960172 ], [ -114.467367, 32.965384 ], [ -114.468379, 32.970745 ], [ -114.468995, 32.972239 ], [ -114.470511, 32.973858 ], [ -114.472606, 32.974654 ], [ -114.475171, 32.975154 ], [ -114.477308, 32.975023 ], [ -114.479477, 32.974189 ], [ -114.480831, 32.973362 ], [ -114.481315, 32.972064 ], [ -114.484806, 32.971339 ], [ -114.488625, 32.969946 ], [ -114.490129, 32.969884 ], [ -114.492184, 32.971021 ], [ -114.492938, 32.971781 ], [ -114.494212, 32.974262 ], [ -114.495712, 32.980075 ], [ -114.496798, 32.986534 ], [ -114.497052, 32.990206 ], [ -114.49941, 33.00004 ], [ -114.499797, 33.003905 ], [ -114.50287, 33.011154 ], [ -114.506129, 33.017009 ], [ -114.507956, 33.019708 ], [ -114.511343, 33.023455 ], [ -114.5149, 33.026524 ], [ -114.52013, 33.029984 ], [ -114.523578, 33.03096 ], [ -114.538459, 33.033422 ], [ -114.553189, 33.033974 ], [ -114.56085, 33.035285 ], [ -114.5648, 33.035077 ], [ -114.571653, 33.036624 ], [ -114.575161, 33.036541 ], [ -114.578287, 33.035375 ], [ -114.581404, 33.032545 ], [ -114.584765, 33.02823 ], [ -114.586982, 33.026944 ], [ -114.589778, 33.026228 ], [ -114.598093, 33.025384 ], [ -114.601014, 33.02541 ], [ -114.611584, 33.026221 ], [ -114.618788, 33.027202 ], [ -114.625787, 33.029435 ], [ -114.628294, 33.03105 ], [ -114.629732, 33.032546 ], [ -114.63419, 33.039024 ], [ -114.639552, 33.04529 ], [ -114.641621, 33.046894 ], [ -114.64482, 33.048644 ], [ -114.645979, 33.048902 ], [ -114.647049, 33.048416 ], [ -114.649001, 33.046762 ], [ -114.650999, 33.044131 ], [ -114.655038000000104, 33.037106 ], [ -114.657827, 33.033824 ], [ -114.659832, 33.032664 ], [ -114.662317, 33.03267 ], [ -114.66506, 33.033906 ], [ -114.670803, 33.037983 ], [ -114.673659, 33.041896 ], [ -114.67483, 33.045507 ], [ -114.675103000000107, 33.04753 ], [ -114.674295, 33.057169 ], [ -114.679114, 33.061966 ], [ -114.686991, 33.070968 ], [ -114.68912, 33.076121 ], [ -114.689307, 33.079179 ], [ -114.688597000000101, 33.082869 ], [ -114.68902, 33.084035 ], [ -114.692548000000102, 33.085786 ], [ -114.694628, 33.086226 ], [ -114.701165000000103, 33.086368 ], [ -114.70473, 33.087051 ], [ -114.706488, 33.08816 ], [ -114.707819, 33.091102 ], [ -114.708133000000103, 33.094022 ], [ -114.707896000000105, 33.097431 ], [ -114.706175, 33.105334 ], [ -114.703682, 33.113768 ], [ -114.696914, 33.131119 ], [ -114.694858, 33.13346 ], [ -114.690246, 33.137724 ], [ -114.687405, 33.141983 ], [ -114.684907, 33.147823 ], [ -114.682759, 33.154808 ], [ -114.679945, 33.159059 ], [ -114.67935, 33.162433 ], [ -114.68089, 33.169074 ], [ -114.680237, 33.169637 ], [ -114.679115, 33.174608 ], [ -114.675830000000104, 33.18152 ], [ -114.6753590000001, 33.185488 ], [ -114.675189, 33.188178 ], [ -114.678163, 33.199488 ], [ -114.678749, 33.203448 ], [ -114.676072, 33.210835 ], [ -114.673715, 33.219245 ], [ -114.673626, 33.223121 ], [ -114.674479000000105, 33.225504 ], [ -114.678097, 33.2303 ], [ -114.682731, 33.234918 ], [ -114.689421, 33.24525 ], [ -114.689541, 33.246428 ], [ -114.688205, 33.247965 ], [ -114.683253, 33.250034 ], [ -114.67766, 33.254426 ], [ -114.674491000000103, 33.255597 ], [ -114.672924, 33.257042 ], [ -114.672088, 33.258499 ], [ -114.672401, 33.260469 ], [ -114.677032, 33.270169 ], [ -114.680507, 33.273576 ], [ -114.684363000000104, 33.276023 ], [ -114.688599, 33.277861 ], [ -114.694449, 33.279785 ], [ -114.702873, 33.281916 ], [ -114.711197, 33.283341 ], [ -114.717875, 33.285156 ], [ -114.72167, 33.286982 ], [ -114.723259, 33.288079 ], [ -114.731223, 33.302433 ], [ -114.731222000000102, 33.304039 ], [ -114.729904000000104, 33.305745 ], [ -114.726484, 33.308273 ], [ -114.724665000000101, 33.310097 ], [ -114.723623000000103, 33.312109 ], [ -114.71861, 33.315761 ], [ -114.710627, 33.3205 ], [ -114.70787, 33.323316 ], [ -114.705186, 33.327709 ], [ -114.700938, 33.337014 ], [ -114.69935, 33.345692 ], [ -114.699124, 33.349258 ], [ -114.698035, 33.352442 ], [ -114.69817, 33.356575 ], [ -114.699056, 33.361148 ], [ -114.701959, 33.367134 ], [ -114.704201, 33.371238 ], [ -114.706722, 33.37503 ], [ -114.707348, 33.376627 ], [ -114.707485000000105, 33.378375 ], [ -114.707009, 33.380633 ], [ -114.707309, 33.38254 ], [ -114.708407, 33.384142 ], [ -114.713602, 33.388256 ], [ -114.72425, 33.40042 ], [ -114.725292, 33.402341 ], [ -114.725535, 33.404055 ], [ -114.725282, 33.405048 ], [ -114.723829, 33.406531 ], [ -114.722201, 33.407384 ], [ -114.720065000000105, 33.407891 ], [ -114.710878, 33.407254 ], [ -114.701788, 33.408377 ], [ -114.697708, 33.410942 ], [ -114.696805, 33.412087 ], [ -114.696507, 33.414063 ], [ -114.695658, 33.415128 ], [ -114.68795, 33.417934 ], [ -114.673691, 33.419157 ], [ -114.658254, 33.413021 ], [ -114.656735, 33.412813 ], [ -114.652828, 33.412923 ], [ -114.64954, 33.413633 ], [ -114.643302, 33.416746 ], [ -114.635183, 33.422725 ], [ -114.633262, 33.425024 ], [ -114.630903, 33.426754 ], [ -114.62964, 33.428137 ], [ -114.627479, 33.432307 ], [ -114.622283, 33.447558 ], [ -114.622519, 33.450879 ], [ -114.623395000000102, 33.45449 ], [ -114.622918, 33.456561 ], [ -114.618354, 33.462708 ], [ -114.614331, 33.467315 ], [ -114.6137820000001, 33.469049 ], [ -114.612472, 33.470768 ], [ -114.607843000000102, 33.474834 ], [ -114.603396000000103, 33.480631 ], [ -114.601694, 33.481396 ], [ -114.599712, 33.484316 ], [ -114.597283000000104, 33.490653 ], [ -114.593721, 33.495932 ], [ -114.592369, 33.498675 ], [ -114.589246, 33.501813 ], [ -114.580468, 33.506465 ], [ -114.573757, 33.507543 ], [ -114.569533, 33.509219 ], [ -114.560963, 33.516739 ], [ -114.560552, 33.518272 ], [ -114.560835, 33.524334 ], [ -114.560098, 33.526663 ], [ -114.559507, 33.530724 ], [ -114.558898, 33.531819 ], [ -114.542011, 33.542481 ], [ -114.531802, 33.547862 ], [ -114.530401, 33.550099 ], [ -114.525997000000103, 33.551457 ], [ -114.524599, 33.552231 ], [ -114.524215, 33.553068 ], [ -114.52822, 33.559318 ], [ -114.531613, 33.561702 ], [ -114.532333, 33.562879 ], [ -114.533192, 33.565823 ], [ -114.535965000000104, 33.569154 ], [ -114.536784, 33.570959 ], [ -114.537801, 33.575555 ], [ -114.538983, 33.576792 ], [ -114.5403, 33.580615 ], [ -114.540652, 33.582872 ], [ -114.540111, 33.588354 ], [ -114.540664, 33.589789 ], [ -114.540617, 33.591412 ], [ -114.537493, 33.594895 ], [ -114.536777, 33.596394 ], [ -114.531051, 33.604482 ], [ -114.529186, 33.60665 ], [ -114.526782, 33.608831 ], [ -114.523994, 33.60999 ], [ -114.522071, 33.611277 ], [ -114.521845, 33.612544 ], [ -114.522367, 33.614172 ], [ -114.527378, 33.617828 ], [ -114.528578, 33.619994 ], [ -114.52908, 33.621711 ], [ -114.531215, 33.623913 ], [ -114.531034, 33.628213 ], [ -114.530311, 33.629037 ], [ -114.52637, 33.630259 ], [ -114.523802, 33.6347 ], [ -114.525394, 33.640669 ], [ -114.529549000000102, 33.643861 ], [ -114.533215, 33.648443 ], [ -114.533194, 33.65166 ], [ -114.532164, 33.653194 ], [ -114.530583, 33.654461 ], [ -114.525163, 33.655939 ], [ -114.518337, 33.655927 ], [ -114.514559000000105, 33.658014 ], [ -114.514057, 33.660179 ], [ -114.515336, 33.662033 ], [ -114.517112, 33.662877 ], [ -114.520671, 33.662681 ], [ -114.526439, 33.66388 ], [ -114.530267, 33.666821 ], [ -114.532123, 33.669702 ], [ -114.531523, 33.675108 ], [ -114.530348, 33.679245 ], [ -114.527782, 33.682684 ], [ -114.523959, 33.685879 ], [ -114.519113, 33.688473 ], [ -114.512409, 33.691282 ], [ -114.507996, 33.692018 ], [ -114.504993, 33.693022 ], [ -114.502899, 33.694255 ], [ -114.496489, 33.696901 ], [ -114.495719, 33.698454 ], [ -114.495537, 33.701506 ], [ -114.494407, 33.705395 ], [ -114.494197, 33.707922 ], [ -114.494901, 33.71443 ], [ -114.496565, 33.719155 ], [ -114.498133, 33.720634 ], [ -114.500788, 33.722204 ], [ -114.502661, 33.724584 ], [ -114.504176, 33.728055 ], [ -114.506799, 33.730518 ], [ -114.510265, 33.732146 ], [ -114.512348000000102, 33.734214 ], [ -114.510777, 33.737574 ], [ -114.508206, 33.741587 ], [ -114.506, 33.746344 ], [ -114.504483, 33.750998 ], [ -114.50434, 33.756381 ], [ -114.504863, 33.760465 ], [ -114.507089, 33.76793 ], [ -114.516734, 33.788345 ], [ -114.518942, 33.797302 ], [ -114.521555, 33.801982 ], [ -114.524682, 33.808961 ], [ -114.527188, 33.812639 ], [ -114.52805, 33.814963 ], [ -114.527886, 33.815617 ], [ -114.527161, 33.816191 ], [ -114.522714, 33.818979 ], [ -114.520733, 33.822031 ], [ -114.51997, 33.825381 ], [ -114.520465, 33.827778 ], [ -114.523409, 33.835323 ], [ -114.525539, 33.838614 ], [ -114.529597, 33.848063 ], [ -114.530607, 33.85544 ], [ -114.529883, 33.857563 ], [ -114.527069, 33.859429 ], [ -114.525666, 33.860003 ], [ -114.524292, 33.860133 ], [ -114.52287, 33.859965 ], [ -114.518998, 33.858563 ], [ -114.516811000000104, 33.85812 ], [ -114.514673, 33.858638 ], [ -114.511346, 33.86157 ], [ -114.506635, 33.863484 ], [ -114.505638, 33.864276 ], [ -114.503887, 33.865754 ], [ -114.503104, 33.867166 ], [ -114.503017, 33.867998 ], [ -114.503860000000103, 33.871234 ], [ -114.503395, 33.875018 ], [ -114.50434, 33.876882 ], [ -114.510138, 33.880777 ], [ -114.51866, 33.888263 ], [ -114.522768, 33.892583 ], [ -114.524813, 33.895684 ], [ -114.525872, 33.901008 ], [ -114.52569, 33.901428 ], [ -114.524289, 33.901587 ], [ -114.516344, 33.897918 ], [ -114.513715, 33.897959 ], [ -114.510944, 33.899099 ], [ -114.508708, 33.90064 ], [ -114.507988, 33.901813 ], [ -114.50792, 33.903807 ], [ -114.508558, 33.906098 ], [ -114.511511, 33.911092 ], [ -114.514503, 33.914214 ], [ -114.518434, 33.917518 ], [ -114.523393, 33.921221 ], [ -114.525361, 33.922272 ], [ -114.528385, 33.923674 ], [ -114.531107, 33.924633 ], [ -114.534146, 33.925187 ], [ -114.534951, 33.9257 ], [ -114.535853, 33.928103 ], [ -114.535478, 33.934651 ], [ -114.530566, 33.943629 ], [ -114.52868, 33.947817 ], [ -114.526353, 33.950917 ], [ -114.522002, 33.955623 ], [ -114.51586, 33.958106 ], [ -114.51497, 33.958149 ], [ -114.511231, 33.95704 ], [ -114.509568, 33.957264 ], [ -114.499883, 33.961789 ], [ -114.496042, 33.96589 ], [ -114.490398, 33.97062 ], [ -114.488459, 33.972832 ], [ -114.484784, 33.975519 ], [ -114.483097, 33.977745 ], [ -114.482333, 33.980181 ], [ -114.481455, 33.981261 ], [ -114.475907, 33.984424 ], [ -114.471138, 33.98804 ], [ -114.467932, 33.992877 ], [ -114.466187, 33.993465 ], [ -114.461133, 33.993541 ], [ -114.46012, 33.993888 ], [ -114.459258, 33.994711 ], [ -114.458028, 33.997158 ], [ -114.458026000000103, 33.99782 ], [ -114.459184, 34.000016 ], [ -114.460689, 34.001128 ], [ -114.46628, 34.003885 ], [ -114.46731, 34.00519 ], [ -114.467404, 34.00745 ], [ -114.465867000000102, 34.010987 ], [ -114.464525, 34.011982 ], [ -114.463336, 34.012259 ], [ -114.454807, 34.010968 ], [ -114.450206, 34.012574 ], [ -114.446815, 34.01421 ], [ -114.443821, 34.016176 ], [ -114.44054, 34.019329 ], [ -114.438266, 34.022609 ], [ -114.436171, 34.028083 ], [ -114.434949, 34.037784 ], [ -114.435816, 34.04373 ], [ -114.438602, 34.050205 ], [ -114.439406, 34.05381 ], [ -114.43934, 34.057893 ], [ -114.437683, 34.071937 ], [ -114.435907, 34.077491 ], [ -114.434181, 34.087379 ], [ -114.43338, 34.088413 ], [ -114.428026, 34.092787 ], [ -114.426168, 34.097042 ], [ -114.422899, 34.099661 ], [ -114.420499, 34.103466 ], [ -114.415908, 34.107636 ], [ -114.41168, 34.110031 ], [ -114.405916, 34.111468 ], [ -114.401336, 34.111638 ], [ -114.390565, 34.110084 ], [ -114.379223, 34.11599 ], [ -114.369292000000101, 34.117519 ], [ -114.366517, 34.118577 ], [ -114.360402, 34.123577 ], [ -114.359389, 34.125016 ], [ -114.358358, 34.127617 ], [ -114.356372, 34.130428 ], [ -114.35303, 34.13312 ], [ -114.350478, 34.134107 ], [ -114.348051, 34.134457 ], [ -114.336112, 34.134034 ], [ -114.324576, 34.136759 ], [ -114.320777, 34.138635 ], [ -114.312206, 34.144776 ], [ -114.307802, 34.150574 ], [ -114.298168, 34.160321 ], [ -114.292806, 34.166725 ], [ -114.287294, 34.170529 ], [ -114.275267, 34.172149 ], [ -114.26846, 34.170177 ], [ -114.257034, 34.172837 ], [ -114.253444000000101, 34.174129 ], [ -114.244421, 34.179403 ], [ -114.240712000000102, 34.183232 ], [ -114.229715, 34.186928 ], [ -114.227034, 34.188866 ], [ -114.225814, 34.191238 ], [ -114.224941, 34.193896 ], [ -114.225075, 34.196814 ], [ -114.22579, 34.199236 ], [ -114.225861, 34.201774 ], [ -114.225194, 34.203642 ], [ -114.223384, 34.205136 ], [ -114.215454, 34.208956 ], [ -114.211761, 34.211539 ], [ -114.208253, 34.215505 ], [ -114.190876, 34.230858 ], [ -114.17805, 34.239969 ], [ -114.176403, 34.241512 ], [ -114.175948, 34.242695 ], [ -114.175906, 34.245587 ], [ -114.174597, 34.247303 ], [ -114.166536, 34.249647 ], [ -114.166124, 34.250015 ], [ -114.164476, 34.251667 ], [ -114.163867, 34.253349 ], [ -114.163959, 34.255377 ], [ -114.165335, 34.258486 ], [ -114.165249, 34.259125 ], [ -114.164648, 34.259699 ], [ -114.156853, 34.258415 ], [ -114.153346, 34.258289 ], [ -114.147159, 34.259564 ], [ -114.144779, 34.259623 ], [ -114.13545, 34.257886 ], [ -114.133264, 34.258462 ], [ -114.131489000000101, 34.260387 ], [ -114.131211, 34.26273 ], [ -114.134768, 34.268965 ], [ -114.136671, 34.274377 ], [ -114.137045, 34.277018 ], [ -114.13605, 34.280833 ], [ -114.136677, 34.283936 ], [ -114.138365, 34.288564 ], [ -114.139534, 34.295844 ], [ -114.139187, 34.298074 ], [ -114.138167, 34.300936 ], [ -114.138282, 34.30323 ], [ -114.14093, 34.305919 ], [ -114.157206, 34.317862 ], [ -114.157939, 34.320277 ], [ -114.164249, 34.330816 ], [ -114.168807, 34.339513 ], [ -114.172845, 34.344979 ], [ -114.176909, 34.349306 ], [ -114.181145, 34.352186 ], [ -114.185556, 34.354386 ], [ -114.191094, 34.356125 ], [ -114.19648, 34.359187 ], [ -114.199482, 34.361373 ], [ -114.213774, 34.36246 ], [ -114.226107, 34.365916 ], [ -114.229686, 34.368908 ], [ -114.233065, 34.375013 ], [ -114.234275, 34.376662 ], [ -114.245261, 34.385659 ], [ -114.248649, 34.388113 ], [ -114.252739, 34.3901 ], [ -114.25822, 34.395046 ], [ -114.262909, 34.400373 ], [ -114.264317000000105, 34.401329 ], [ -114.267521, 34.402486 ], [ -114.272184, 34.402961 ], [ -114.280108, 34.403147 ], [ -114.282261, 34.403641 ], [ -114.286802, 34.40534 ], [ -114.288663, 34.406623 ], [ -114.290219, 34.408291 ], [ -114.291751, 34.411104 ], [ -114.291903, 34.416231 ], [ -114.292226, 34.417606 ], [ -114.294836, 34.421389 ], [ -114.301016000000104, 34.426807 ], [ -114.308659, 34.430485 ], [ -114.312251, 34.432726 ], [ -114.319054, 34.435831 ], [ -114.32613, 34.437251 ], [ -114.32688, 34.438048 ], [ -114.330669, 34.445295 ], [ -114.332991, 34.448082 ], [ -114.335372, 34.450038 ], [ -114.339627, 34.451435 ], [ -114.342615, 34.451442 ], [ -114.348974, 34.450166 ], [ -114.356025, 34.449744 ], [ -114.363404000000102, 34.447773 ], [ -114.373719, 34.446938 ], [ -114.375789, 34.447798 ], [ -114.378852, 34.450376 ], [ -114.382985, 34.453971 ], [ -114.386699, 34.457911 ], [ -114.387407, 34.460492 ], [ -114.387187, 34.462021 ], [ -114.383525, 34.470405 ], [ -114.381701, 34.47604 ], [ -114.381555, 34.477883 ], [ -114.383038, 34.488903 ], [ -114.382358, 34.495758 ], [ -114.381402, 34.499245 ], [ -114.378124, 34.507288 ], [ -114.378223, 34.516521 ], [ -114.380838, 34.529724 ], [ -114.389603, 34.542982 ], [ -114.398847, 34.559149 ], [ -114.405228, 34.569637 ], [ -114.422382, 34.580711 ], [ -114.435671, 34.593841 ], [ -114.43681, 34.596074 ], [ -114.436363, 34.596797 ], [ -114.427502, 34.599227 ], [ -114.425338, 34.600842 ], [ -114.424326, 34.602338 ], [ -114.424202, 34.610453 ], [ -114.428648, 34.614641 ], [ -114.438739, 34.621455 ], [ -114.439495, 34.625858 ], [ -114.441398, 34.630171 ], [ -114.441525, 34.631529 ], [ -114.440685, 34.634739 ], [ -114.440294, 34.63824 ], [ -114.440519, 34.640066 ], [ -114.441465, 34.64253 ], [ -114.444276, 34.646542 ], [ -114.445664, 34.647542 ], [ -114.449549, 34.651423 ], [ -114.457985, 34.657113 ], [ -114.45821, 34.657994 ], [ -114.457702, 34.659328 ], [ -114.457185, 34.659992 ], [ -114.451785, 34.663891 ], [ -114.450614, 34.665793 ], [ -114.450506, 34.666836 ], [ -114.451532, 34.668605 ], [ -114.454305, 34.671234 ], [ -114.45491, 34.673092 ], [ -114.454881, 34.675639 ], [ -114.455536, 34.677335 ], [ -114.458163, 34.681161 ], [ -114.462178, 34.6858 ], [ -114.463633, 34.68794 ], [ -114.465246, 34.691202 ], [ -114.46809, 34.701786 ], [ -114.46813, 34.704445 ], [ -114.46862, 34.707573 ], [ -114.470477, 34.711368 ], [ -114.47162, 34.712966 ], [ -114.473682, 34.713964 ], [ -114.477297, 34.714514 ], [ -114.482779, 34.714511 ], [ -114.487508, 34.716626 ], [ -114.489287, 34.720155 ], [ -114.490971, 34.724848 ], [ -114.492017, 34.725702 ], [ -114.495858, 34.727956 ], [ -114.499007, 34.729096 ], [ -114.500795, 34.730418 ], [ -114.510292, 34.733582 ], [ -114.514178, 34.735288 ], [ -114.516619, 34.736745 ], [ -114.521048, 34.741173 ], [ -114.522619, 34.74373 ], [ -114.525611, 34.747005 ], [ -114.529079, 34.750006 ], [ -114.529615, 34.750822 ], [ -114.540306, 34.757109 ], [ -114.546884, 34.761802 ], [ -114.552682, 34.766871 ], [ -114.558653, 34.773852 ], [ -114.563979, 34.782597 ], [ -114.565184, 34.785976 ], [ -114.569383, 34.791568 ], [ -114.571010000000101, 34.794294 ], [ -114.574474, 34.804214 ], [ -114.574694, 34.807471 ], [ -114.576452, 34.8153 ], [ -114.578681000000103, 34.820977 ], [ -114.581126, 34.826115 ], [ -114.586842, 34.835672 ], [ -114.592339, 34.841153 ], [ -114.600653, 34.847361 ], [ -114.604255, 34.849573 ], [ -114.619878, 34.856873 ], [ -114.623939, 34.859738 ], [ -114.628276, 34.863596 ], [ -114.630682000000107, 34.866352 ], [ -114.633051, 34.869971 ], [ -114.634382, 34.87289 ], [ -114.635176, 34.875003 ], [ -114.635458, 34.876902 ] ] ] ] }
`
================================================
FILE: cmd/tile38-benchmark/main.go
================================================
package main
import (
"fmt"
"log"
"math"
"math/rand"
"net"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tidwall/redbench"
"github.com/tidwall/redcon"
"github.com/tidwall/tile38/cmd/tile38-benchmark/az"
"github.com/tidwall/tile38/core"
)
var (
hostname = "127.0.0.1"
port = 9851
auth = ""
clients = 50
requests = 100000
quiet = false
pipeline = 1
csv = false
json = false
allTests = "PING,SET,GET,INTERSECTS,WITHIN,NEARBY,EVAL"
tests = allTests
redis = false
)
var addr string
func showHelp() bool {
gitsha := ""
if core.GitSHA == "" || core.GitSHA == "0000000" {
gitsha = ""
} else {
gitsha = " (git:" + core.GitSHA + ")"
}
fmt.Fprintf(os.Stdout, "tile38-benchmark %s%s\n\n", core.Version, gitsha)
fmt.Fprintf(os.Stdout, "Usage: tile38-benchmark [-h ] [-p ] [-c ] [-n ]\n")
fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s)\n", hostname)
fmt.Fprintf(os.Stdout, " -p Server port (default: %d)\n", port)
fmt.Fprintf(os.Stdout, " -a Password for Tile38 Auth\n")
fmt.Fprintf(os.Stdout, " -c Number of parallel connections (default %d)\n", clients)
fmt.Fprintf(os.Stdout, " -n Total number or requests (default %d)\n", requests)
fmt.Fprintf(os.Stdout, " -q Quiet. Just show query/sec values\n")
fmt.Fprintf(os.Stdout, " -P Pipeline requests. Default 1 (no pipeline).\n")
fmt.Fprintf(os.Stdout, " -t Only run the comma separated list of tests. The test\n")
fmt.Fprintf(os.Stdout, " names are the same as the ones produced as output.\n")
fmt.Fprintf(os.Stdout, " --csv Output in CSV format.\n")
fmt.Fprintf(os.Stdout, " --json Request JSON responses (default is RESP output)\n")
fmt.Fprintf(os.Stdout, " --redis Runs against a Redis server\n")
fmt.Fprintf(os.Stdout, "\n")
return false
}
func parseArgs() bool {
defer func() {
if v := recover(); v != nil {
if v, ok := v.(string); ok && v == "bad arg" {
showHelp()
}
}
}()
args := os.Args[1:]
readArg := func(arg string) string {
if len(args) == 0 {
panic("bad arg")
}
var narg = args[0]
args = args[1:]
return narg
}
readIntArg := func(arg string) int {
n, err := strconv.ParseUint(readArg(arg), 10, 64)
if err != nil {
panic("bad arg")
}
return int(n)
}
badArg := func(arg string) bool {
fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg)
return false
}
for len(args) > 0 {
arg := readArg("")
if arg == "--help" || arg == "-?" {
return showHelp()
}
if !strings.HasPrefix(arg, "-") {
args = append([]string{arg}, args...)
break
}
switch arg {
default:
return badArg(arg)
case "-h":
hostname = readArg(arg)
case "-p":
port = readIntArg(arg)
case "-a":
auth = readArg(arg)
case "-c":
clients = readIntArg(arg)
if clients <= 0 {
clients = 1
}
case "-n":
requests = readIntArg(arg)
if requests <= 0 {
requests = 0
}
case "-q":
quiet = true
case "-P":
pipeline = readIntArg(arg)
if pipeline <= 0 {
pipeline = 1
}
case "-t":
tests = readArg(arg)
case "--csv":
csv = true
case "--json":
json = true
case "--redis":
redis = true
}
}
return true
}
func fillOpts() *redbench.Options {
opts := *redbench.DefaultOptions
opts.CSV = csv
opts.Clients = clients
opts.Pipeline = pipeline
opts.Quiet = quiet
opts.Requests = requests
opts.Stderr = os.Stderr
opts.Stdout = os.Stdout
return &opts
}
func randPoint() (lat, lon float64) {
return rand.Float64()*180 - 90, rand.Float64()*360 - 180
}
func isValidRect(minlat, minlon, maxlat, maxlon float64) bool {
return minlat > -90 && maxlat < 90 && minlon > -180 && maxlon < 180
}
func randRect(meters float64) (minlat, minlon, maxlat, maxlon float64) {
for {
lat, lon := randPoint()
maxlat, _ = destinationPoint(lat, lon, meters, 0)
_, maxlon = destinationPoint(lat, lon, meters, 90)
minlat, _ = destinationPoint(lat, lon, meters, 180)
_, minlon = destinationPoint(lat, lon, meters, 270)
if isValidRect(minlat, minlon, maxlat, maxlon) {
return
}
}
}
func prepFn(conn net.Conn) bool {
var resp [64]byte
conn.Write([]byte("CONFIG GET requirepass\r\n"))
n, err := conn.Read(resp[:])
if err != nil {
log.Fatal(err)
}
if string(resp[:n]) == "-ERR authentication required\r\n" {
if auth == "" {
log.Fatal("invalid auth")
} else {
cmd := redcon.AppendArray(nil, 2)
cmd = redcon.AppendBulkString(cmd, "AUTH")
cmd = redcon.AppendBulkString(cmd, auth)
conn.Write(cmd)
n, err := conn.Read(resp[:])
if err != nil || string(resp[:n]) != "+OK\r\n" {
log.Fatal("invalid auth")
}
}
} else if auth != "" {
log.Fatal("invalid auth")
}
if json {
conn.Write([]byte("output json\r\n"))
conn.Read(make([]byte, 64))
}
return true
}
func main() {
rand.Seed(time.Now().UnixNano())
if !parseArgs() {
return
}
opts := fillOpts()
addr = fmt.Sprintf("%s:%d", hostname, port)
testsArr := strings.Split(allTests, ",")
var subtract bool
var add bool
for _, test := range strings.Split(tests, ",") {
if strings.HasPrefix(test, "-") {
if add {
os.Stderr.Write([]byte("test flag cannot mix add and subtract\n"))
os.Exit(1)
}
subtract = true
for i := range testsArr {
if strings.EqualFold(testsArr[i], test[1:]) {
testsArr = append(testsArr[:i], testsArr[i+1:]...)
break
}
}
} else if subtract {
add = true
os.Stderr.Write([]byte("test flag cannot mix add and subtract\n"))
os.Exit(1)
}
}
if !subtract {
testsArr = strings.Split(tests, ",")
}
for _, test := range testsArr {
switch strings.ToUpper(strings.TrimSpace(test)) {
case "PING":
redbench.Bench("PING", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf, "PING")
},
)
case "GEOADD":
//GEOADD key longitude latitude member
if redis {
var i int64
redbench.Bench("GEOADD", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
lat, lon := randPoint()
return redbench.AppendCommand(buf, "GEOADD", "key:bench",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"id:"+strconv.FormatInt(i, 10),
)
},
)
}
case "SET", "SET-POINT", "SET-RECT", "SET-LINE", "SET-STRING":
if redis {
redbench.Bench("SET", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf, "SET", "key:__rand_int__", "xxx")
},
)
} else {
var i int64
switch strings.ToUpper(strings.TrimSpace(test)) {
case "SET", "SET-POINT":
redbench.Bench("SET (point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
lat, lon := randPoint()
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "SET", "SET-RECT":
redbench.Bench("SET (rect)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
minlat, minlon, maxlat, maxlon := randRect(10000)
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64),
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "SET", "SET-LINE":
redbench.Bench("SET (line)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
alat, alon, blat, blon := randRect(10000)
if rand.Int()%2 == 0 {
alat, alon, blat, blon = blat, blon, alat, alon
}
linestring := fmt.Sprintf(`{"type":"LineString","coordinates":[[%f,%f],[%f,%f]]}`, alon, alat, blon, blat)
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT",
linestring,
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "SET", "SET-STRING":
redbench.Bench("SET (string)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "SET", "key:bench", "id:"+strconv.FormatInt(i, 10), "STRING", "xxx")
},
)
}
}
case "GET":
if redis {
redbench.Bench("GET", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf, "GET", "key:__rand_int__")
},
)
} else {
var i int64
redbench.Bench("GET (point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "POINT")
},
)
redbench.Bench("GET (rect)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "BOUNDS")
},
)
redbench.Bench("GET (string)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "GET", "key:bench", "id:"+strconv.FormatInt(i, 10), "OBJECT")
},
)
}
case "INTERSECTS",
"INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-1000", "INTERSECTS-BOUNDS-10000", "INTERSECTS-BOUNDS-100000",
"INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000", "INTERSECTS-CIRCLE-10000", "INTERSECTS-CIRCLE-100000",
"INTERSECTS-AZ":
if redis {
break
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000":
redbench.Bench("INTERSECTS (intersects-circle 1km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"1000")
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-10000":
redbench.Bench("INTERSECTS (intersects-circle 10km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"10000")
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-100000":
redbench.Bench("INTERSECTS (intersects-circle 100km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"100000")
},
)
}
// INTERSECTS-BOUNDS
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-1000":
minlat, minlon, maxlat, maxlon := randRect(1000)
redbench.Bench("INTERSECTS (intersects-bounds 1km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-10000":
minlat, minlon, maxlat, maxlon := randRect(10000)
redbench.Bench("INTERSECTS (intersects-bounds 10km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-100000":
minlat, minlon, maxlat, maxlon := randRect(10000)
redbench.Bench("INTERSECTS (intersects-bounds 100km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"INTERSECTS", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "INTERSECTS", "INTERSECTS-AZ":
var mu sync.Mutex
var loaded bool
redbench.Bench("INTERSECTS (intersects-az limit 5)", addr, opts, func(conn net.Conn) bool {
func() {
mu.Lock()
defer mu.Unlock()
if loaded {
return
}
loaded = true
p := make([]byte, 0xFF)
conn.Write([]byte("GET keys:bench:geo az point\r\n"))
n, err := conn.Read(p)
if err != nil {
panic(err)
}
if string(p[:n]) != "$-1\r\n" {
return
}
args := []string{"SET", "key:bench:geo", "az", "object", az.JSON}
out := redcon.AppendArray(nil, len(args))
for _, arg := range args {
out = redcon.AppendBulkString(out, arg)
}
conn.Write(out)
n, err = conn.Read(p)
if err != nil {
panic(err)
}
if string(p[:n]) != "+OK\r\n" {
panic("expected OK")
}
}()
return prepFn(conn)
},
func(buf []byte) []byte {
args := []string{"INTERSECTS", "key:bench", "LIMIT", "5",
"COUNT", "GET", "key:bench:geo", "az"}
return redbench.AppendCommand(buf, args...)
},
)
}
case "WITHIN",
"WITHIN-RECT", "WITHIN-RECT-1000", "WITHIN-RECT-10000", "WITHIN-RECT-100000",
"WITHIN-CIRCLE", "WITHIN-CIRCLE-1000", "WITHIN-CIRCLE-10000", "WITHIN-CIRCLE-100000":
if redis {
break
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-1000":
redbench.Bench("WITHIN (within-circle 1km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"1000")
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-10000":
redbench.Bench("WITHIN (within-circle 10km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"10000")
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-CIRCLE", "WITHIN-CIRCLE-100000":
redbench.Bench("WITHIN (within-circle 100km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "CIRCLE",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"100000")
},
)
}
// WITHIN-BOUNDS
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-1000":
minlat, minlon, maxlat, maxlon := randRect(1000)
redbench.Bench("WITHIN (within-bounds 1km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-10000":
minlat, minlon, maxlat, maxlon := randRect(10000)
redbench.Bench("WITHIN (within-bounds 10km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "WITHIN", "WITHIN-BOUNDS", "WITHIN-BOUNDS-100000":
minlat, minlon, maxlat, maxlon := randRect(10000)
redbench.Bench("WITHIN (within-bounds 100km)", addr, opts, prepFn,
func(buf []byte) []byte {
return redbench.AppendCommand(buf,
"WITHIN", "key:bench", "COUNT", "BOUNDS",
strconv.FormatFloat(minlat, 'f', 5, 64),
strconv.FormatFloat(minlon, 'f', 5, 64),
strconv.FormatFloat(maxlat, 'f', 5, 64),
strconv.FormatFloat(maxlon, 'f', 5, 64))
},
)
}
case "NEARBY",
"NEARBY-KNN", "NEARBY-KNN-1", "NEARBY-KNN-10", "NEARBY-KNN-100",
"NEARBY-POINT", "NEARBY-POINT-1000", "NEARBY-POINT-10000", "NEARBY-POINT-100000":
if redis {
break
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-1":
redbench.Bench("NEARBY (limit 1)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "LIMIT", "1", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-10":
redbench.Bench("NEARBY (limit 10)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "LIMIT", "10", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-KNN", "NEARBY-KNN-100":
redbench.Bench("NEARBY (limit 100)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "LIMIT", "100", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-1000":
redbench.Bench("NEARBY (point 1km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"1000",
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-10000":
redbench.Bench("NEARBY (point 10km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"10000",
)
},
)
}
switch strings.ToUpper(strings.TrimSpace(test)) {
case "NEARBY", "NEARBY-POINT", "NEARBY-POINT-100000":
redbench.Bench("NEARBY (point 100km)", addr, opts, prepFn,
func(buf []byte) []byte {
lat, lon := randPoint()
return redbench.AppendCommand(buf,
"NEARBY", "key:bench", "COUNT", "POINT",
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
"100000",
)
},
)
}
case "EVAL":
if !redis {
var i int64
getScript := "return tile38.call('GET', KEYS[1], ARGV[1], 'point')"
get4Script :=
"local a = tile38.call('GET', KEYS[1], ARGV[1], 'point');" +
"local b = tile38.call('GET', KEYS[1], ARGV[2], 'point');" +
"local c = tile38.call('GET', KEYS[1], ARGV[3], 'point');" +
"local d = tile38.call('GET', KEYS[1], ARGV[4], 'point');" +
"return d"
setScript := "return tile38.call('SET', KEYS[1], ARGV[1], 'point', ARGV[2], ARGV[3])"
if !opts.Quiet {
fmt.Println("Scripts to run:")
fmt.Println("GET SCRIPT: " + getScript)
fmt.Println("GET FOUR SCRIPT: " + get4Script)
fmt.Println("SET SCRIPT: " + setScript)
}
redbench.Bench("EVAL (set point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
lat, lon := randPoint()
return redbench.AppendCommand(buf, "EVAL", setScript, "1",
"key:bench",
"id:"+strconv.FormatInt(i, 10),
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
redbench.Bench("EVALNA (set point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
lat, lon := randPoint()
return redbench.AppendCommand(buf, "EVALNA", setScript, "1",
"key:bench",
"id:"+strconv.FormatInt(i, 10),
strconv.FormatFloat(lat, 'f', 5, 64),
strconv.FormatFloat(lon, 'f', 5, 64),
)
},
)
redbench.Bench("EVALRO (get point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "EVALRO", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10))
},
)
redbench.Bench("EVALRO (get 4 points)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "EVALRO", get4Script, "1",
"key:bench",
"id:"+strconv.FormatInt(i, 10),
"id:"+strconv.FormatInt(i+1, 10),
"id:"+strconv.FormatInt(i+2, 10),
"id:"+strconv.FormatInt(i+3, 10),
)
},
)
redbench.Bench("EVALNA (get point)", addr, opts, prepFn,
func(buf []byte) []byte {
i := atomic.AddInt64(&i, 1)
return redbench.AppendCommand(buf, "EVALNA", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10))
},
)
}
}
}
}
const earthRadius = 6371e3
func toRadians(deg float64) float64 { return deg * math.Pi / 180 }
func toDegrees(rad float64) float64 { return rad * 180 / math.Pi }
// destinationPoint return the destination from a point based on a distance and bearing.
func destinationPoint(lat, lon, meters, bearingDegrees float64) (destLat, destLon float64) {
// see http://williams.best.vwh.net/avform.htm#LL
δ := meters / earthRadius // angular distance in radians
θ := toRadians(bearingDegrees)
φ1 := toRadians(lat)
λ1 := toRadians(lon)
φ2 := math.Asin(math.Sin(φ1)*math.Cos(δ) + math.Cos(φ1)*math.Sin(δ)*math.Cos(θ))
λ2 := λ1 + math.Atan2(math.Sin(θ)*math.Sin(δ)*math.Cos(φ1), math.Cos(δ)-math.Sin(φ1)*math.Sin(φ2))
λ2 = math.Mod(λ2+3*math.Pi, 2*math.Pi) - math.Pi // normalise to -180..+180°
return toDegrees(φ2), toDegrees(λ2)
}
================================================
FILE: cmd/tile38-cli/main.go
================================================
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"github.com/peterh/liner"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/core"
)
func getEnv(name string, defaultValue string) string {
val, exists := os.LookupEnv(name)
if !exists {
return defaultValue
}
return val
}
func userHomeDir() string {
if runtime.GOOS == "windows" {
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
if home == "" {
home = os.Getenv("USERPROFILE")
}
return home
}
return os.Getenv("HOME")
}
var (
historyFile = filepath.Join(userHomeDir(), ".liner_example_history")
)
type connError struct {
OK bool `json:"ok"`
Err string `json:"err"`
}
var (
hostname = "127.0.0.1"
output = "json"
port = 9851
oneCommand string
raw bool
noprompt bool
tty bool
stdin bool
)
func showHelp() bool {
gitsha := ""
if core.GitSHA == "" || core.GitSHA == "0000000" {
gitsha = ""
} else {
gitsha = " (git:" + core.GitSHA + ")"
}
fmt.Fprintf(os.Stdout, "tile38-cli %s%s\n\n", core.Version, gitsha)
fmt.Fprintf(os.Stdout, "Usage: tile38-cli [OPTIONS] [cmd [arg [arg ...]]]\n")
fmt.Fprintf(os.Stdout, " --raw Use raw formatting for replies\n")
fmt.Fprintf(os.Stdout, " --noprompt Do not display a prompt\n")
fmt.Fprintf(os.Stdout, " --resp Use RESP output formatting (default is JSON output)\n")
fmt.Fprintf(os.Stdout, " --json Use JSON output formatting (default is JSON output)\n")
fmt.Fprintf(os.Stdout, " -h Server hostname (default: %s)\n", hostname)
fmt.Fprintf(os.Stdout, " -p Server port (default: %d)\n", port)
fmt.Fprintf(os.Stdout, " -x Read last argument from STDIN.\n")
fmt.Fprintf(os.Stdout, "\n")
return false
}
func parseArgs() bool {
defer func() {
if v := recover(); v != nil {
if v, ok := v.(string); ok && v == "bad arg" {
showHelp()
}
}
}()
hostname = getEnv("TILE38_HOSTNAME", hostname)
output = getEnv("TILE38_OUTPUT", output)
portStr := getEnv("TILE38_PORT", "")
if portStr != "" {
tempPort, err := strconv.Atoi(portStr)
if err == nil {
port = tempPort
}
}
args := os.Args[1:]
readArg := func(arg string) string {
if len(args) == 0 {
panic("bad arg")
}
var narg = args[0]
args = args[1:]
return narg
}
badArg := func(arg string) bool {
fmt.Fprintf(os.Stderr, "Unrecognized option or bad number of args for: '%s'\n", arg)
return false
}
for len(args) > 0 {
arg := readArg("")
if arg == "--help" || arg == "-?" {
return showHelp()
}
if !strings.HasPrefix(arg, "-") {
args = append([]string{arg}, args...)
break
}
switch arg {
default:
return badArg(arg)
case "--raw":
raw = true
case "--tty":
tty = true
case "--noprompt":
noprompt = true
case "--resp":
output = "resp"
case "--json":
output = "json"
case "-x":
stdin = true
case "-h":
hostname = readArg(arg)
case "-p":
n, err := strconv.ParseUint(readArg(arg), 10, 16)
if err != nil {
return badArg(arg)
}
port = int(n)
}
}
oneCommand = strings.Join(args, " ")
if stdin {
data, err := io.ReadAll(os.Stdin)
if err != nil {
println(err)
}
if !gjson.ValidBytes(data) {
fmt.Fprintf(os.Stderr, "Invalid STDIN: Not JSON\n")
return false
}
arg := strings.Replace(string(data), "\r", "", -1)
arg = strings.Replace(arg, "\n", "", -1)
arg = strings.Replace(arg, "'", "\\'", -1)
oneCommand += " '" + arg + "'"
}
return true
}
func refusedErrorString(addr string) string {
return fmt.Sprintf("Could not connect to Tile38 at %s: Connection refused", addr)
}
var groupsM = make(map[string][]string)
func jsonOK(msg []byte) bool {
return gjson.GetBytes(msg, "ok").Bool()
}
func main() {
if !parseArgs() {
return
}
if len(oneCommand) > 0 && strings.Split(strings.ToLower(oneCommand), " ")[0] == "help" {
showHelp()
return
}
addr := fmt.Sprintf("%s:%d", hostname, port)
var conn *client
connDial := func() {
var err error
conn, err = clientDial("tcp", addr)
if err != nil {
if _, ok := err.(net.Error); ok {
fmt.Fprintln(os.Stderr, refusedErrorString(addr))
} else {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if oneCommand != "" {
os.Exit(1)
}
} else if _, err := conn.Do("output " + output); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
connDial()
monitor := false
livemode := false
aof := false
defer func() {
if livemode {
var err error
if aof {
_, err = io.Copy(os.Stdout, conn.Reader())
fmt.Fprintln(os.Stderr, "")
} else {
var msg []byte
for {
msg, err = conn.readLiveResp()
if err != nil {
break
}
if !raw {
if output == "resp" {
msg = convert2termresp(msg)
} else {
msg = convert2termjson(msg)
}
}
fmt.Fprintln(os.Stderr, string(msg))
}
}
if err != nil && err != io.EOF {
fmt.Fprintln(os.Stderr, err.Error())
}
}
}()
line := liner.NewLiner()
defer line.Close()
var commands []string
for name, command := range core.Commands {
commands = append(commands, name)
groupsM[command.Group] = append(groupsM[command.Group], name)
}
sort.Strings(commands)
var groups []string
for group, arr := range groupsM {
groups = append(groups, "@"+group)
sort.Strings(arr)
groupsM[group] = arr
}
sort.Strings(groups)
line.SetMultiLineMode(false)
line.SetCtrlCAborts(true)
if !(noprompt && tty) {
line.SetCompleter(func(line string) (c []string) {
if strings.HasPrefix(strings.ToLower(line), "help ") {
var nitems []string
nline := strings.TrimSpace(line[5:])
if nline == "" || nline[0] == '@' {
for _, n := range groups {
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) {
nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToLower(n))
}
}
} else {
for _, n := range commands {
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(nline)) {
nitems = append(nitems, line[:len(line)-len(nline)]+strings.ToUpper(n))
}
}
}
for _, n := range nitems {
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) {
c = append(c, n)
}
}
} else {
for _, n := range commands {
if strings.HasPrefix(strings.ToLower(n), strings.ToLower(line)) {
c = append(c, n)
}
}
}
return
})
}
if f, err := os.Open(historyFile); err == nil {
line.ReadHistory(f)
f.Close()
}
defer func() {
if f, err := os.Create(historyFile); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
} else {
line.WriteHistory(f)
f.Close()
}
}()
password := getEnv("TILE38_PASSWORD", "")
if conn != nil && password != "" {
conn.Do(fmt.Sprintf("auth %s", password))
}
for {
var command string
var err error
if oneCommand == "" {
if raw || noprompt {
command, err = line.Prompt("")
} else {
if conn == nil {
command, err = line.Prompt("not connected> ")
} else {
command, err = line.Prompt(addr + "> ")
}
}
} else {
command = oneCommand
}
if err == nil {
nohist := strings.HasPrefix(command, " ")
command = strings.TrimSpace(command)
if command == "" {
if conn != nil {
_, err := conn.Do("pInG")
if err != nil {
if err != io.EOF && !strings.Contains(err.Error(), "broken pipe") {
fmt.Fprintln(os.Stderr, err.Error())
} else {
fmt.Fprintln(os.Stderr, refusedErrorString(addr))
}
conn.wr.Close()
conn = nil
continue
}
}
} else {
if !nohist {
line.AppendHistory(command)
}
if strings.ToLower(command) == "exit" {
return
}
if strings.ToLower(command) == "quit" {
return
}
if strings.ToLower(command) == "clear" {
clearScreen()
continue
}
if strings.ToLower(command) == "help" || strings.HasPrefix(strings.ToLower(command), "help") {
err = help(strings.TrimSpace(command[4:]))
if err != nil {
return
}
continue
}
aof = strings.HasPrefix(strings.ToLower(command), "aof ")
tryAgain:
if conn == nil {
connDial()
if conn == nil {
continue
}
}
msg, err := conn.Do(command)
if err != nil {
if err != io.EOF {
fmt.Fprintln(os.Stderr, err.Error())
}
conn.wr.Close()
conn = nil
goto tryAgain
}
switch strings.ToLower(command) {
case "output resp":
if string(msg) == "+OK\r\n" {
output = "resp"
}
case "output json":
if jsonOK(msg) {
output = "json"
}
case "monitor":
monitor = true
livemode = true
output = "resp"
}
if output == "resp" &&
(strings.HasPrefix(string(msg), "*3\r\n$10\r\npsubscribe\r\n") ||
strings.HasPrefix(string(msg), "*3\r\n$9\r\nsubscribe\r\n")) {
livemode = true
}
if !raw {
if output == "resp" {
msg = convert2termresp(msg)
} else {
msg = convert2termjson(msg)
}
}
if !livemode && output == "json" {
if gjson.GetBytes(msg, "command").String() == "psubscribe" ||
gjson.GetBytes(msg, "command").String() == "subscribe" ||
string(msg) == liveJSON {
livemode = true
}
}
mustOutput := true
if !monitor && oneCommand == "" && output == "json" && !jsonOK(msg) {
var cerr connError
if err := json.Unmarshal(msg, &cerr); err == nil {
fmt.Fprintln(os.Stderr, "(error) "+cerr.Err)
mustOutput = false
}
} else if livemode {
fmt.Fprintln(os.Stderr, string(msg))
break // break out of prompt and just feed data to screen
}
if mustOutput {
fmt.Fprintln(os.Stdout, string(msg))
}
}
} else if err == liner.ErrPromptAborted {
return
} else if err == io.EOF {
os.Exit(0)
} else {
fmt.Fprintf(os.Stderr, "Error reading line: %s", err.Error())
}
if oneCommand != "" {
return
}
}
}
func convert2termresp(msg []byte) []byte {
rd := resp.NewReader(bytes.NewBuffer(msg))
out := ""
for {
v, _, err := rd.ReadValue()
if err != nil {
break
}
out += convert2termrespval(v, 0)
}
return []byte(strings.TrimSpace(out))
}
func convert2termjson(msg []byte) []byte {
if msg[0] == '{' {
return msg
}
return bytes.TrimSpace(msg[bytes.IndexByte(msg, '\n')+1:])
}
func convert2termrespval(v resp.Value, spaces int) string {
switch v.Type() {
default:
return v.String()
case resp.BulkString:
if v.IsNull() {
return "(nil)"
}
return "\"" + v.String() + "\""
case resp.Integer:
return "(integer) " + v.String()
case resp.Error:
return "(error) " + v.String()
case resp.Array:
arr := v.Array()
if len(arr) == 0 {
return "(empty list or set)"
}
out := ""
nspaces := spaces + numlen(len(arr))
for i, v := range arr {
if i > 0 {
out += strings.Repeat(" ", spaces)
}
iout := strings.TrimSpace(convert2termrespval(v, nspaces+2))
out += fmt.Sprintf("%d) %s\n", i+1, iout)
}
return out
}
}
func numlen(n int) int {
l := 1
if n < 0 {
l++
n = n * -1
}
for i := 0; i < 1000; i++ {
if n < 10 {
break
}
l++
n = n / 10
}
return l
}
func help(arg string) error {
var groupsA []string
for group := range groupsM {
groupsA = append(groupsA, "@"+group)
}
groups := "Groups: " + strings.Join(groupsA, ", ") + "\n"
if arg == "" {
fmt.Fprintf(os.Stderr, "tile38-cli %s (git:%s)\n", core.Version, core.GitSHA)
fmt.Fprintf(os.Stderr, `Type: "help @" to get a list of commands in `+"\n")
fmt.Fprintf(os.Stderr, ` "help " for help on `+"\n")
if !(noprompt && tty) {
fmt.Fprintf(os.Stderr, ` "help " to get a list of possible help topics`+"\n")
}
fmt.Fprintf(os.Stderr, ` "quit" to exit`+"\n")
if noprompt && tty {
fmt.Fprint(os.Stderr, groups)
}
return nil
}
showGroups := false
found := false
if strings.HasPrefix(arg, "@") {
for _, command := range groupsM[arg[1:]] {
fmt.Fprintf(os.Stderr, "%s\n", core.Commands[command].TermOutput(" "))
found = true
}
if !found {
showGroups = true
}
} else {
if command, ok := core.Commands[strings.ToUpper(arg)]; ok {
fmt.Fprintf(os.Stderr, "%s\n", command.TermOutput(" "))
found = true
}
}
if showGroups {
if noprompt && tty {
fmt.Fprint(os.Stderr, groups)
}
} else if !found {
if noprompt && tty {
help("")
}
}
return nil
}
const liveJSON = `{"ok":true,"live":true}`
type client struct {
wr net.Conn
rd *bufio.Reader
}
func clientDial(network, addr string) (*client, error) {
conn, err := net.Dial(network, addr)
if err != nil {
return nil, err
}
return &client{wr: conn, rd: bufio.NewReader(conn)}, nil
}
func (c *client) Do(command string) ([]byte, error) {
_, err := c.wr.Write(plainToCompat(command))
if err != nil {
return nil, err
}
return c.readResp()
}
func (c *client) readResp() ([]byte, error) {
ch, err := c.rd.Peek(1)
if err != nil {
return nil, err
}
switch ch[0] {
case ':', '+', '-', '{':
return c.readLine()
case '$':
return c.readBulk()
case '*':
return c.readArray()
default:
return nil, fmt.Errorf("invalid response character '%c", ch[0])
}
}
func (c *client) readArray() ([]byte, error) {
out, err := c.readLine()
if err != nil {
return nil, err
}
n, err := strconv.ParseUint(string(bytes.TrimSpace(out[1:])), 10, 64)
if err != nil {
return nil, err
}
for i := 0; i < int(n); i++ {
resp, err := c.readResp()
if err != nil {
return nil, err
}
out = append(out, resp...)
}
return out, nil
}
func (c *client) readBulk() ([]byte, error) {
line, err := c.readLine()
if err != nil {
return nil, err
}
x, err := strconv.ParseInt(string(bytes.TrimSpace(line[1:])), 10, 64)
if err != nil {
return nil, err
}
if x < 0 {
return line, nil
}
out := make([]byte, len(line)+int(x)+2)
if _, err := io.ReadFull(c.rd, out[len(line):]); err != nil {
return nil, err
}
if !bytes.HasSuffix(out, []byte{'\r', '\n'}) {
return nil, errors.New("invalid response")
}
copy(out, line)
return out, nil
}
func (c *client) readLine() ([]byte, error) {
line, err := c.rd.ReadBytes('\r')
if err != nil {
return nil, err
}
ch, err := c.rd.ReadByte()
if err != nil {
return nil, err
}
if ch != '\n' {
return nil, errors.New("invalid response")
}
return append(line, '\n'), nil
}
func (c *client) Reader() io.Reader {
return c.rd
}
func (c *client) readLiveResp() (message []byte, err error) {
return c.readResp()
}
// plainToCompat converts a plain message like "SET fleet truck1 ..." into a
// Tile38 compatible blob.
func plainToCompat(message string) []byte {
var args []string
// search for the beginning of the first argument
for i := 0; i < len(message); i++ {
if message[i] != ' ' {
// first argument found
if message[i] == '"' || message[i] == '\'' {
// using a string caps
s := i
cap := message[i]
for ; i < len(message); i++ {
if message[i] == cap {
if message[i-1] == '\\' {
continue
}
if i == len(message)-1 || message[i+1] == ' ' {
args = append(args, message[s:i+1])
i++
break
}
}
}
} else {
// using plain string, terminated by a space
s := i
var quotes bool
for ; i < len(message); i++ {
if message[i] == '"' || message[i] == '\'' {
quotes = true
}
if i == len(message)-1 || message[i+1] == ' ' {
arg := message[s : i+1]
if quotes {
arg = strconv.Quote(arg)
}
args = append(args, arg)
i++
break
}
}
}
}
}
return []byte(strings.Join(args, " ") + "\r\n")
}
func clearScreen() {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", "cls")
} else {
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
cmd.Run()
}
================================================
FILE: cmd/tile38-luamemtest/main.go
================================================
package main
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"math"
"runtime"
"runtime/debug"
"strings"
"github.com/tidwall/resp"
lua "github.com/yuin/gopher-lua"
)
func Sha1Sum(s string) string {
h := sha1.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
// Convert lua LValue to RESP value
func ConvertToResp(val lua.LValue) resp.Value {
switch val.Type() {
case lua.LTNil:
return resp.NullValue()
case lua.LTBool:
if val == lua.LTrue {
return resp.IntegerValue(1)
} else {
return resp.NullValue()
}
case lua.LTNumber:
if float := float64(val.(lua.LNumber)); math.IsNaN(float) || math.IsInf(float, 0) {
return resp.FloatValue(float)
} else {
return resp.IntegerValue(int(math.Floor(float)))
}
case lua.LTString:
return resp.StringValue(val.String())
case lua.LTTable:
var values []resp.Value
var specialValues []resp.Value
var cb func(lk lua.LValue, lv lua.LValue)
tbl := val.(*lua.LTable)
if tbl.Len() != 0 { // list
cb = func(lk lua.LValue, lv lua.LValue) {
values = append(values, ConvertToResp(lv))
}
} else { // map
cb = func(lk lua.LValue, lv lua.LValue) {
if lk.Type() == lua.LTString {
lks := lk.String()
switch lks {
case "ok":
specialValues = append(specialValues, resp.SimpleStringValue(lv.String()))
case "err":
specialValues = append(specialValues, resp.ErrorValue(errors.New(lv.String())))
}
}
values = append(values, resp.ArrayValue(
[]resp.Value{ConvertToResp(lk), ConvertToResp(lv)}))
}
}
tbl.ForEach(cb)
if len(values) == 1 && len(specialValues) == 1 {
return specialValues[0]
}
return resp.ArrayValue(values)
}
return resp.ErrorValue(errors.New("Unsupported lua type: " + val.Type().String()))
}
// Convert RESP value to lua LValue
func ConvertToLua(L *lua.LState, val resp.Value) lua.LValue {
if val.IsNull() {
return lua.LFalse
}
switch val.Type() {
case resp.Integer:
return lua.LNumber(val.Integer())
case resp.BulkString:
return lua.LString(val.String())
case resp.Error:
tbl := L.CreateTable(0, 1)
tbl.RawSetString("err", lua.LString(val.String()))
return tbl
case resp.SimpleString:
tbl := L.CreateTable(0, 1)
tbl.RawSetString("ok", lua.LString(val.String()))
return tbl
case resp.Array:
tbl := L.CreateTable(len(val.Array()), 0)
for _, item := range val.Array() {
tbl.Append(ConvertToLua(L, item))
}
return tbl
}
return lua.LString("ERR: unknown RESP type: " + val.Type().String())
}
func luaTile38Call(evalcmd string, cmd string, args ...string) (resp.Value, error) {
var values []resp.Value
values = append(values, resp.StringValue("RUNNING:"))
values = append(values, resp.StringValue(evalcmd))
values = append(values, resp.StringValue(cmd))
for _, arg := range args {
values = append(values, resp.StringValue(arg))
}
return resp.ArrayValue(values), nil
}
func NewLuaState() *lua.LState {
L := lua.NewState()
get_args := func(ls *lua.LState) (evalCmd string, args []string) {
evalCmd = ls.GetGlobal("EVAL_CMD").String()
//log.Debugf("EVAL_CMD %s\n", evalCmd)
// Trying to work with unknown number of args.
// When we see empty arg we call it enough.
for i := 1; ; i++ {
if arg := ls.ToString(i); arg == "" {
break
} else {
args = append(args, arg)
}
}
return
}
call := func(ls *lua.LState) int {
evalCmd, args := get_args(ls)
if res, err := luaTile38Call(evalCmd, args[0], args[1:]...); err != nil {
//log.Debugf("RES type: %s value: %s ERR %s\n", res.Type(), res.String(), err);
ls.RaiseError("ERR %s", err.Error())
return 0
} else {
//log.Debugf("RES type: %s value: %s\n", res.Type(), res.String());
ls.Push(ConvertToLua(ls, res))
return 1
}
}
pcall := func(ls *lua.LState) int {
evalCmd, args := get_args(ls)
if res, err := luaTile38Call(evalCmd, args[0], args[1:]...); err != nil {
//log.Debugf("RES type: %s value: %s ERR %s\n", res.Type(), res.String(), err);
ls.Push(ConvertToLua(ls, resp.ErrorValue(err)))
return 1
} else {
//log.Debugf("RES type: %s value: %s\n", res.Type(), res.String());
ls.Push(ConvertToLua(ls, res))
return 1
}
}
error_reply := func(ls *lua.LState) int {
tbl := L.CreateTable(0, 1)
tbl.RawSetString("err", lua.LString(ls.ToString(1)))
ls.Push(tbl)
return 1
}
status_reply := func(ls *lua.LState) int {
tbl := L.CreateTable(0, 1)
tbl.RawSetString("ok", lua.LString(ls.ToString(1)))
ls.Push(tbl)
return 1
}
sha1hex := func(ls *lua.LState) int {
sha_sum := Sha1Sum(ls.ToString(1))
ls.Push(lua.LString(sha_sum))
return 1
}
var exports = map[string]lua.LGFunction{
"call": call,
"pcall": pcall,
"error_reply": error_reply,
"status_reply": status_reply,
"sha1hex": sha1hex,
}
L.SetGlobal("tile38", L.SetFuncs(L.NewTable(), exports))
return L
}
func makeSafeErr(err error) error {
return errors.New(strings.Replace(err.Error(), "\n", `\n`, -1))
}
func runLuaFunc(luaState *lua.LState, script string, name string) resp.Value {
luaState.SetGlobal("EVAL_CMD", lua.LString("FAKE_EVAL"))
fn, err := luaState.Load(strings.NewReader(script), name)
if err != nil {
return resp.ErrorValue(makeSafeErr(err))
}
luaState.Push(fn)
if err := luaState.PCall(0, 1, nil); err != nil {
return resp.ErrorValue(makeSafeErr(err))
}
ret := luaState.Get(-1) // returned value
luaState.Pop(1)
luaState.SetGlobal("EVAL_CMD", lua.LNil)
return ConvertToResp(ret)
}
func runMany(luaState *lua.LState, start int, num int) int {
fmt.Printf("\nRunning %d lua calls... ", num)
for i := 0; i < num; i++ {
script := fmt.Sprintf("return tile38.call('foo', 'bar', %d)", i)
name := fmt.Sprintf("f_%020d", i)
ret := runLuaFunc(luaState, script, name)
if ret.Type() == resp.Error {
panic(ret.String())
}
}
fmt.Printf("done.\n")
return start + num
}
func printMemStats() {
var mem runtime.MemStats
runtime.GC()
debug.FreeOSMemory()
runtime.GC()
debug.FreeOSMemory()
runtime.GC()
debug.FreeOSMemory()
runtime.GC()
debug.FreeOSMemory()
runtime.ReadMemStats(&mem)
fmt.Printf("MemStats: Alloc %d, HeapAlloc %d, HeapSys %d, GCSys %d, HeapObjects %d.\n",
mem.Alloc, mem.HeapAlloc, mem.HeapSys, mem.GCSys, mem.HeapObjects)
}
func testLua() {
var luaState *lua.LState
start := 12345
luaState = NewLuaState()
printMemStats()
fmt.Printf("\nRunning single call as a test\n")
ret := runLuaFunc(luaState, "return tile38.call('fake_cmd', 'a', 'b')", "test_call")
fmt.Printf("Result: %s\n", ret.String())
printMemStats()
start = runMany(luaState, start, 100)
printMemStats()
start = runMany(luaState, start, 100)
printMemStats()
start = runMany(luaState, start, 100)
printMemStats()
start = runMany(luaState, start, 100)
printMemStats()
start = runMany(luaState, start, 1000)
printMemStats()
start = runMany(luaState, start, 10000)
printMemStats()
start = runMany(luaState, start, 1000)
printMemStats()
start = runMany(luaState, start, 100)
printMemStats()
_ = runMany(luaState, start, 1000)
printMemStats()
luaState.Close()
}
func main() {
fmt.Printf("Starting memtest.\n")
testLua()
}
================================================
FILE: cmd/tile38-server/main.go
================================================
package main
import (
"flag"
"fmt"
"io"
"net"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"sync"
"syscall"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/hservice"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// TODO: Set to false in 2.*
var httpTransport = true
////////////////////////////////////////////////////////////////////////////////
//
// Fire up a webhook test server by using the --webhook-http-consumer-port
// for example
// $ ./tile38-server --webhook-http-consumer-port 9999
//
// The create hooks like such...
// SETHOOK myhook http://localhost:9999/myhook NEARBY mykey FENCE POINT 33.5 -115.5 1000
//
////////////////////////////////////////////////////////////////////////////////
//
// Memory profiling - start the server with the -pprofport flag
//
// $ ./tile38-server -pprofport 6060
//
// Then, at any point, from a different terminal execute:
// $ go tool pprof -svg http://localhost:6060/debug/pprof/heap > out.svg
//
// Load the SVG into a web browser to visualize the memory usage
//
////////////////////////////////////////////////////////////////////////////////
type hserver struct{}
func (s *hserver) Send(ctx context.Context, in *hservice.MessageRequest) (*hservice.MessageReply, error) {
return &hservice.MessageReply{Ok: true}, nil
}
func main() {
gitsha := " (" + core.GitSHA + ")"
if gitsha == " (0000000)" {
gitsha = ""
}
versionLine := `tile38-server version: ` + core.Version + gitsha
output := os.Stderr
flag.Usage = func() {
fmt.Fprintf(output,
"%s", versionLine+`
Usage: tile38-server [-p port]
Basic Options:
-h hostname : listening host
-p port : listening port (default: 9851)
-d path : data directory (default: data)
-s socket : listen on unix socket file
-l encoding : set log encoding to json or text (default: text)
-o output : auto set client output to json or resp (default: resp)
-q : no logging. totally silent output
-v : enable verbose logging
-vv : enable very verbose logging
Advanced Options:
--pidfile path : file that contains the pid
--appendonly yes/no : AOF persistence (default: yes)
--appendfilename path : AOF path (default: data/appendonly.aof)
--queuefilename path : Event queue path (default:data/queue.db)
--http-transport yes/no : HTTP transport (default: yes)
--protected-mode yes/no : protected mode (default: yes)
--nohup : do not exit on SIGHUP
--spinlock : use a spinlock. For very write-heavy workloads
Developer Options:
--dev : enable developer mode
--webhook-http-consumer-port port : Start a test HTTP webhook server
--webhook-grpc-consumer-port port : Start a test GRPC webhook server
`,
)
}
if len(os.Args) == 3 && os.Args[1] == "--webhook-http-consumer-port" {
log.SetOutput(os.Stderr)
port, err := strconv.ParseUint(os.Args[2], 10, 16)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
log.Fatal(err)
}
log.HTTPf("http: %s : %s", r.URL.Path, string(data))
})
log.Infof("webhook server http://localhost:%d/", port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
log.Fatal(err)
}
return
}
if len(os.Args) == 3 && os.Args[1] == "--webhook-grpc-consumer-port" {
log.SetOutput(os.Stderr)
port, err := strconv.ParseUint(os.Args[2], 10, 16)
if err != nil {
log.Fatal(err)
}
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
hservice.RegisterHookServiceServer(s, &hserver{})
log.Infof("webhook server grpc://localhost:%d/", port)
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
return
}
var (
nohup bool
showEvioDisabled bool
showThreadsDisabled bool
spinlock bool
)
var (
// use to be in core/options.go
// DevMode puts application in to dev mode
devMode = false
// ShowDebugMessages allows for log.Debug to print to console.
showDebugMessages = false
// ProtectedMode forces Tile38 to default in protected mode.
protectedMode = "no"
// AppendOnly allows for disabling the appendonly file.
appendOnly = true
// AppendFileName allows for custom appendonly file path
appendFileName = ""
// QueueFileName allows for custom queue.db file path
queueFileName = ""
// ClientOutput for auto assigning the output for client.
clientOutput = ""
)
// parse non standard args.
nargs := []string{os.Args[0]}
for i := 1; i < len(os.Args); i++ {
switch os.Args[i] {
case "--help":
output = os.Stdout
flag.Usage()
return
case "--version":
fmt.Fprintf(os.Stdout, "%s\n", versionLine)
return
case "--protected-mode", "-protected-mode":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "no":
protectedMode = "no"
continue
case "yes":
protectedMode = "yes"
continue
}
}
fmt.Fprintf(os.Stderr, "protected-mode must be 'yes' or 'no'\n")
os.Exit(1)
case "--dev", "-dev":
devMode = true
continue
case "--nohup", "-nohup":
nohup = true
continue
case "--spinlock", "-spinlock":
spinlock = true
continue
case "--appendonly", "-appendonly":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "no":
appendOnly = false
continue
case "yes":
appendOnly = true
continue
}
}
fmt.Fprintf(os.Stderr, "appendonly must be 'yes' or 'no'\n")
os.Exit(1)
case "--appendfilename", "-appendfilename":
i++
if i == len(os.Args) || os.Args[i] == "" {
fmt.Fprintf(os.Stderr, "appendfilename must have a value\n")
os.Exit(1)
}
appendFileName = os.Args[i]
case "--queuefilename", "-queuefilename":
i++
if i == len(os.Args) || os.Args[i] == "" {
fmt.Fprintf(os.Stderr, "queuefilename must have a value\n")
os.Exit(1)
}
queueFileName = os.Args[i]
case "-o":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "resp", "json":
clientOutput = strings.ToLower(os.Args[i])
continue
}
}
fmt.Fprintf(os.Stderr, "output must be 'resp' or 'json'\n")
os.Exit(1)
case "--http-transport", "-http-transport":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "1", "true", "yes":
httpTransport = true
continue
case "0", "false", "no":
httpTransport = false
continue
}
}
fmt.Fprintf(os.Stderr, "http-transport must be 'yes' or 'no'\n")
os.Exit(1)
case "--threads", "-threads":
i++
if i < len(os.Args) {
_, err := strconv.ParseUint(os.Args[i], 10, 16)
if err != nil {
fmt.Fprintf(os.Stderr, "threads must be a valid number\n")
os.Exit(1)
}
showThreadsDisabled = true
continue
}
fmt.Fprintf(os.Stderr, "threads must be a valid number \n")
os.Exit(1)
case "--evio", "-evio":
i++
if i < len(os.Args) {
switch strings.ToLower(os.Args[i]) {
case "no", "yes":
showEvioDisabled = true
continue
}
}
fmt.Fprintf(os.Stderr, "evio must be 'yes' or 'no'\n")
os.Exit(1)
default:
nargs = append(nargs, os.Args[i])
}
}
os.Args = nargs
metricsAddr := flag.String("metrics-addr", "", "The listening addr for Prometheus metrics.")
var (
dir string
port int
host string
unixSocket string
verbose bool
veryVerbose bool
logEncoding string
quiet bool
pidfile string
cpuprofile string
memprofile string
pprofport int
)
flag.IntVar(&port, "p", 9851, "The listening port")
flag.StringVar(&pidfile, "pidfile", "", "A file that contains the pid")
flag.StringVar(&host, "h", "", "The listening host")
flag.StringVar(&unixSocket, "s", "", "Listen on a unix socket")
flag.StringVar(&dir, "d", "data", "The data directory")
flag.StringVar(&logEncoding, "l", "text", "The log encoding json or text (default: text)")
flag.BoolVar(&verbose, "v", false, "Enable verbose logging")
flag.BoolVar(&quiet, "q", false, "Quiet logging. Totally silent")
flag.BoolVar(&veryVerbose, "vv", false, "Enable very verbose logging")
flag.IntVar(&pprofport, "pprofport", 0, "pprofport http at port")
flag.StringVar(&cpuprofile, "cpuprofile", "", "write cpu profile to `file`")
flag.StringVar(&memprofile, "memprofile", "", "write memory profile to `file`")
flag.Parse()
if logEncoding == "json" {
log.SetLogJSON(true)
data, _ := os.ReadFile(filepath.Join(dir, "config"))
if gjson.GetBytes(data, "logconfig.encoding").String() == "json" {
c := gjson.GetBytes(data, "logconfig").String()
log.Build(c)
} else {
log.Build("")
}
}
var logw io.Writer = os.Stderr
if quiet {
logw = io.Discard
}
log.SetOutput(logw)
if quiet {
log.SetLevel(0)
} else if veryVerbose {
log.SetLevel(3)
} else if verbose {
log.SetLevel(2)
} else {
log.SetLevel(1)
}
showDebugMessages = veryVerbose
hostd := ""
if host != "" {
hostd = "Addr: " + host + ", "
}
// pprof
if cpuprofile != "" {
log.Debugf("cpuprofile active")
f, err := os.Create(cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
}
if memprofile != "" {
log.Debug("memprofile active")
}
var pprofcleanedup bool
var pprofcleanupMu sync.Mutex
pprofcleanup := func() {
pprofcleanupMu.Lock()
defer pprofcleanupMu.Unlock()
if pprofcleanedup {
return
}
// cleanup code
if cpuprofile != "" {
pprof.StopCPUProfile()
}
if memprofile != "" {
f, err := os.Create(memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
f.Close()
}
pprofcleanedup = true
}
defer pprofcleanup()
if pprofport != 0 {
log.Debugf("pprof http at port %d", pprofport)
go func() {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", pprofport), nil))
}()
}
if unixSocket != "" {
port = 0
}
// pid file
var pidferr error
var pidcleanedup bool
var pidcleanupMu sync.Mutex
pidcleanup := func() {
if pidfile != "" {
pidcleanupMu.Lock()
defer pidcleanupMu.Unlock()
if pidcleanedup {
return
}
// cleanup code
if pidfile != "" {
os.Remove(pidfile)
}
pidcleanedup = true
}
}
defer pidcleanup()
if pidfile != "" {
os.WriteFile(pidfile, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0666)
}
c := make(chan os.Signal, 1)
shutdown := make(chan bool, 1)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
for s := range c {
if s == syscall.SIGHUP && nohup {
continue
}
log.Warnf("signal: %v", s)
pidcleanup()
pprofcleanup()
switch {
default:
os.Exit(-1)
case s == syscall.SIGHUP:
os.Exit(1)
case s == syscall.SIGINT:
os.Exit(2)
case s == syscall.SIGQUIT:
os.Exit(3)
case s == syscall.SIGTERM:
shutdown <- true
}
}
}()
var saddr string
if unixSocket != "" {
saddr = fmt.Sprintf("Socket: %s", unixSocket)
} else {
saddr = fmt.Sprintf("Port: %d", port)
}
if log.LogJSON() {
log.Printf(`Tile38 %s%s %d bit (%s/%s) %s%s, PID: %d. Visit tile38.com/sponsor to support the project`,
core.Version, gitsha, strconv.IntSize, runtime.GOARCH, runtime.GOOS, hostd, saddr, os.Getpid())
} else {
fmt.Fprintf(logw, `
_____ _ _ ___ ___
|_ _|_| |___|_ | . | Tile38 %s%s %d bit (%s/%s)
| | | | | -_|_ | . | %s%s, PID: %d
|_| |_|_|___|___|___| tile38.com
`, core.Version, gitsha, strconv.IntSize, runtime.GOARCH, runtime.GOOS, hostd,
saddr, os.Getpid())
}
if pidferr != nil {
log.Warnf("pidfile: %v", pidferr)
}
if showEvioDisabled {
log.Warnf("evio is not currently supported")
}
if showThreadsDisabled {
log.Warnf("thread flag is deprecated use GOMAXPROCS to set number of threads instead")
}
opts := server.Options{
Host: host,
Port: port,
Dir: dir,
UseHTTP: httpTransport,
MetricsAddr: *metricsAddr,
UnixSocketPath: unixSocket,
DevMode: devMode,
ShowDebugMessages: showDebugMessages,
ProtectedMode: protectedMode,
AppendOnly: appendOnly,
AppendFileName: appendFileName,
QueueFileName: queueFileName,
Shutdown: shutdown,
Spinlock: spinlock,
ClientOutput: clientOutput,
}
if err := server.Serve(opts); err != nil {
log.Fatal(err)
}
}
================================================
FILE: core/commands.go
================================================
//go:build ignore
package core
import (
"encoding/json"
"strings"
)
const (
clear = "\x1b[0m"
bright = "\x1b[1m"
gray = "\x1b[90m"
yellow = "\x1b[33m"
)
// Command represents a Tile38 command.
type Command struct {
Name string `json:"-"`
Summary string `json:"summary"`
Complexity string `json:"complexity"`
Arguments []Argument `json:"arguments"`
Since string `json:"since"`
Group string `json:"group"`
DevOnly bool `json:"dev"`
}
// String returns a string representation of the command.
func (c Command) String() string {
var s = c.Name
for _, arg := range c.Arguments {
s += " " + arg.String()
}
return s
}
// TermOutput returns a string representation of the command suitable for displaying in a terminal.
func (c Command) TermOutput(indent string) string {
line := c.String()
var line1 string
if strings.HasPrefix(line, c.Name) {
line1 = bright + c.Name + clear + gray + line[len(c.Name):] + clear
} else {
line1 = bright + strings.Replace(c.String(), " ", " "+clear+gray, 1) + clear
}
line2 := yellow + "summary: " + clear + c.Summary
//line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" //+ indent + line3 + "\n"
}
// EnumArg represents a enum arguments.
type EnumArg struct {
Name string `json:"name"`
Arguments []Argument `json:"arguments"`
}
// String returns a string representation of an EnumArg.
func (a EnumArg) String() string {
var s = a.Name
for _, arg := range a.Arguments {
s += " " + arg.String()
}
return s
}
// Argument represents a command argument.
type Argument struct {
Command string `json:"command"`
NameAny interface{} `json:"name"`
TypeAny interface{} `json:"type"`
Optional bool `json:"optional"`
Multiple bool `json:"multiple"`
Variadic bool `json:"variadic"`
Enum []string `json:"enum"`
EnumArgs []EnumArg `json:"enumargs"`
}
// String returns a string representation of an Argument.
func (a Argument) String() string {
var s string
if a.Command != "" {
s += " " + a.Command
}
if len(a.EnumArgs) > 0 {
eargs := ""
for _, arg := range a.EnumArgs {
v := arg.String()
if strings.Contains(v, " ") {
v = "(" + v + ")"
}
eargs += v + "|"
}
if len(eargs) > 0 {
eargs = eargs[:len(eargs)-1]
}
s += " " + eargs
} else if len(a.Enum) > 0 {
s += " " + strings.Join(a.Enum, "|")
} else {
names, _ := a.NameTypes()
subs := ""
for _, name := range names {
subs += " " + name
}
subs = strings.TrimSpace(subs)
s += " " + subs
if a.Variadic {
if len(names) == 0 {
s += " [" + subs + " ...]"
} else {
s += " [" + names[len(names)-1] + " ...]"
}
}
if a.Multiple {
s += " ..."
}
}
s = strings.TrimSpace(s)
if a.Optional {
s = "[" + s + "]"
}
return s
}
func parseAnyStringArray(any interface{}) []string {
if str, ok := any.(string); ok {
return []string{str}
} else if any, ok := any.([]interface{}); ok {
arr := []string{}
for _, any := range any {
if str, ok := any.(string); ok {
arr = append(arr, str)
}
}
return arr
}
return []string{}
}
// NameTypes returns the types and names of an argument as separate arrays.
func (a Argument) NameTypes() (names, types []string) {
names = parseAnyStringArray(a.NameAny)
types = parseAnyStringArray(a.TypeAny)
if len(types) > len(names) {
types = types[:len(names)]
} else {
for len(types) < len(names) {
types = append(types, "")
}
}
return
}
// Commands is a map of all of the commands.
var Commands = func() map[string]Command {
var commands map[string]Command
if err := json.Unmarshal([]byte(commandsJSON), &commands); err != nil {
panic(err.Error())
}
for name, command := range commands {
command.Name = strings.ToUpper(name)
commands[name] = command
}
return commands
}()
var commandsJSON = `{{.CommandsJSON}}`
================================================
FILE: core/commands.json
================================================
{
"SET": {
"summary": "Sets the value of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "FIELD",
"name": ["name", "value"],
"type": ["string", "double"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "NX"
},
{
"name": "XX"
}
]
},
{
"name": "value",
"enumargs": [
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "z",
"type": "double",
"optional": true
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "STRING",
"arguments": [
{
"name": "value",
"type": "string"
}
]
}
]
}
],
"since": "1.0.0",
"group": "keys"
},
"EXPIRE": {
"summary": "Set a timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "seconds",
"type": "double"
}
],
"since": "1.0.0",
"group": "keys"
},
"TTL": {
"summary": "Get a timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"EXISTS": {
"summary": "Checks to see if a id exists",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"FEXISTS": {
"summary": "Checks to see if a field exists on a id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"PERSIST": {
"summary": "Remove the existing timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"FSET": {
"summary": "Set the value for one or more fields of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "XX",
"name": [],
"type": [],
"optional": true
},
{
"name": ["field", "value"],
"type": ["string", "double"]
},
{
"name": ["field", "value"],
"type": ["string", "double"],
"multiple": true,
"optional": true
}
],
"since": "1.0.0",
"group": "keys"
},
"FGET": {
"summary": "Gets the value for the field of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"BOUNDS": {
"summary": "Get the combined bounds of all the objects in a key",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
}
],
"since": "1.3.0",
"group": "keys"
},
"GET": {
"summary": "Get the object of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "WITHFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "OBJECT"
},
{
"name": "POINT"
},
{
"name": "BOUNDS"
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
}
],
"since": "1.0.0",
"group": "keys"
},
"DEL": {
"summary": "Delete an id from a key",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "ERRON404",
"name": [],
"type": [],
"optional": true
}
],
"since": "1.0.0",
"group": "keys"
},
"DROP": {
"summary": "Remove a key from the database",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"RENAME": {
"summary": "Rename a key to be stored under a different name.",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "newkey",
"type": "string"
}
],
"since": "1.14.5",
"group": "keys"
},
"RENAMENX": {
"summary": "Rename a key to be stored under a different name, if a new key does not exist.",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "newkey",
"type": "string"
}
],
"since": "1.14.5",
"group": "keys"
},
"KEYS": {
"summary": "Finds all keys matching the given pattern",
"complexity": "O(N) where N is the number of keys in the database",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"since": "1.0.0",
"group": "keys"
},
"STATS": {
"summary": "Show stats for one or more keys",
"complexity": "O(N) where N is the number of keys being requested",
"arguments": [
{
"name": "key",
"type": "string",
"variadic": true
}
],
"since": "1.0.0",
"group": "keys"
},
"SEARCH": {
"summary": "Search for string values in a key",
"complexity": "O(N) where N is the number of values in the key",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"name": "order",
"optional": true,
"enumargs": [
{
"name": "ASC"
},
{
"name": "DESC"
}
]
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
}
]
}
],
"since": "1.4.2",
"group": "search"
},
"SCAN": {
"summary": "Incrementally iterate though a key",
"complexity": "O(N) where N is the number of ids in the key",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"name": "order",
"optional": true,
"enumargs": [
{
"name": "ASC"
},
{
"name": "DESC"
}
]
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"NEARBY": {
"summary": "Searches for ids that are nearby a point",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "DISTANCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "ROAM",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "pattern",
"type": "pattern"
},
{
"name": "meters",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"WITHIN": {
"summary": "Searches for ids that completely within the area",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "SECTOR",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "radius",
"type": "double"
},
{
"name": "startBearing",
"type": "double"
},
{
"name": "endBearing",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"INTERSECTS": {
"summary": "Searches for ids that intersect an area",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "CLIP",
"name": [],
"type": [],
"optional": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "SECTOR",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "radius",
"type": "double"
},
{
"name": "startBearing",
"type": "double"
},
{
"name": "endBearing",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"CONFIG GET": {
"summary": "Get the value of a configuration parameter",
"arguments": [
{
"name": "parameter",
"type": "string"
}
],
"group": "server"
},
"CONFIG SET": {
"summary": "Set a configuration parameter to the given value",
"arguments": [
{
"name": "parameter",
"type": "string"
},
{
"name": "value",
"type": "string",
"optional": true
}
],
"group": "server"
},
"CONFIG REWRITE": {
"summary": "Rewrite the configuration file with the in memory configuration",
"arguments": [],
"group": "server"
},
"SERVER": {
"summary": "Show server stats and details",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"GC": {
"summary": "Forces a garbage collection",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"READONLY": {
"summary": "Turns on or off readonly mode",
"complexity": "O(1)",
"arguments": [
{
"enum": ["yes", "no"]
}
],
"since": "1.0.0",
"group": "server"
},
"FLUSHDB": {
"summary": "Removes all keys",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"FOLLOW": {
"summary": "Follows a leader host",
"complexity": "O(1)",
"arguments": [
{
"name": "host",
"type": "string"
},
{
"name": "port",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOF": {
"summary": "Downloads the AOF starting from pos and keeps the connection alive",
"complexity": "O(1)",
"arguments": [
{
"name": "pos",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOFMD5": {
"summary": "Performs a checksum on a portion of the aof",
"complexity": "O(1)",
"arguments": [
{
"name": "pos",
"type": "integer"
},
{
"name": "size",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOFSHRINK": {
"summary": "Shrinks the aof in the background",
"group": "replication"
},
"PING": {
"summary": "Ping the server",
"group": "connection"
},
"QUIT": {
"summary": "Close the connection",
"group": "connection"
},
"AUTH": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "password",
"type": "string"
}
],
"group": "connection"
},
"OUTPUT": {
"summary": "Gets or sets the output format for the current connection.",
"arguments": [
{
"name": "format",
"optional": true,
"enumargs": [
{
"name": "json"
},
{
"name": "resp"
}
]
}
],
"group": "connection"
},
"TIMEOUT": {
"summary": "Runs the following command with the timeout",
"arguments": [
{
"name": "seconds",
"type": "double"
},
{
"name": "COMMAND",
"type": "string"
},
{
"command": "arg",
"type": "string",
"multiple": true,
"optional": true
}
],
"group": "connection"
},
"SETHOOK": {
"summary": "Creates a webhook which points to geofenced search",
"arguments": [
{
"name": "name",
"type": "string"
},
{
"name": "endpoint",
"type": "string"
},
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"enum": ["NEARBY", "WITHIN", "INTERSECTS"]
},
{
"name": "key",
"type": "string"
},
{
"command": "FENCE",
"name": [],
"type": []
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "param",
"type": "string",
"variadic": true
}
],
"group": "webhook"
},
"DELHOOK": {
"summary": "Removes a webhook",
"arguments": [
{
"name": "name",
"type": "string"
}
],
"group": "webhook"
},
"HOOKS": {
"summary": "Finds all hooks matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "webhook"
},
"PDELHOOK": {
"summary": "Removes all hooks matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "webhook"
},
"SETCHAN": {
"summary": "Creates a pubsub channel which points to geofenced search",
"arguments": [
{
"name": "name",
"type": "string"
},
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"enum": ["NEARBY", "WITHIN", "INTERSECTS"]
},
{
"name": "key",
"type": "string"
},
{
"command": "FENCE",
"name": [],
"type": []
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "param",
"type": "string",
"variadic": true
}
],
"group": "pubsub"
},
"DELCHAN": {
"summary": "Removes a channel",
"arguments": [
{
"name": "name",
"type": "string"
}
],
"group": "pubsub"
},
"CHANS": {
"summary": "Finds all channels matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "pubsub"
},
"PDELCHAN": {
"summary": "Removes all channels matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "pubsub"
},
"SUBSCRIBE": {
"summary": "Subscribe to a geofence channel",
"arguments": [
{
"name": "channel",
"type": "string",
"variadic": true
}
],
"group": "pubsub"
},
"PSUBSCRIBE": {
"summary": "Subscribes the client to the given patterns",
"arguments": [
{
"name": "pattern",
"type": "pattern",
"variadic": true
}
],
"group": "pubsub"
},
"PDEL": {
"summary": "Removes all objects matching a pattern",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "pattern",
"type": "pattern"
}
],
"group": "keys"
},
"JGET": {
"summary": "Get a value from a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
},
{
"command": "RAW",
"name": [],
"type": [],
"optional": true
}
],
"group": "keys"
},
"JSET": {
"summary": "Set a value in a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
},
{
"name": "value",
"type": "string"
},
{
"name": [],
"optional": true,
"enumargs": [
{
"name": "RAW"
},
{
"name": "STR"
}
]
}
],
"group": "keys"
},
"JDEL": {
"summary": "Delete a value from a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
}
],
"group": "keys"
},
"EVAL": {
"summary": "Evaluates a Lua script",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALSHA": {
"summary": "Evaluates a Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "sha1",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALRO": {
"summary": "Evaluates a read-only Lua script",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALROSHA": {
"summary": "Evaluates a read-only Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALNA": {
"summary": "Evaluates a Lua script in a non-atomic fashion",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALNASHA": {
"summary": "Evaluates, in a non-atomic fashion, a Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "sha1",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT EXISTS": {
"summary": "Returns information about the existence of the scripts in server cache",
"complexity": "O(N) where N is the number of provided sha1 arguments",
"arguments": [
{
"name": "sha1",
"type": "string",
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT LOAD": {
"summary": "Loads the compiled version of a script into the server cache, without executing",
"complexity": "O(N) where N is the number of bytes in the script",
"arguments": [
{
"name": "script",
"type": "string"
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT FLUSH": {
"summary": "Flushes the server cache of Lua scripts",
"complexity": "O(1)",
"since": "1.10.0",
"group": "scripting"
},
"TEST": {
"summary": "Performs spatial test",
"complexity": "One test per command, complexity depends on the test",
"arguments": [
{
"name": "area1",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
}
]
},
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
},
{
"name": "test",
"enumargs": [
{
"name": "INTERSECTS"
},
{
"name": "WITHIN"
}
]
},
{
"command": "CLIP",
"name": [],
"type": [],
"optional": true
},
{
"name": "area2",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
}
]
},
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
}
],
"since": "1.16.0",
"group": "tests"
}
}
================================================
FILE: core/commands_gen.go
================================================
// This file was autogenerated. DO NOT EDIT.
package core
import (
"encoding/json"
"strings"
)
const (
clear = "\x1b[0m"
bright = "\x1b[1m"
gray = "\x1b[90m"
yellow = "\x1b[33m"
)
// Command represents a Tile38 command.
type Command struct {
Name string `json:"-"`
Summary string `json:"summary"`
Complexity string `json:"complexity"`
Arguments []Argument `json:"arguments"`
Since string `json:"since"`
Group string `json:"group"`
DevOnly bool `json:"dev"`
}
// String returns a string representation of the command.
func (c Command) String() string {
var s = c.Name
for _, arg := range c.Arguments {
s += " " + arg.String()
}
return s
}
// TermOutput returns a string representation of the command suitable for displaying in a terminal.
func (c Command) TermOutput(indent string) string {
line := c.String()
var line1 string
if strings.HasPrefix(line, c.Name) {
line1 = bright + c.Name + clear + gray + line[len(c.Name):] + clear
} else {
line1 = bright + strings.Replace(c.String(), " ", " "+clear+gray, 1) + clear
}
line2 := yellow + "summary: " + clear + c.Summary
//line3 := yellow + "since: " + clear + c.Since
return indent + line1 + "\n" + indent + line2 + "\n" //+ indent + line3 + "\n"
}
// EnumArg represents a enum arguments.
type EnumArg struct {
Name string `json:"name"`
Arguments []Argument `json:"arguments"`
}
// String returns a string representation of an EnumArg.
func (a EnumArg) String() string {
var s = a.Name
for _, arg := range a.Arguments {
s += " " + arg.String()
}
return s
}
// Argument represents a command argument.
type Argument struct {
Command string `json:"command"`
NameAny interface{} `json:"name"`
TypeAny interface{} `json:"type"`
Optional bool `json:"optional"`
Multiple bool `json:"multiple"`
Variadic bool `json:"variadic"`
Enum []string `json:"enum"`
EnumArgs []EnumArg `json:"enumargs"`
}
// String returns a string representation of an Argument.
func (a Argument) String() string {
var s string
if a.Command != "" {
s += " " + a.Command
}
if len(a.EnumArgs) > 0 {
eargs := ""
for _, arg := range a.EnumArgs {
v := arg.String()
if strings.Contains(v, " ") {
v = "(" + v + ")"
}
eargs += v + "|"
}
if len(eargs) > 0 {
eargs = eargs[:len(eargs)-1]
}
s += " " + eargs
} else if len(a.Enum) > 0 {
s += " " + strings.Join(a.Enum, "|")
} else {
names, _ := a.NameTypes()
subs := ""
for _, name := range names {
subs += " " + name
}
subs = strings.TrimSpace(subs)
s += " " + subs
if a.Variadic {
if len(names) == 0 {
s += " [" + subs + " ...]"
} else {
s += " [" + names[len(names)-1] + " ...]"
}
}
if a.Multiple {
s += " ..."
}
}
s = strings.TrimSpace(s)
if a.Optional {
s = "[" + s + "]"
}
return s
}
func parseAnyStringArray(any interface{}) []string {
if str, ok := any.(string); ok {
return []string{str}
} else if any, ok := any.([]interface{}); ok {
arr := []string{}
for _, any := range any {
if str, ok := any.(string); ok {
arr = append(arr, str)
}
}
return arr
}
return []string{}
}
// NameTypes returns the types and names of an argument as separate arrays.
func (a Argument) NameTypes() (names, types []string) {
names = parseAnyStringArray(a.NameAny)
types = parseAnyStringArray(a.TypeAny)
if len(types) > len(names) {
types = types[:len(names)]
} else {
for len(types) < len(names) {
types = append(types, "")
}
}
return
}
// Commands is a map of all of the commands.
var Commands = func() map[string]Command {
var commands map[string]Command
if err := json.Unmarshal([]byte(commandsJSON), &commands); err != nil {
panic(err.Error())
}
for name, command := range commands {
command.Name = strings.ToUpper(name)
commands[name] = command
}
return commands
}()
var commandsJSON = `{
"SET": {
"summary": "Sets the value of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "FIELD",
"name": ["name", "value"],
"type": ["string", "double"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "NX"
},
{
"name": "XX"
}
]
},
{
"name": "value",
"enumargs": [
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "z",
"type": "double",
"optional": true
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "STRING",
"arguments": [
{
"name": "value",
"type": "string"
}
]
}
]
}
],
"since": "1.0.0",
"group": "keys"
},
"EXPIRE": {
"summary": "Set a timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "seconds",
"type": "double"
}
],
"since": "1.0.0",
"group": "keys"
},
"TTL": {
"summary": "Get a timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"EXISTS": {
"summary": "Checks to see if a id exists",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"FEXISTS": {
"summary": "Checks to see if a field exists on a id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"PERSIST": {
"summary": "Remove the existing timeout on an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"FSET": {
"summary": "Set the value for one or more fields of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "XX",
"name": [],
"type": [],
"optional": true
},
{
"name": ["field", "value"],
"type": ["string", "double"]
},
{
"name": ["field", "value"],
"type": ["string", "double"],
"multiple": true,
"optional": true
}
],
"since": "1.0.0",
"group": "keys"
},
"FGET": {
"summary": "Gets the value for the field of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "field",
"type": "string"
}
],
"since": "1.33.0",
"group": "keys"
},
"BOUNDS": {
"summary": "Get the combined bounds of all the objects in a key",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
}
],
"since": "1.3.0",
"group": "keys"
},
"GET": {
"summary": "Get the object of an id",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "WITHFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "OBJECT"
},
{
"name": "POINT"
},
{
"name": "BOUNDS"
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
}
],
"since": "1.0.0",
"group": "keys"
},
"DEL": {
"summary": "Delete an id from a key",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"command": "ERRON404",
"name": [],
"type": [],
"optional": true
}
],
"since": "1.0.0",
"group": "keys"
},
"DROP": {
"summary": "Remove a key from the database",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
}
],
"since": "1.0.0",
"group": "keys"
},
"RENAME": {
"summary": "Rename a key to be stored under a different name.",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "newkey",
"type": "string"
}
],
"since": "1.14.5",
"group": "keys"
},
"RENAMENX": {
"summary": "Rename a key to be stored under a different name, if a new key does not exist.",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "newkey",
"type": "string"
}
],
"since": "1.14.5",
"group": "keys"
},
"KEYS": {
"summary": "Finds all keys matching the given pattern",
"complexity": "O(N) where N is the number of keys in the database",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"since": "1.0.0",
"group": "keys"
},
"STATS": {
"summary": "Show stats for one or more keys",
"complexity": "O(N) where N is the number of keys being requested",
"arguments": [
{
"name": "key",
"type": "string",
"variadic": true
}
],
"since": "1.0.0",
"group": "keys"
},
"SEARCH": {
"summary": "Search for string values in a key",
"complexity": "O(N) where N is the number of values in the key",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"name": "order",
"optional": true,
"enumargs": [
{
"name": "ASC"
},
{
"name": "DESC"
}
]
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
}
]
}
],
"since": "1.4.2",
"group": "search"
},
"SCAN": {
"summary": "Incrementally iterate though a key",
"complexity": "O(N) where N is the number of ids in the key",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"name": "order",
"optional": true,
"enumargs": [
{
"name": "ASC"
},
{
"name": "DESC"
}
]
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"NEARBY": {
"summary": "Searches for ids that are nearby a point",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "DISTANCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "ROAM",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "pattern",
"type": "pattern"
},
{
"name": "meters",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"WITHIN": {
"summary": "Searches for ids that completely within the area",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "SECTOR",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "radius",
"type": "double"
},
{
"name": "startBearing",
"type": "double"
},
{
"name": "endBearing",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"INTERSECTS": {
"summary": "Searches for ids that intersect an area",
"complexity": "O(log(N)) where N is the number of ids in the area",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"command": "CURSOR",
"name": "start",
"type": "integer",
"optional": true
},
{
"command": "LIMIT",
"name": "count",
"type": "integer",
"optional": true
},
{
"command": "MATCH",
"name": "pattern",
"type": "pattern",
"optional": true
},
{
"command": "WHERE",
"name": ["field", "min", "max"],
"type": ["string", "double", "double"],
"optional": true,
"multiple": true
},
{
"command": "WHEREIN",
"name": ["field", "count", "value"],
"type": ["string", "integer", "double"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVAL",
"name": ["script", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "WHEREEVALSHA",
"name": ["sha1", "numargs", "arg"],
"type": ["string", "integer", "string"],
"optional": true,
"multiple": true,
"variadic": true
},
{
"command": "CLIP",
"name": [],
"type": [],
"optional": true
},
{
"command": "NOFIELDS",
"name": [],
"type": [],
"optional": true
},
{
"command": "FENCE",
"name": [],
"type": [],
"optional": true
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "type",
"optional": true,
"enumargs": [
{
"name": "COUNT"
},
{
"name": "IDS"
},
{
"name": "OBJECTS"
},
{
"name": "POINTS"
},
{
"name": "BOUNDS"
},
{
"name": "HASHES",
"arguments": [
{
"name": "precision",
"type": "integer"
}
]
}
]
},
{
"name": "area",
"enumargs": [
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
},
{
"name": "SECTOR",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "radius",
"type": "double"
},
{
"name": "startBearing",
"type": "double"
},
{
"name": "endBearing",
"type": "double"
}
]
}
]
}
],
"since": "1.0.0",
"group": "search"
},
"CONFIG GET": {
"summary": "Get the value of a configuration parameter",
"arguments": [
{
"name": "parameter",
"type": "string"
}
],
"group": "server"
},
"CONFIG SET": {
"summary": "Set a configuration parameter to the given value",
"arguments": [
{
"name": "parameter",
"type": "string"
},
{
"name": "value",
"type": "string",
"optional": true
}
],
"group": "server"
},
"CONFIG REWRITE": {
"summary": "Rewrite the configuration file with the in memory configuration",
"arguments": [],
"group": "server"
},
"SERVER": {
"summary": "Show server stats and details",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"GC": {
"summary": "Forces a garbage collection",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"READONLY": {
"summary": "Turns on or off readonly mode",
"complexity": "O(1)",
"arguments": [
{
"enum": ["yes", "no"]
}
],
"since": "1.0.0",
"group": "server"
},
"FLUSHDB": {
"summary": "Removes all keys",
"complexity": "O(1)",
"arguments": [],
"since": "1.0.0",
"group": "server"
},
"FOLLOW": {
"summary": "Follows a leader host",
"complexity": "O(1)",
"arguments": [
{
"name": "host",
"type": "string"
},
{
"name": "port",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOF": {
"summary": "Downloads the AOF starting from pos and keeps the connection alive",
"complexity": "O(1)",
"arguments": [
{
"name": "pos",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOFMD5": {
"summary": "Performs a checksum on a portion of the aof",
"complexity": "O(1)",
"arguments": [
{
"name": "pos",
"type": "integer"
},
{
"name": "size",
"type": "integer"
}
],
"since": "1.0.0",
"group": "replication"
},
"AOFSHRINK": {
"summary": "Shrinks the aof in the background",
"group": "replication"
},
"PING": {
"summary": "Ping the server",
"group": "connection"
},
"QUIT": {
"summary": "Close the connection",
"group": "connection"
},
"AUTH": {
"summary": "Authenticate to the server",
"arguments": [
{
"name": "password",
"type": "string"
}
],
"group": "connection"
},
"OUTPUT": {
"summary": "Gets or sets the output format for the current connection.",
"arguments": [
{
"name": "format",
"optional": true,
"enumargs": [
{
"name": "json"
},
{
"name": "resp"
}
]
}
],
"group": "connection"
},
"TIMEOUT": {
"summary": "Runs the following command with the timeout",
"arguments": [
{
"name": "seconds",
"type": "double"
},
{
"name": "COMMAND",
"type": "string"
},
{
"command": "arg",
"type": "string",
"multiple": true,
"optional": true
}
],
"group": "connection"
},
"SETHOOK": {
"summary": "Creates a webhook which points to geofenced search",
"arguments": [
{
"name": "name",
"type": "string"
},
{
"name": "endpoint",
"type": "string"
},
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"enum": ["NEARBY", "WITHIN", "INTERSECTS"]
},
{
"name": "key",
"type": "string"
},
{
"command": "FENCE",
"name": [],
"type": []
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "param",
"type": "string",
"variadic": true
}
],
"group": "webhook"
},
"DELHOOK": {
"summary": "Removes a webhook",
"arguments": [
{
"name": "name",
"type": "string"
}
],
"group": "webhook"
},
"HOOKS": {
"summary": "Finds all hooks matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "webhook"
},
"PDELHOOK": {
"summary": "Removes all hooks matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "webhook"
},
"SETCHAN": {
"summary": "Creates a pubsub channel which points to geofenced search",
"arguments": [
{
"name": "name",
"type": "string"
},
{
"command": "META",
"name": ["name", "value"],
"type": ["string", "string"],
"optional": true,
"multiple": true
},
{
"command": "EX",
"name": ["seconds"],
"type": ["double"],
"optional": true,
"multiple": false
},
{
"enum": ["NEARBY", "WITHIN", "INTERSECTS"]
},
{
"name": "key",
"type": "string"
},
{
"command": "FENCE",
"name": [],
"type": []
},
{
"command": "DETECT",
"name": ["what"],
"type": ["string"],
"optional": true
},
{
"command": "COMMANDS",
"name": ["which"],
"type": ["string"],
"optional": true
},
{
"name": "param",
"type": "string",
"variadic": true
}
],
"group": "pubsub"
},
"DELCHAN": {
"summary": "Removes a channel",
"arguments": [
{
"name": "name",
"type": "string"
}
],
"group": "pubsub"
},
"CHANS": {
"summary": "Finds all channels matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "pubsub"
},
"PDELCHAN": {
"summary": "Removes all channels matching a pattern",
"arguments": [
{
"name": "pattern",
"type": "pattern"
}
],
"group": "pubsub"
},
"SUBSCRIBE": {
"summary": "Subscribe to a geofence channel",
"arguments": [
{
"name": "channel",
"type": "string",
"variadic": true
}
],
"group": "pubsub"
},
"PSUBSCRIBE": {
"summary": "Subscribes the client to the given patterns",
"arguments": [
{
"name": "pattern",
"type": "pattern",
"variadic": true
}
],
"group": "pubsub"
},
"PDEL": {
"summary": "Removes all objects matching a pattern",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "pattern",
"type": "pattern"
}
],
"group": "keys"
},
"JGET": {
"summary": "Get a value from a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
},
{
"command": "RAW",
"name": [],
"type": [],
"optional": true
}
],
"group": "keys"
},
"JSET": {
"summary": "Set a value in a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
},
{
"name": "value",
"type": "string"
},
{
"name": [],
"optional": true,
"enumargs": [
{
"name": "RAW"
},
{
"name": "STR"
}
]
}
],
"group": "keys"
},
"JDEL": {
"summary": "Delete a value from a JSON document",
"complexity": "O(1)",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
},
{
"name": "path",
"type": "string"
}
],
"group": "keys"
},
"EVAL": {
"summary": "Evaluates a Lua script",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALSHA": {
"summary": "Evaluates a Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "sha1",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALRO": {
"summary": "Evaluates a read-only Lua script",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALROSHA": {
"summary": "Evaluates a read-only Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALNA": {
"summary": "Evaluates a Lua script in a non-atomic fashion",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "script",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"EVALNASHA": {
"summary": "Evaluates, in a non-atomic fashion, a Lua script cached on the server by its SHA1 digest",
"complexity": "Depends on the evaluated script",
"arguments": [
{
"name": "sha1",
"type": "string"
},
{
"name": "numkeys",
"type": "integer"
},
{
"name": "key",
"type": "string",
"optional": true,
"multiple": true
},
{
"name": "arg",
"type": "string",
"optional": true,
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT EXISTS": {
"summary": "Returns information about the existence of the scripts in server cache",
"complexity": "O(N) where N is the number of provided sha1 arguments",
"arguments": [
{
"name": "sha1",
"type": "string",
"multiple": true
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT LOAD": {
"summary": "Loads the compiled version of a script into the server cache, without executing",
"complexity": "O(N) where N is the number of bytes in the script",
"arguments": [
{
"name": "script",
"type": "string"
}
],
"since": "1.10.0",
"group": "scripting"
},
"SCRIPT FLUSH": {
"summary": "Flushes the server cache of Lua scripts",
"complexity": "O(1)",
"since": "1.10.0",
"group": "scripting"
},
"TEST": {
"summary": "Performs spatial test",
"complexity": "One test per command, complexity depends on the test",
"arguments": [
{
"name": "area1",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
}
]
},
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
},
{
"name": "test",
"enumargs": [
{
"name": "INTERSECTS"
},
{
"name": "WITHIN"
}
]
},
{
"command": "CLIP",
"name": [],
"type": [],
"optional": true
},
{
"name": "area2",
"enumargs": [
{
"name": "POINT",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
}
]
},
{
"name": "GET",
"arguments": [
{
"name": "key",
"type": "string"
},
{
"name": "id",
"type": "string"
}
]
},
{
"name": "BOUNDS",
"arguments": [
{
"name": "minlat",
"type": "double"
},
{
"name": "minlon",
"type": "double"
},
{
"name": "maxlat",
"type": "double"
},
{
"name": "maxlon",
"type": "double"
}
]
},
{
"name": "OBJECT",
"arguments": [
{
"name": "geojson",
"type": "geojson"
}
]
},
{
"name": "CIRCLE",
"arguments": [
{
"name": "lat",
"type": "double"
},
{
"name": "lon",
"type": "double"
},
{
"name": "meters",
"type": "double"
}
]
},
{
"name": "TILE",
"arguments": [
{
"name": "x",
"type": "double"
},
{
"name": "y",
"type": "double"
},
{
"name": "z",
"type": "double"
}
]
},
{
"name": "QUADKEY",
"arguments": [
{
"name": "quadkey",
"type": "string"
}
]
},
{
"name": "HASH",
"arguments": [
{
"name": "geohash",
"type": "geohash"
}
]
}
]
}
],
"since": "1.16.0",
"group": "tests"
}
}`
================================================
FILE: core/commands_test.go
================================================
package core
import (
"fmt"
"sort"
"testing"
)
func TestCommands(t *testing.T) {
var names []string
for name := range Commands {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
cmd := Commands[name]
if cmd.Group == "server" {
fmt.Printf("%v\n", cmd.String())
}
}
}
================================================
FILE: core/gen.sh
================================================
#!/bin/sh
set -e
cd $(dirname $0)
export CommandsJSON="$(cat commands.json)"
# replace out the json
perl -pe '
while (($i = index($_, "{{.CommandsJSON}}")) != -1) {
substr($_, $i, length("{{.CommandsJSON}}")) = $ENV{"CommandsJSON"};
}
' commands.go > commands_gen.go
# remove the ignore
sed -i -e 's/\/\/go:build ignore/\/\/ This file was autogenerated. DO NOT EDIT./g' commands_gen.go
rm -rf commands_gen.go-e
================================================
FILE: core/version.go
================================================
package core
// Build variables
var (
Version = "0.0.0" // Placeholder for the version
BuildTime = "" // Placeholder for the build time
GitSHA = "0000000" // Placeholder for the git sha
)
================================================
FILE: go.mod
================================================
module github.com/tidwall/tile38
go 1.24.0
require (
github.com/tidwall/assert v0.1.0
github.com/tidwall/btree v1.8.1
github.com/tidwall/buntdb v1.3.2
github.com/tidwall/expr v0.14.0
github.com/tidwall/geojson v1.4.6
github.com/tidwall/gjson v1.18.0
github.com/tidwall/hashmap v1.8.1
github.com/tidwall/limiter v0.4.0
github.com/tidwall/match v1.2.0
github.com/tidwall/mvt v0.2.1
github.com/tidwall/pretty v1.2.1
github.com/tidwall/redbench v0.1.0
github.com/tidwall/redcon v1.6.2
github.com/tidwall/resp v0.1.1
github.com/tidwall/rtree v1.10.0
github.com/tidwall/sjson v1.2.5
github.com/tidwall/tinylru v1.2.1
)
require (
cloud.google.com/go/pubsub v1.50.0
github.com/Azure/azure-event-hubs-go/v3 v3.6.2
github.com/IBM/sarama v1.46.0
github.com/aws/aws-sdk-go v1.55.8
github.com/cloudflare/cloudflare-go/v4 v4.6.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/golang/protobuf v1.5.4
github.com/gomodule/redigo v1.9.2
github.com/iwpnd/sectr v0.1.2
github.com/mmcloughlin/geohash v0.10.0
github.com/nats-io/nats.go v1.44.0
github.com/peterh/liner v1.2.2
github.com/prometheus/client_golang v1.23.0
github.com/streadway/amqp v1.1.0
github.com/xdg-go/scram v1.1.2
github.com/yuin/gopher-lua v1.1.1
go.uber.org/atomic v1.11.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.47.0
golang.org/x/term v0.37.0
google.golang.org/api v0.246.0
google.golang.org/grpc v1.74.2
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
)
require (
cloud.google.com/go v0.121.4 // indirect
cloud.google.com/go/auth v0.16.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
github.com/Azure/azure-amqp-common-go/v4 v4.2.0 // indirect
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect
github.com/Azure/go-amqp v1.0.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/devigned/tab v0.1.1 // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/tidwall/conv v0.1.0 // indirect
github.com/tidwall/geoindex v1.7.0 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk=
cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/pubsub v1.50.0 h1:hnYpOIxVlgVD1Z8LN7est4DQZK3K6tvZNurZjIVjUe0=
cloud.google.com/go/pubsub v1.50.0/go.mod h1:Di2Y+nqXBpIS+dXUEJPQzLh8PbIQZMLE9IVUFhf2zmM=
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
github.com/Azure/azure-amqp-common-go/v4 v4.2.0 h1:q/jLx1KJ8xeI8XGfkOWMN9XrXzAfVTkyvCxPvHCjd2I=
github.com/Azure/azure-amqp-common-go/v4 v4.2.0/go.mod h1:GD3m/WPPma+621UaU6KNjKEo5Hl09z86viKwQjTpV0Q=
github.com/Azure/azure-event-hubs-go/v3 v3.6.2 h1:7rNj1/iqS/i3mUKokA2n2eMYO72TB7lO7OmpbKoakKY=
github.com/Azure/azure-event-hubs-go/v3 v3.6.2/go.mod h1:n+ocYr9j2JCLYqUqz9eI+lx/TEAtL/g6rZzyTFSuIpc=
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw=
github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-amqp v1.0.0 h1:QfCugi1M+4F2JDTRgVnRw7PYXLXZ9hmqk3+9+oJh3OA=
github.com/Azure/go-amqp v1.0.0/go.mod h1:+bg0x3ce5+Q3ahCEXnCsGG3ETpDQe3MEVnOuT2ywPwc=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM=
github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA=
github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk=
github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk=
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U=
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw=
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/IBM/sarama v1.46.0 h1:+YTM1fNd6WKMchlnLKRUB5Z0qD4M8YbvwIIPLvJD53s=
github.com/IBM/sarama v1.46.0/go.mod h1:0lOcuQziJ1/mBGHkdp5uYrltqQuKQKM5O5FOWUQVVvo=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go/v4 v4.6.0 h1:ZaWwXjHFR5NoY8UEf4QFY0g3KTi72kqqEXpajV610/o=
github.com/cloudflare/cloudflare-go/v4 v4.6.0/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA=
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/iwpnd/sectr v0.1.2 h1:FauaPRn5C2tC42HTF7gM3FJZXvGXWc6jabBbIxzTMag=
github.com/iwpnd/sectr v0.1.2/go.mod h1:Dm6YXDJCRx1NTfMX/1RMIkGfVp2ORjCY/cQGfbknz4c=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mmcloughlin/geohash v0.10.0 h1:9w1HchfDfdeLc+jFEf/04D27KP7E2QmpDu52wPbJWRE=
github.com/mmcloughlin/geohash v0.10.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjwFUqcTB1SmG0c=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM=
github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
github.com/tidwall/cities v0.1.0 h1:CVNkmMf7NEC9Bvokf5GoSsArHCKRMTgLuubRTHnH0mE=
github.com/tidwall/cities v0.1.0/go.mod h1:lV/HDp2gCcRcHJWqgt6Di54GiDrTZwh1aG2ZUPNbqa4=
github.com/tidwall/conv v0.1.0 h1:hieA69RrgNAIpirLIymxM1+v0TmuijwU4w9qfeyApHo=
github.com/tidwall/conv v0.1.0/go.mod h1:jvlaM2rq1CHtNznWb3A5i+tkG6k6XYeMUNwPfyEGxIo=
github.com/tidwall/expr v0.14.0 h1:tb2MDhay/Qtorm+UXv3DXdCpMlpsE8Phg1bWhXMM3Dw=
github.com/tidwall/expr v0.14.0/go.mod h1:R5XZxQS2HA/teLqU67CqLZX78FyfvcFoDBDne6VRlaA=
github.com/tidwall/geoindex v1.4.4/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
github.com/tidwall/geoindex v1.7.0 h1:jtk41sfgwIt8MEDyC3xyKSj75iXXf6rjReJGDNPtR5o=
github.com/tidwall/geoindex v1.7.0/go.mod h1:rvVVNEFfkJVWGUdEfU8QaoOg/9zFX0h9ofWzA60mz1I=
github.com/tidwall/geojson v1.4.6 h1:HpEGer4tc5ieFn8Ts8aTG9fo+hgFJkqfql4O9cgphmg=
github.com/tidwall/geojson v1.4.6/go.mod h1:1cn3UWfSYCJOq53NZoQ9rirdw89+DM0vw+ZOAVvuReg=
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
github.com/tidwall/hashmap v1.8.1 h1:hXNzBfSJ2Jwvt0lbkWD59O/r3OfatSIcbuWT0VKEVns=
github.com/tidwall/hashmap v1.8.1/go.mod h1:v+0qJrJn7l+l2dB8+fAFpC62p2G0SMP2Teu8ejkebg8=
github.com/tidwall/limiter v0.4.0 h1:nj+7mS6aMDRzp15QTVDrgkun0def5/PfB4ogs5NlIVQ=
github.com/tidwall/limiter v0.4.0/go.mod h1:n+qBGuSOgAvgcq1xUvo+mXWg8oBLQC8wkkheN9KZou0=
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/mvt v0.2.1 h1:pVoD4/INMeXk8VF8yfMOzn534W4u/0vhSnqyi+/FfG4=
github.com/tidwall/mvt v0.2.1/go.mod h1:RLOVf5y8yfDR4vyA636MiY0kBJeP/i7RxxpZzygREI8=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/redbench v0.1.0 h1:UZYUMhwMMObQRq5xU4SA3lmlJRztXzqtushDii+AmPo=
github.com/tidwall/redbench v0.1.0/go.mod h1:zxcRGCq/JcqV48YjK9WxBNJL7JSpMzbLXaHvMcnanKQ=
github.com/tidwall/redcon v1.6.2 h1:5qfvrrybgtO85jnhSravmkZyC0D+7WstbfCs3MmPhow=
github.com/tidwall/redcon v1.6.2/go.mod h1:p5Wbsgeyi2VSTBWOcA5vRXrOb9arFTcU2+ZzFjqV75Y=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8=
github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ=
github.com/tidwall/rtree v1.3.1/go.mod h1:S+JSsqPTI8LfWA4xHBo5eXzie8WJLVFeppAutSegl6M=
github.com/tidwall/rtree v1.10.0 h1:+EcI8fboEaW1L3/9oW/6AMoQ8HiEIHyR7bQOGnmz4Mg=
github.com/tidwall/rtree v1.10.0/go.mod h1:iDJQ9NBRtbfKkzZu02za+mIlaP+bjYPnunbSNidpbCQ=
github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tidwall/tinylru v1.2.1 h1:VgBr72c2IEr+V+pCdkPZUwiQ0KJknnWIYbhxAVkYfQk=
github.com/tidwall/tinylru v1.2.1/go.mod h1:9bQnEduwB6inr2Y7AkBP7JPgCkyrhTV/ZpX0oOOpBI4=
github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE=
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.246.0 h1:H0ODDs5PnMZVZAEtdLMn2Ul2eQi7QNjqM2DIFp8TlTM=
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s=
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf h1:rRz0YsF7VXj9fXRF6yQgFI7DzST+hsI3TeFSGupntu0=
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc=
================================================
FILE: internal/bing/bing.go
================================================
// https://msdn.microsoft.com/en-us/library/bb259689.aspx
package bing
import "math"
const (
// EarthRadius is the radius of the earth
EarthRadius = 6378137.0
// MinLatitude is the min lat
MinLatitude = -85.05112878
// MaxLatitude is the max lat
MaxLatitude = 85.05112878
// MinLongitude is the min lon
MinLongitude = -180.0
// MaxLongitude is the max lon
MaxLongitude = 180.0
// TileSize is the size of a tile
TileSize = 256
// MaxLevelOfDetail is the max level of detail
MaxLevelOfDetail = 38
)
// Clips a number to the specified minimum and maximum values.
// Param 'n' is the number to clip.
// Param 'minValue' is the minimum allowable value.
// Param 'maxValue' is the maximum allowable value.
// Returns the clipped value.
func clip(n, minValue, maxValue float64) float64 {
if n < minValue {
return minValue
}
if n > maxValue {
return maxValue
}
return n
}
// MapSize determines the map width and height (in pixels) at a specified level of detail.
// Param 'levelOfDetail' is the level of detail, from 1 (lowest detail) to N (highest detail).
// Returns the map width and height in pixels.
func MapSize(levelOfDetail uint64) uint64 {
return TileSize << levelOfDetail
}
// // Determines the ground resolution (in meters per pixel) at a specified latitude and level of detail.
// // Param 'latitude' is the Latitude (in degrees) at which to measure the ground resolution.
// // Param 'levelOfDetail' is the Level of detail, from 1 (lowest detail) to N (highest detail).
// // Returns the ground resolution, in meters per pixel.
// func GroundResolution(latitude float64, levelOfDetail uint64) float64 {
// latitude = clip(latitude, MinLatitude, MaxLatitude)
// return math.Cos(latitude*math.Pi/180) * 2 * math.Pi * EarthRadius / float64(MapSize(levelOfDetail))
// }
// // Determines the map scale at a specified latitude, level of detail, and screen resolution.
// // Param 'latitude' is the latitude (in degrees) at which to measure the map scale.
// // Param 'levelOfDetail' is the level of detail, from 1 (lowest detail) to N (highest detail).
// // Param 'screenDpi' is the resolution of the screen, in dots per inch.
// // Returns the map scale, expressed as the denominator N of the ratio 1 : N.
// func MapScale(latitude float64, levelOfDetail, screenDpi uint64) float64 {
// return GroundResolution(latitude, levelOfDetail) * float64(screenDpi) / 0.0254
// }
// LatLongToPixelXY converts a point from latitude/longitude WGS-84 coordinates (in degrees) into pixel XY coordinates at a specified level of detail.
// Param 'latitude' is the latitude of the point, in degrees.
// Param 'longitude' is the longitude of the point, in degrees.
// Param 'levelOfDetail' is the level of detail, from 1 (lowest detail) to N (highest detail).
// Return value 'pixelX' is the output parameter receiving the X coordinate in pixels.
// Return value 'pixelY' is the output parameter receiving the Y coordinate in pixels.
func LatLongToPixelXY(latitude, longitude float64, levelOfDetail uint64) (pixelX, pixelY int64) {
latitude = clip(latitude, MinLatitude, MaxLatitude)
longitude = clip(longitude, MinLongitude, MaxLongitude)
x := (longitude + 180) / 360
sinLatitude := math.Sin(latitude * math.Pi / 180)
y := 0.5 - math.Log((1+sinLatitude)/(1-sinLatitude))/(4*math.Pi)
mapSize := float64(MapSize(levelOfDetail))
pixelX = int64(clip(x*mapSize+0.5, 0, mapSize-1))
pixelY = int64(clip(y*mapSize+0.5, 0, mapSize-1))
return
}
// PixelXYToLatLong converts a pixel from pixel XY coordinates at a specified level of detail into latitude/longitude WGS-84 coordinates (in degrees).
// Param 'pixelX' is the X coordinate of the point, in pixels.
// Param 'pixelY' is the Y coordinates of the point, in pixels.
// Param 'levelOfDetail' is the level of detail, from 1 (lowest detail) to N (highest detail).
// Return value 'latitude' is the output parameter receiving the latitude in degrees.
// Return value 'longitude' is the output parameter receiving the longitude in degrees.
func PixelXYToLatLong(pixelX, pixelY int64, levelOfDetail uint64) (latitude, longitude float64) {
mapSize := float64(MapSize(levelOfDetail))
x := (clip(float64(pixelX), 0, mapSize-1) / mapSize) - 0.5
y := 0.5 - (clip(float64(pixelY), 0, mapSize-1) / mapSize)
latitude = 90 - 360*math.Atan(math.Exp(-y*2*math.Pi))/math.Pi
longitude = 360 * x
return
}
// PixelXYToTileXY converts pixel XY coordinates into tile XY coordinates of the tile containing the specified pixel.
// Param 'pixelX' is the pixel X coordinate.
// Param 'pixelY' is the pixel Y coordinate.
// Return value 'tileX' is the output parameter receiving the tile X coordinate.
// Return value 'tileY' is the output parameter receiving the tile Y coordinate.
func PixelXYToTileXY(pixelX, pixelY int64) (tileX, tileY int64) {
return pixelX >> 8, pixelY >> 8
}
// TileXYToPixelXY converts tile XY coordinates into pixel XY coordinates of the upper-left pixel of the specified tile.
// Param 'tileX' is the tile X coordinate.
// Param 'tileY' is the tile Y coordinate.
// Return value 'pixelX' is the output parameter receiving the pixel X coordinate.
// Return value 'pixelY' is the output parameter receiving the pixel Y coordinate.
func TileXYToPixelXY(tileX, tileY int64) (pixelX, pixelY int64) {
return tileX << 8, tileY << 8
}
// TileXYToQuadKey converts tile XY coordinates into a QuadKey at a specified level of detail.
// Param 'tileX' is the tile X coordinate.
// Param 'tileY' is the tile Y coordinate.
// Param 'levelOfDetail' is the Level of detail, from 1 (lowest detail) to N (highest detail).
// Returns a string containing the QuadKey.
func TileXYToQuadKey(tileX, tileY int64, levelOfDetail uint64) string {
quadKey := make([]byte, levelOfDetail)
for i, j := levelOfDetail, 0; i > 0; i, j = i-1, j+1 {
mask := int64(1 << (i - 1))
if (tileX & mask) != 0 {
if (tileY & mask) != 0 {
quadKey[j] = '3'
} else {
quadKey[j] = '1'
}
} else if (tileY & mask) != 0 {
quadKey[j] = '2'
} else {
quadKey[j] = '0'
}
}
return string(quadKey)
}
// QuadKeyToTileXY converts a QuadKey into tile XY coordinates.
// Param 'quadKey' is the quadKey of the tile.
// Return value 'tileX' is the output parameter receiving the tile X coordinate.
// Return value 'tileY is the output parameter receiving the tile Y coordinate.
// Return value 'levelOfDetail' is the output parameter receiving the level of detail.
func QuadKeyToTileXY(quadKey string) (tileX, tileY int64, levelOfDetail uint64) {
levelOfDetail = uint64(len(quadKey))
for i := levelOfDetail; i > 0; i-- {
mask := int64(1 << (i - 1))
switch quadKey[levelOfDetail-i] {
case '0':
case '1':
tileX |= mask
case '2':
tileY |= mask
case '3':
tileX |= mask
tileY |= mask
default:
panic("Invalid QuadKey digit sequence.")
}
}
return
}
================================================
FILE: internal/bing/bing_test.go
================================================
package bing
import (
"math/rand"
"testing"
"time"
)
func TestLevelFuzz(t *testing.T) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10000; i++ {
level := (rand.Int() % MaxLevelOfDetail) + 1
quad := ""
for j := 0; j < level; j++ {
quad += string(byte(rand.Int()%4) + '0')
}
tileX, tileY, levelOfDetail := QuadKeyToTileXY(quad)
if levelOfDetail != uint64(len(quad)) {
t.Fatalf("[%d,%d] levelOfDetail == %d, expect %d", i, level, levelOfDetail, len(quad))
}
pixelX, pixelY := TileXYToPixelXY(tileX, tileY)
latitude, longitude := PixelXYToLatLong(pixelX, pixelY, levelOfDetail)
pixelX2, pixelY2 := LatLongToPixelXY(latitude, longitude, levelOfDetail)
if pixelX2 != pixelX {
t.Fatalf("[%d,%d] pixelX2 == %d, expect %d", i, level, pixelX2, pixelX)
}
if pixelY2 != pixelY {
t.Fatalf("[%d,%d] pixelY2 == %d, expect %d", i, level, pixelY2, pixelY)
}
tileX2, tileY2 := PixelXYToTileXY(pixelX2, pixelY2)
if tileX2 != tileX {
t.Fatalf("[%d,%d] tileX2 == %d, expect %d", i, level, tileX2, tileX)
}
if tileY2 != tileY {
t.Fatalf("[%d,%d] tileY2 == %d, expect %d", i, level, tileY2, tileY)
}
quad2 := TileXYToQuadKey(tileX2, tileY2, levelOfDetail)
if quad2 != quad {
t.Fatalf("[%d,%d] quad2 == %s, expect %s", i, level, quad2, quad)
}
}
}
func TestInvalidQuadKeyFuzz(t *testing.T) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10000; i++ {
func() {
defer func() {
var s string
if v := recover(); v != nil {
s = v.(string)
}
if s != "Invalid QuadKey digit sequence." {
t.Fatalf("s == '%s', expect '%s", s, "Invalid QuadKey digit sequence.")
}
}()
level := (rand.Int() % MaxLevelOfDetail) + 1
valid := true
quad := ""
for valid {
quad = ""
for j := 0; j < level; j++ {
c := byte(rand.Int()%5) + '0'
quad += string(c)
if c < '0' || c > '3' {
valid = false
}
}
}
QuadKeyToTileXY(quad)
}()
}
}
func TestLatLonClippingFuzz(t *testing.T) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10000; i++ {
lat := clip(rand.Float64()*180.0-90.0, MinLatitude, MaxLatitude)
lon := clip(rand.Float64()*380.0-180.0, MinLongitude, MaxLongitude)
if lat < MinLatitude {
t.Fatalf("lat == %f, expect < %f", lat, MinLatitude)
}
if lat > MaxLatitude {
t.Fatalf("lat == %f, expect > %f", lat, MaxLatitude)
}
if lon < MinLongitude {
t.Fatalf("lon == %f, expect < %f", lon, MinLongitude)
}
if lon > MaxLongitude {
t.Fatalf("lon == %f, expect > %f", lon, MaxLongitude)
}
}
}
func TestIssue302(t *testing.T) {
// Requesting tile with zoom level > 63 crashes the server #302
for z := uint64(0); z < 256; z++ {
tileX, tileY := PixelXYToTileXY(LatLongToPixelXY(33, -115, z))
TileXYToBounds(tileX, tileY, z)
}
}
================================================
FILE: internal/bing/ext.go
================================================
package bing
import "errors"
// LatLongToQuad iterates through all of the quads parts until levelOfDetail is reached.
func LatLongToQuad(latitude, longitude float64, levelOfDetail uint64, iterator func(part int) bool) {
pixelX, pixelY := LatLongToPixelXY(latitude, longitude, levelOfDetail)
tileX, tileY := PixelXYToTileXY(pixelX, pixelY)
for i := levelOfDetail; i > 0; i-- {
if !iterator(partForTileXY(tileX, tileY, i)) {
break
}
}
}
func partForTileXY(tileX, tileY int64, levelOfDetail uint64) int {
mask := int64(1 << (levelOfDetail - 1))
if (tileX & mask) != 0 {
if (tileY & mask) != 0 {
return 3
}
return 1
} else if (tileY & mask) != 0 {
return 2
}
return 0
}
// TileXYToBounds returns the bounds around a tile.
func TileXYToBounds(tileX, tileY int64, levelOfDetail uint64) (minLat, minLon, maxLat, maxLon float64) {
size := int64(1 << levelOfDetail)
pixelX, pixelY := TileXYToPixelXY(tileX, tileY)
maxLat, minLon = PixelXYToLatLong(pixelX, pixelY, levelOfDetail)
pixelX, pixelY = TileXYToPixelXY(tileX+1, tileY+1)
minLat, maxLon = PixelXYToLatLong(pixelX, pixelY, levelOfDetail)
if size == 0 || tileX%size == 0 {
minLon = MinLongitude
}
if size == 0 || tileX%size == size-1 {
maxLon = MaxLongitude
}
if tileY <= 0 {
maxLat = MaxLatitude
}
if tileY >= size-1 {
minLat = MinLatitude
}
return
}
// QuadKeyToBounds converts a quadkey to bounds
func QuadKeyToBounds(quadkey string) (minLat, minLon, maxLat, maxLon float64, err error) {
for i := 0; i < len(quadkey); i++ {
switch quadkey[i] {
case '0', '1', '2', '3':
default:
err = errors.New("invalid quadkey")
return
}
}
minLat, minLon, maxLat, maxLon = TileXYToBounds(QuadKeyToTileXY(quadkey))
return
}
================================================
FILE: internal/bing/ext_test.go
================================================
package bing
import (
"math/rand"
"testing"
"time"
)
func TestIteratorFuzz(t *testing.T) {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10000; i++ {
latitude := rand.Float64()*180.0 - 90.0
longitude := rand.Float64()*380.0 - 180.0
levelOfDetail := uint64((rand.Int() % MaxLevelOfDetail) + 1)
pixelX, pixelY := LatLongToPixelXY(latitude, longitude, levelOfDetail)
tileX, tileY := PixelXYToTileXY(pixelX, pixelY)
quad1 := TileXYToQuadKey(tileX, tileY, levelOfDetail)
l := rand.Int() % len(quad1)
i := 0
quad2 := ""
LatLongToQuad(latitude, longitude, levelOfDetail, func(part int) bool {
if i == l {
return false
}
quad2 += string(byte(part) + '0')
i++
return true
})
if quad2 != quad1[:l] {
t.Fatalf("[%d,%d] quad2 == %s, expect %s", i, levelOfDetail, quad2, quad1[:l])
}
}
}
func TestExt(t *testing.T) {
// tileX, tileY, levelOfDetail := int64(0), int64(0), uint64(0)
// parts := strings.Split(os.Getenv("TEST_TILE"), ",")
// if len(parts) == 3 {
// tileX, _ = strconv.ParseInt(parts[0], 10, 64)
// tileY, _ = strconv.ParseInt(parts[1], 10, 64)
// levelOfDetail, _ = strconv.ParseUint(parts[2], 10, 64)
// }
// minLat, minLon, maxLat, maxLon := TileXYToBounds(tileX, tileY, levelOfDetail)
// fmt.Printf("\x1b[32m== Tile Boundaries ==\x1b[0m\n")
// fmt.Printf("\x1b[31m%d,%d,%d\x1b[0m\n", tileX, tileY, levelOfDetail)
// fmt.Printf("\x1b[31mWGS84 datum (longitude/latitude):\x1b[0m\n")
// fmt.Printf("%v %v\n%v %v\n\n", minLon, minLat, maxLon, maxLat)
//fmt.Printf("\x1b[32m\x1b[0m\n%v %v\n%v %v\n\n", minLon, minLat, maxLon, maxLat)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(1, 0, 1)
// fmt.Printf("\x1b[32m1,0\x1b[0m\n%v %v\n%v %v\n\n", minLon, minLat, maxLon, maxLat)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(0, 1, 1)
// fmt.Printf("\x1b[32m0,1\x1b[0m\n%v %v\n%v %v\n\n", minLon, minLat, maxLon, maxLat)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(1, 1, 1)
// fmt.Printf("\x1b[32m1,1\x1b[0m\n%v %v\n%v %v\n\n", minLon, minLat, maxLon, maxLat)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(1, 0, 1)
// fmt.Printf("1,0: %f,%f %f,%f\n", minLat, minLon, maxLat, maxLon)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(0, 1, 1)
// fmt.Printf("0,1: %f,%f %f,%f\n", minLat, minLon, maxLat, maxLon)
// minLat, minLon, maxLat, maxLon = TileXYToBounds(1, 1, 1)
// fmt.Printf("1,1: %f,%f %f,%f\n", minLat, minLon, maxLat, maxLon)
}
================================================
FILE: internal/buffer/buffer.go
================================================
package buffer
import (
"errors"
"math"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geo"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/gjson"
)
// TODO: detect of pole and antimeridian crossing and generate
// valid multigeometries
const bufferSteps = 15
// Simple performs a very simple buffer operation on a geojson object.
func Simple(g geojson.Object, meters float64) (geojson.Object, error) {
if meters <= 0 {
return g, nil
}
if math.IsInf(meters, 0) || math.IsNaN(meters) {
return g, errors.New("invalid meters")
}
switch g := g.(type) {
case *geojson.Point:
return bufferSimplePoint(g.Base(), meters), nil
case *geojson.SimplePoint:
return bufferSimplePoint(g.Base(), meters), nil
case *geojson.MultiPoint:
return bufferSimpleGeometries(g.Base(), meters)
case *geojson.LineString:
return bufferSimpleLineString(g, meters)
case *geojson.MultiLineString:
return bufferSimpleGeometries(g.Base(), meters)
case *geojson.Polygon:
return bufferSimplePolygon(g, meters)
case *geojson.MultiPolygon:
return bufferSimpleGeometries(g.Base(), meters)
case *geojson.FeatureCollection:
return bufferSimpleFeatures(g.Base(), meters)
case *geojson.Feature:
bg, err := Simple(g.Base(), meters)
if err != nil {
return nil, err
}
return geojson.NewFeature(bg, g.Members()), nil
case *geojson.Circle:
return Simple(g.Polygon(), meters)
case nil:
return nil, errors.New("cannot buffer nil object")
default:
typ := gjson.Get(g.JSON(), "type").String()
return nil, errors.New("cannot buffer " + typ + " type")
}
}
func bufferSimplePoint(p geometry.Point, meters float64) *geojson.Polygon {
meters = geo.NormalizeDistance(meters)
points := make([]geometry.Point, 0, bufferSteps+1)
// calc the four corners
maxY, _ := geo.DestinationPoint(p.Y, p.X, meters, 0)
_, maxX := geo.DestinationPoint(p.Y, p.X, meters, 90)
minY, _ := geo.DestinationPoint(p.Y, p.X, meters, 180)
_, minX := geo.DestinationPoint(p.Y, p.X, meters, 270)
// use the half width of the lat and lon
lons := (maxX - minX) / 2
lats := (maxY - minY) / 2
// generate the circle polygon
for th := 0.0; th <= 360.0; th += 360.0 / float64(bufferSteps) {
radians := (math.Pi / 180) * th
x := p.X + lons*math.Cos(radians)
y := p.Y + lats*math.Sin(radians)
points = append(points, geometry.Point{X: x, Y: y})
}
// add last connecting point, make a total of steps+1
points = append(points, points[0])
poly := geojson.NewPolygon(
geometry.NewPoly(points, nil, &geometry.IndexOptions{
Kind: geometry.None,
}),
)
return poly
}
func bufferSimpleGeometries(objs []geojson.Object, meters float64,
) (*geojson.GeometryCollection, error) {
geoms := make([]geojson.Object, len(objs))
for i := 0; i < len(objs); i++ {
g, err := Simple(objs[i], meters)
if err != nil {
return nil, err
}
geoms[i] = g
}
return geojson.NewGeometryCollection(geoms), nil
}
func bufferSimpleFeatures(objs []geojson.Object, meters float64,
) (*geojson.FeatureCollection, error) {
geoms := make([]geojson.Object, len(objs))
for i := 0; i < len(objs); i++ {
g, err := Simple(objs[i], meters)
if err != nil {
return nil, err
}
geoms[i] = g
}
return geojson.NewFeatureCollection(geoms), nil
}
// appendBufferSimpleSeries buffers a series and appends its parts to dst
func appendBufferSimpleSeries(dst []geojson.Object, s geometry.Series, meters float64) []geojson.Object {
nsegs := s.NumSegments()
for i := 0; i < nsegs; i++ {
dst = appendSimpleBufferSegment(dst, s.SegmentAt(i), meters, i == 0)
}
return dst
}
// appendSimpleBufferSegment buffers a segment and appends its parts to dst
func appendSimpleBufferSegment(dst []geojson.Object, seg geometry.Segment,
meters float64, first bool,
) []geojson.Object {
if first {
// endcap A
dst = append(dst, bufferSimplePoint(seg.A, meters))
}
// line polygon
bear1 := geo.BearingTo(seg.A.Y, seg.A.X, seg.B.Y, seg.B.X)
lat1, lon1 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1-90)
lat2, lon2 := geo.DestinationPoint(seg.A.Y, seg.A.X, meters, bear1+90)
bear2 := geo.BearingTo(seg.B.Y, seg.B.X, seg.A.Y, seg.A.X)
lat3, lon3 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2-90)
lat4, lon4 := geo.DestinationPoint(seg.B.Y, seg.B.X, meters, bear2+90)
dst = append(dst, geojson.NewPolygon(
geometry.NewPoly([]geometry.Point{
{X: lon1, Y: lat1},
{X: lon2, Y: lat2},
{X: lon3, Y: lat3},
{X: lon4, Y: lat4},
{X: lon1, Y: lat1},
}, nil, nil)))
// endcap B
dst = append(dst, bufferSimplePoint(seg.B, meters))
return dst
}
func bufferSimplePolygon(p *geojson.Polygon, meters float64,
) (*geojson.GeometryCollection, error) {
var geoms []geojson.Object
b := p.Base()
geoms = appendBufferSimpleSeries(geoms, b.Exterior, meters)
for _, hole := range b.Holes {
geoms = appendBufferSimpleSeries(geoms, hole, meters)
}
geoms = append(geoms, p)
return geojson.NewGeometryCollection(geoms), nil
}
func bufferSimpleLineString(l *geojson.LineString, meters float64,
) (*geojson.GeometryCollection, error) {
geoms := appendBufferSimpleSeries(nil, l.Base(), meters)
return geojson.NewGeometryCollection(geoms), nil
}
================================================
FILE: internal/buffer/buffer_test.go
================================================
package buffer
import (
"testing"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
const lineString = `{"type":"LineString","coordinates":[
[-116.40289306640624,34.125447565116126],
[-116.36444091796875,34.14818102254435],
[-116.0980224609375,34.15045403191448],
[-115.74920654296874,34.127721186043985],
[-115.54870605468749,34.075412438417395],
[-115.5267333984375,34.11407854333859],
[-115.21911621093749,34.048108084909835],
[-115.25207519531249,33.8339199536547],
[-115.40588378906249,33.71748624018193]
]}`
var lineInPoints = []geometry.Point{
{X: -115.64363479614258, Y: 34.108251327293296},
{X: -115.54355621337892, Y: 34.07199987534163},
{X: -115.21482467651367, Y: 34.051237154976164},
{X: -115.4110336303711, Y: 33.715201644740844},
{X: -116.40701293945311, Y: 34.12345809664606},
}
func TestBufferLineString(t *testing.T) {
g, err := geojson.Parse(lineString, nil)
if err != nil {
t.Fatal(err)
}
g2, err := Simple(g, 1000)
if err != nil {
t.Fatal(err)
}
for _, pt := range lineInPoints {
ok := g2.Contains(geojson.NewPoint(pt))
if !ok {
t.Fatalf("!ok")
}
}
}
const polygon = `{"type": "Polygon","coordinates":[
[
[116.46881103515624,34.277644878733824],
[115.87280273437499,34.20953080048952],
[115.70251464843749,34.397844946449865],
[115.9881591796875,34.61286625296406],
[116.46881103515624,34.277644878733824]
],
[
[115.90438842773436,34.38651267795365],
[116.05270385742188,34.35023911062779],
[115.99914550781249,34.44655621402982],
[115.90438842773436,34.38651267795365]
]
]}`
var polyInPoints = []geometry.Point{
{X: 115.95837593078612, Y: 34.59887847065301},
{X: 115.98755836486816, Y: 34.61879975173954},
{X: 115.98833084106445, Y: 34.59795999847678},
{X: 116.04536533355714, Y: 34.58082509817638},
{X: 116.47567749023438, Y: 34.27651009584797},
{X: 116.42005920410155, Y: 34.32018817684490},
{X: 116.33216857910156, Y: 34.25948651450623},
{X: 115.89340209960939, Y: 34.24132422972854},
{X: 115.95588684082033, Y: 34.42786803680155},
{X: 115.97236633300783, Y: 34.42107129982385},
{X: 115.99639892578125, Y: 34.43579686485573},
{X: 116.04652404785155, Y: 34.35364042469895},
{X: 115.92155456542967, Y: 34.38877925439021},
{X: 115.96755981445311, Y: 34.37687904351907},
{X: 115.88859558105467, Y: 34.42956713470528},
{X: 115.97511291503906, Y: 34.36327673174518},
{X: 115.69564819335938, Y: 34.39784494644986},
{X: 115.87005615234375, Y: 34.20385213966983},
{X: 115.76980590820312, Y: 34.31678550602221},
}
var polyOutPoints = []geometry.Point{
{X: 115.68534851074217, Y: 34.40917568058836},
{X: 115.98953247070312, Y: 34.63038297923298},
{X: 115.98541259765624, Y: 34.39671178864245},
{X: 116.31500244140626, Y: 34.22145474280257},
{X: 115.85426330566406, Y: 34.18510984477340},
}
func TestBufferPolygon(t *testing.T) {
g, err := geojson.Parse(polygon, nil)
if err != nil {
t.Fatal(err)
}
g2, err := Simple(g, 1000)
if err != nil {
t.Fatal(err)
}
for _, pt := range polyInPoints {
ok := g2.Contains(geojson.NewPoint(pt))
if !ok {
t.Fatalf("!ok")
}
}
for _, pt := range polyOutPoints {
ok := g2.Contains(geojson.NewPoint(pt))
if ok {
t.Fatalf("ok")
}
}
}
================================================
FILE: internal/clip/clip.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
// Clip clips the contents of a geojson object and return
func Clip(
obj geojson.Object, clipper geojson.Object, opts *geometry.IndexOptions,
) (clipped geojson.Object) {
switch obj := obj.(type) {
case *geojson.Point:
return clipPoint(obj, clipper, opts)
case *geojson.Rect:
return clipRect(obj, clipper, opts)
case *geojson.LineString:
return clipLineString(obj, clipper, opts)
case *geojson.Polygon:
return clipPolygon(obj, clipper, opts)
case *geojson.Feature:
return clipFeature(obj, clipper, opts)
case geojson.Collection:
return clipCollection(obj, clipper, opts)
}
return obj
}
// clipSegment is Cohen-Sutherland Line Clipping
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/lineClip.html
func clipSegment(seg geometry.Segment, rect geometry.Rect) (
res geometry.Segment, rejected bool,
) {
startCode := getCode(rect, seg.A)
endCode := getCode(rect, seg.B)
if (startCode | endCode) == 0 {
// trivially accept
res = seg
} else if (startCode & endCode) != 0 {
// trivially reject
rejected = true
} else if startCode != 0 {
// start is outside. get new start.
newStart := intersect(rect, startCode, seg.A, seg.B)
res, rejected =
clipSegment(geometry.Segment{A: newStart, B: seg.B}, rect)
} else {
// end is outside. get new end.
newEnd := intersect(rect, endCode, seg.A, seg.B)
res, rejected = clipSegment(geometry.Segment{A: seg.A, B: newEnd}, rect)
}
return
}
// clipRing is Sutherland-Hodgman Polygon Clipping
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/intro2.html
func clipRing(ring []geometry.Point, bbox geometry.Rect) (
resRing []geometry.Point,
) {
if len(ring) < 4 {
// under 4 elements this is not a polygon ring!
return
}
var edge uint8
var inside, prevInside bool
var prev geometry.Point
for edge = 1; edge <= 8; edge *= 2 {
prev = ring[len(ring)-2]
prevInside = (getCode(bbox, prev) & edge) == 0
for _, p := range ring {
inside = (getCode(bbox, p) & edge) == 0
if prevInside && inside {
// Staying inside
resRing = append(resRing, p)
} else if prevInside && !inside {
// Leaving
resRing = append(resRing, intersect(bbox, edge, prev, p))
} else if !prevInside && inside {
// Entering
resRing = append(resRing, intersect(bbox, edge, prev, p))
resRing = append(resRing, p)
} /* else {
// Stay outside
} */
prev, prevInside = p, inside
}
if len(resRing) > 0 && resRing[0] != resRing[len(resRing)-1] {
resRing = append(resRing, resRing[0])
}
ring, resRing = resRing, []geometry.Point{}
if len(ring) == 0 {
break
}
}
resRing = ring
return
}
func getCode(bbox geometry.Rect, point geometry.Point) (code uint8) {
code = 0
if point.X < bbox.Min.X {
code |= 1 // left
} else if point.X > bbox.Max.X {
code |= 2 // right
}
if point.Y < bbox.Min.Y {
code |= 4 // bottom
} else if point.Y > bbox.Max.Y {
code |= 8 // top
}
return
}
func intersect(bbox geometry.Rect, code uint8, start, end geometry.Point) (
new geometry.Point,
) {
if (code & 8) != 0 { // top
new = geometry.Point{
X: start.X + (end.X-start.X)*(bbox.Max.Y-start.Y)/(end.Y-start.Y),
Y: bbox.Max.Y,
}
} else if (code & 4) != 0 { // bottom
new = geometry.Point{
X: start.X + (end.X-start.X)*(bbox.Min.Y-start.Y)/(end.Y-start.Y),
Y: bbox.Min.Y,
}
} else if (code & 2) != 0 { //right
new = geometry.Point{
X: bbox.Max.X,
Y: start.Y + (end.Y-start.Y)*(bbox.Max.X-start.X)/(end.X-start.X),
}
} else if (code & 1) != 0 { // left
new = geometry.Point{
X: bbox.Min.X,
Y: start.Y + (end.Y-start.Y)*(bbox.Min.X-start.X)/(end.X-start.X),
}
} /* else {
// should not call intersect with the zero code
} */
return
}
================================================
FILE: internal/clip/clip_test.go
================================================
package clip
import (
"testing"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func LO(points []geometry.Point) *geojson.LineString {
return geojson.NewLineString(geometry.NewLine(points, nil))
}
func RO(minX, minY, maxX, maxY float64) *geojson.Rect {
return geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minX, Y: minY},
Max: geometry.Point{X: maxX, Y: maxY},
})
}
func PPO(exterior []geometry.Point, holes [][]geometry.Point) *geojson.Polygon {
return geojson.NewPolygon(geometry.NewPoly(exterior, holes, nil))
}
func TestClipLineStringSimple(t *testing.T) {
ls := LO([]geometry.Point{
{X: 1, Y: 1},
{X: 2, Y: 2},
{X: 3, Y: 1}})
clipped := Clip(ls, RO(1.5, 0.5, 2.5, 1.8), nil)
cl, ok := clipped.(*geojson.MultiLineString)
if !ok {
t.Fatal("wrong type")
}
if len(cl.Children()) != 2 {
t.Fatal("result must have two parts in MultiString")
}
}
func TestClipPolygonSimple(t *testing.T) {
exterior := []geometry.Point{
{X: 2, Y: 2},
{X: 1, Y: 2},
{X: 1.5, Y: 1.5},
{X: 1, Y: 1},
{X: 2, Y: 1},
{X: 2, Y: 2},
}
holes := [][]geometry.Point{
{
{X: 1.9, Y: 1.9},
{X: 1.2, Y: 1.9},
{X: 1.45, Y: 1.65},
{X: 1.9, Y: 1.5},
{X: 1.9, Y: 1.9},
},
}
polygon := PPO(exterior, holes)
clipped := Clip(polygon, RO(1.3, 1.3, 1.4, 2.15), nil)
cp, ok := clipped.(*geojson.Polygon)
if !ok {
t.Fatal("wrong type")
}
if cp.Base().Exterior.Empty() {
t.Fatal("Empty result.")
}
if len(cp.Base().Holes) != 1 {
t.Fatal("result must be a two-ring Polygon")
}
}
func TestClipPolygon2(t *testing.T) {
exterior := []geometry.Point{
{X: 2, Y: 2},
{X: 1, Y: 2},
{X: 1.5, Y: 1.5},
{X: 1, Y: 1},
{X: 2, Y: 1},
{X: 2, Y: 2},
}
holes := [][]geometry.Point{
{
{X: 1.9, Y: 1.9},
{X: 1.2, Y: 1.9},
{X: 1.45, Y: 1.65},
{X: 1.9, Y: 1.5},
{X: 1.9, Y: 1.9},
},
}
polygon := PPO(exterior, holes)
clipped := Clip(polygon, RO(1.1, 0.8, 1.15, 2.1), nil)
cp, ok := clipped.(*geojson.Polygon)
if !ok {
t.Fatal("wrong type")
}
if cp.Base().Exterior.Empty() {
t.Fatal("Empty result.")
}
if len(cp.Base().Holes) != 0 {
t.Fatal("result must be a single-ring Polygon")
}
}
// func TestClipLineString(t *testing.T) {
// featuresJSON := `
// {"type": "FeatureCollection","features": [
// {"type": "Feature","properties":{},"geometry": {"type": "LineString","coordinates": [[-71.46537780761717,42.594290856363344],[-71.37714385986328,42.600861802789524],[-71.37508392333984,42.538156868495555],[-71.43756866455078,42.535374141307415],[-71.44683837890625,42.466018925787495],[-71.334228515625,42.465005871175755],[-71.32736206054688,42.52424199254517]]}},
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.49284362792969,42.527784255084676],[-71.35791778564453,42.527784255084676],[-71.35791778564453,42.61096959812047],[-71.49284362792969,42.61096959812047],[-71.49284362792969,42.527784255084676]]]}},
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.47396087646484,42.48247876554176],[-71.30744934082031,42.48247876554176],[-71.30744934082031,42.576596402826894],[-71.47396087646484,42.576596402826894],[-71.47396087646484,42.48247876554176]]]}},
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.33491516113281,42.613496290695196],[-71.29920959472656,42.613496290695196],[-71.29920959472656,42.643556064374536],[-71.33491516113281,42.643556064374536],[-71.33491516113281,42.613496290695196]]]}},
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.37130737304686,42.530061317794775],[-71.3287353515625,42.530061317794775],[-71.3287353515625,42.60414701616359],[-71.37130737304686,42.60414701616359],[-71.37130737304686,42.530061317794775]]]}},
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.52889251708984,42.564460160624115],[-71.45713806152342,42.54043355305221],[-71.53266906738281,42.49969365675931],[-71.36547088623047,42.508552415528634],[-71.43962860107422,42.58999409368092],[-71.52889251708984,42.564460160624115]]]}},
// {"type": "Feature","properties": {},"geometry": {"type": "Point","coordinates": [-71.33079528808594,42.55940269610327]}},
// {"type": "Feature","properties": {},"geometry": {"type": "Point","coordinates": [-71.27208709716797,42.53107331902133]}}
// ]}
// `
// rectJSON := `{"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[-71.44065856933594,42.51740991900762],[-71.29131317138672,42.51740991900762],[-71.29131317138672,42.62663343969058],[-71.44065856933594,42.62663343969058],[-71.44065856933594,42.51740991900762]]]}}`
// features := expectJSON(t, featuresJSON, nil)
// rect := expectJSON(t, rectJSON, nil)
// clipped := features.Clipped(rect)
// println(clipped.String())
// }
================================================
FILE: internal/clip/collection.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipCollection(
collection geojson.Collection, clipper geojson.Object,
opts *geometry.IndexOptions,
) geojson.Object {
var features []geojson.Object
for _, feature := range collection.Children() {
feature = Clip(feature, clipper, opts)
if feature.Empty() {
continue
}
if _, ok := feature.(*geojson.Feature); !ok {
feature = geojson.NewFeature(feature, "")
}
features = append(features, feature)
}
return geojson.NewFeatureCollection(features)
}
================================================
FILE: internal/clip/feature.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipFeature(
feature *geojson.Feature, clipper geojson.Object,
opts *geometry.IndexOptions,
) geojson.Object {
newFeature := Clip(feature.Base(), clipper, opts)
if _, ok := newFeature.(*geojson.Feature); !ok {
newFeature = geojson.NewFeature(newFeature, feature.Members())
}
return newFeature
}
================================================
FILE: internal/clip/linestring.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipLineString(
lineString *geojson.LineString, clipper geojson.Object,
opts *geometry.IndexOptions,
) geojson.Object {
bbox := clipper.Rect()
var newPoints [][]geometry.Point
var clipped geometry.Segment
var rejected bool
var line []geometry.Point
base := lineString.Base()
nSegments := base.NumSegments()
for i := 0; i < nSegments; i++ {
clipped, rejected = clipSegment(base.SegmentAt(i), bbox)
if rejected {
continue
}
if len(line) > 0 && line[len(line)-1] != clipped.A {
newPoints = append(newPoints, line)
line = []geometry.Point{clipped.A}
} else if len(line) == 0 {
line = append(line, clipped.A)
}
line = append(line, clipped.B)
}
if len(line) > 0 {
newPoints = append(newPoints, line)
}
var children []*geometry.Line
for _, points := range newPoints {
children = append(children,
geometry.NewLine(points, opts))
}
if len(children) == 1 {
return geojson.NewLineString(children[0])
}
return geojson.NewMultiLineString(children)
}
================================================
FILE: internal/clip/point.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipPoint(
point *geojson.Point, clipper geojson.Object, opts *geometry.IndexOptions,
) geojson.Object {
if point.IntersectsRect(clipper.Rect()) {
return point
}
return geojson.NewMultiPoint(nil)
}
================================================
FILE: internal/clip/polygon.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipPolygon(
polygon *geojson.Polygon, clipper geojson.Object,
opts *geometry.IndexOptions,
) geojson.Object {
rect := clipper.Rect()
var newPoints [][]geometry.Point
base := polygon.Base()
rings := []geometry.Ring{base.Exterior}
rings = append(rings, base.Holes...)
for _, ring := range rings {
ringPoints := make([]geometry.Point, ring.NumPoints())
for i := 0; i < len(ringPoints); i++ {
ringPoints[i] = ring.PointAt(i)
}
if clippedRing := clipRing(ringPoints, rect); len(clippedRing) > 0 {
newPoints = append(newPoints, clippedRing)
}
}
var exterior []geometry.Point
var holes [][]geometry.Point
if len(newPoints) > 0 {
exterior = newPoints[0]
}
if len(newPoints) > 1 {
holes = newPoints[1:]
}
newPoly := geojson.NewPolygon(
geometry.NewPoly(exterior, holes, opts),
)
if newPoly.Empty() {
return geojson.NewMultiPolygon(nil)
}
return newPoly
}
================================================
FILE: internal/clip/rect.go
================================================
package clip
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
func clipRect(
rect *geojson.Rect, clipper geojson.Object, opts *geometry.IndexOptions,
) geojson.Object {
base := rect.Base()
points := make([]geometry.Point, base.NumPoints())
for i := 0; i < len(points); i++ {
points[i] = base.PointAt(i)
}
poly := geometry.NewPoly(points, nil, opts)
gPoly := geojson.NewPolygon(poly)
return Clip(gPoly, clipper, opts)
}
================================================
FILE: internal/collection/collection.go
================================================
package collection
import (
"math"
"runtime"
"github.com/tidwall/btree"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/rtree"
"github.com/tidwall/tile38/internal/deadline"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/object"
)
// yieldStep forces the iterator to yield goroutine every 256 steps.
const yieldStep = 256
// Cursor allows for quickly paging through Scan, Within, Intersects, and Nearby
type Cursor interface {
Offset() uint64
Step(count uint64)
}
func byID(a, b *object.Object) bool {
return a.ID() < b.ID()
}
func byValue(a, b *object.Object) bool {
value1 := a.String()
value2 := b.String()
if value1 < value2 {
return true
}
if value1 > value2 {
return false
}
// the values match so we'll compare IDs, which are always unique.
return byID(a, b)
}
func byExpires(a, b *object.Object) bool {
if a.Expires() < b.Expires() {
return true
}
if a.Expires() > b.Expires() {
return false
}
// the values match so we'll compare IDs, which are always unique.
return byID(a, b)
}
// Collection represents a collection of geojson objects.
type Collection struct {
objs btree.Map[string, *object.Object] // sorted by id
spatial rtree.RTreeGN[float32, *object.Object] // geospatially indexed
values *btree.BTreeG[*object.Object] // sorted by value+id
expires *btree.BTreeG[*object.Object] // sorted by ex+id
weight int
points int
objects int // geometry count
nobjects int // non-geometry count
}
var optsNoLock = btree.Options{NoLocks: true}
// New creates an empty collection
func New() *Collection {
col := &Collection{
values: btree.NewBTreeGOptions(byValue, optsNoLock),
expires: btree.NewBTreeGOptions(byExpires, optsNoLock),
}
return col
}
// Count returns the number of objects in collection.
func (c *Collection) Count() int {
return c.objects + c.nobjects
}
// StringCount returns the number of string values.
func (c *Collection) StringCount() int {
return c.nobjects
}
// PointCount returns the number of points (lat/lon coordinates) in collection.
func (c *Collection) PointCount() int {
return c.points
}
// TotalWeight calculates the in-memory cost of the collection in bytes.
func (c *Collection) TotalWeight() int {
return c.weight
}
// Bounds returns the bounds of all the items in the collection.
func (c *Collection) Bounds() (minX, minY, maxX, maxY float64) {
_, _, left := c.spatial.LeftMost()
_, _, bottom := c.spatial.BottomMost()
_, _, right := c.spatial.RightMost()
_, _, top := c.spatial.TopMost()
if left == nil {
return
}
return left.Rect().Min.X, bottom.Rect().Min.Y,
right.Rect().Max.X, top.Rect().Max.Y
}
func (c *Collection) indexDelete(item *object.Object) {
if !item.Geo().Empty() {
c.spatial.Delete(rtreeItem(item))
}
}
func (c *Collection) indexInsert(item *object.Object) {
if !item.Geo().Empty() {
c.spatial.Insert(rtreeItem(item))
}
}
const dRNDTOWARDS = (1.0 - 1.0/8388608.0) /* Round towards zero */
const dRNDAWAY = (1.0 + 1.0/8388608.0) /* Round away from zero */
func rtreeValueDown(d float64) float32 {
f := float32(d)
if float64(f) > d {
if d < 0 {
f = float32(d * dRNDAWAY)
} else {
f = float32(d * dRNDTOWARDS)
}
}
return f
}
func rtreeValueUp(d float64) float32 {
f := float32(d)
if float64(f) < d {
if d < 0 {
f = float32(d * dRNDTOWARDS)
} else {
f = float32(d * dRNDAWAY)
}
}
return f
}
func rtreeItem(item *object.Object) (min, max [2]float32, data *object.Object) {
min, max = rtreeRect(item.Rect())
return min, max, item
}
func rtreeRect(rect geometry.Rect) (min, max [2]float32) {
return [2]float32{
rtreeValueDown(rect.Min.X),
rtreeValueDown(rect.Min.Y),
}, [2]float32{
rtreeValueUp(rect.Max.X),
rtreeValueUp(rect.Max.Y),
}
}
// Set adds or replaces an object in the collection and returns the fields
// array.
func (c *Collection) Set(obj *object.Object) (prev *object.Object) {
prev, _ = c.objs.Set(obj.ID(), obj)
c.setFill(prev, obj)
return prev
}
func (c *Collection) setFill(prev, obj *object.Object) {
if prev != nil {
if prev.IsSpatial() {
c.indexDelete(prev)
c.objects--
} else {
c.values.Delete(prev)
c.nobjects--
}
if prev.Expires() != 0 {
c.expires.Delete(prev)
}
c.points -= prev.Geo().NumPoints()
c.weight -= prev.Weight()
}
if obj.IsSpatial() {
c.indexInsert(obj)
c.objects++
} else {
c.values.Set(obj)
c.nobjects++
}
if obj.Expires() != 0 {
c.expires.Set(obj)
}
c.points += obj.Geo().NumPoints()
c.weight += obj.Weight()
}
// Delete removes an object and returns it.
// If the object does not exist then the 'ok' return value will be false.
func (c *Collection) Delete(id string) (prev *object.Object) {
prev, _ = c.objs.Delete(id)
if prev == nil {
return nil
}
if prev.IsSpatial() {
if !prev.Geo().Empty() {
c.indexDelete(prev)
}
c.objects--
} else {
c.values.Delete(prev)
c.nobjects--
}
if prev.Expires() != 0 {
c.expires.Delete(prev)
}
c.points -= prev.Geo().NumPoints()
c.weight -= prev.Weight()
return prev
}
// Get returns an object.
// If the object does not exist then the 'ok' return value will be false.
func (c *Collection) Get(id string) *object.Object {
obj, _ := c.objs.Get(id)
return obj
}
// Scan iterates though the collection ids.
func (c *Collection) Scan(
desc bool,
cursor Cursor,
deadline *deadline.Deadline,
iterator func(obj *object.Object) bool,
) bool {
var keepon = true
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
iter := func(_ string, obj *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
keepon = iterator(obj)
return keepon
}
if desc {
c.objs.Reverse(iter)
} else {
c.objs.Scan(iter)
}
return keepon
}
// ScanRange iterates though the collection starting with specified id.
func (c *Collection) ScanRange(
start, end string,
desc bool,
cursor Cursor,
deadline *deadline.Deadline,
iterator func(o *object.Object) bool,
) bool {
var keepon = true
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
iter := func(_ string, o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
if !desc {
if o.ID() >= end {
return false
}
} else {
if o.ID() <= end {
return false
}
}
keepon = iterator(o)
return keepon
}
if desc {
c.objs.Descend(start, iter)
} else {
c.objs.Ascend(start, iter)
}
return keepon
}
// SearchValues iterates though the collection values.
func (c *Collection) SearchValues(
desc bool,
cursor Cursor,
deadline *deadline.Deadline,
iterator func(o *object.Object) bool,
) bool {
var keepon = true
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
iter := func(o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
keepon = iterator(o)
return keepon
}
if desc {
c.values.Reverse(iter)
} else {
c.values.Scan(iter)
}
return keepon
}
// SearchValuesRange iterates though the collection values.
func (c *Collection) SearchValuesRange(start, end string, desc bool,
cursor Cursor,
deadline *deadline.Deadline,
iterator func(o *object.Object) bool,
) bool {
var keepon = true
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
iter := func(o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
keepon = iterator(o)
return keepon
}
pstart := object.New("", String(start), 0, field.List{})
pend := object.New("", String(end), 0, field.List{})
if desc {
// descend range
c.values.Descend(pstart, func(item *object.Object) bool {
return bGT(c.values, item, pend) && iter(item)
})
} else {
c.values.Ascend(pstart, func(item *object.Object) bool {
return bLT(c.values, item, pend) && iter(item)
})
}
return keepon
}
func bLT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(a, b) }
func bGT(tr *btree.BTreeG[*object.Object], a, b *object.Object) bool { return tr.Less(b, a) }
// ScanGreaterOrEqual iterates though the collection starting with specified id.
func (c *Collection) ScanGreaterOrEqual(id string, desc bool,
cursor Cursor,
deadline *deadline.Deadline,
iterator func(o *object.Object) bool,
) bool {
var keepon = true
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
iter := func(_ string, o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
keepon = iterator(o)
return keepon
}
if desc {
c.objs.Descend(id, iter)
} else {
c.objs.Ascend(id, iter)
}
return keepon
}
func (c *Collection) geoSearch(
rect geometry.Rect,
iter func(o *object.Object) bool,
) bool {
alive := true
min, max := rtreeRect(rect)
// avoid search if NaN present as it results in full search
// https://github.com/tidwall/tile38/issues/793
if math.IsNaN(float64(min[0])) && math.IsNaN(float64(min[1])) &&
math.IsNaN(float64(max[0])) && math.IsNaN(float64(max[1])) {
return alive
}
c.spatial.Search(
min, max,
func(_, _ [2]float32, o *object.Object) bool {
alive = iter(o)
return alive
},
)
return alive
}
func (c *Collection) geoSparse(
obj geojson.Object, sparse uint8,
iter func(o *object.Object) (match, ok bool),
) bool {
matches := make(map[string]bool)
alive := true
c.geoSparseInner(obj.Rect(), sparse, func(o *object.Object) (match, ok bool) {
ok = true
if !matches[o.ID()] {
match, ok = iter(o)
if match {
matches[o.ID()] = true
}
}
return match, ok
})
return alive
}
func (c *Collection) geoSparseInner(
rect geometry.Rect, sparse uint8,
iter func(o *object.Object) (match, ok bool),
) bool {
if sparse > 0 {
w := rect.Max.X - rect.Min.X
h := rect.Max.Y - rect.Min.Y
quads := [4]geometry.Rect{
{
Min: geometry.Point{X: rect.Min.X, Y: rect.Min.Y + h/2},
Max: geometry.Point{X: rect.Min.X + w/2, Y: rect.Max.Y},
},
{
Min: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y + h/2},
Max: geometry.Point{X: rect.Max.X, Y: rect.Max.Y},
},
{
Min: geometry.Point{X: rect.Min.X, Y: rect.Min.Y},
Max: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y + h/2},
},
{
Min: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y},
Max: geometry.Point{X: rect.Max.X, Y: rect.Min.Y + h/2},
},
}
for _, quad := range quads {
if !c.geoSparseInner(quad, sparse-1, iter) {
return false
}
}
return true
}
alive := true
c.geoSearch(rect, func(o *object.Object) bool {
match, ok := iter(o)
if !ok {
alive = false
return false
}
return !match
})
return alive
}
// Within returns all object that are fully contained within an object or
// bounding box. Set obj to nil in order to use the bounding box.
func (c *Collection) Within(
obj geojson.Object,
sparse uint8,
cursor Cursor,
deadline *deadline.Deadline,
iter func(o *object.Object) bool,
) bool {
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
if sparse > 0 {
return c.geoSparse(obj, sparse, func(o *object.Object) (match, ok bool) {
count++
if count <= offset {
return false, true
}
nextStep(count, cursor, deadline)
if match = o.Geo().Within(obj); match {
ok = iter(o)
}
return match, ok
})
}
return c.geoSearch(obj.Rect(), func(o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
if o.Geo().Within(obj) {
return iter(o)
}
return true
})
}
// Intersects returns all object that are intersect an object or bounding box.
// Set obj to nil in order to use the bounding box.
func (c *Collection) Intersects(
gobj geojson.Object,
sparse uint8,
cursor Cursor,
deadline *deadline.Deadline,
iter func(o *object.Object) bool,
) bool {
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
if sparse > 0 {
return c.geoSparse(gobj, sparse, func(o *object.Object) (match, ok bool) {
count++
if count <= offset {
return false, true
}
nextStep(count, cursor, deadline)
if match = o.Geo().Intersects(gobj); match {
ok = iter(o)
}
return match, ok
})
}
return c.geoSearch(gobj.Rect(), func(o *object.Object) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
if o.Geo().Intersects(gobj) {
return iter(o)
}
return true
},
)
}
// Nearby returns the nearest neighbors
func (c *Collection) Nearby(
target geojson.Object,
cursor Cursor,
deadline *deadline.Deadline,
iter func(o *object.Object, dist float64) bool,
) bool {
alive := true
center := target.Center()
var count uint64
var offset uint64
if cursor != nil {
offset = cursor.Offset()
cursor.Step(offset)
}
distFn := geodeticDistAlgo([2]float64{center.X, center.Y})
c.spatial.Nearby(
func(min, max [2]float32, data *object.Object, item bool) float64 {
return distFn(
[2]float64{float64(min[0]), float64(min[1])},
[2]float64{float64(max[0]), float64(max[1])},
data, item,
)
},
func(_, _ [2]float32, o *object.Object, dist float64) bool {
count++
if count <= offset {
return true
}
nextStep(count, cursor, deadline)
alive = iter(o, dist)
return alive
},
)
return alive
}
func nextStep(step uint64, cursor Cursor, deadline *deadline.Deadline) {
if step&(yieldStep-1) == (yieldStep - 1) {
runtime.Gosched()
deadline.Check()
}
if cursor != nil {
cursor.Step(1)
}
}
// ScanExpires returns a list of all objects that have expired.
func (c *Collection) ScanExpires(iter func(o *object.Object) bool) {
c.expires.Scan(iter)
}
================================================
FILE: internal/collection/collection_test.go
================================================
package collection
import (
"fmt"
"math/rand"
"reflect"
"strconv"
"testing"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/object"
)
func PO(x, y float64) *geojson.Point {
return geojson.NewPoint(geometry.Point{X: x, Y: y})
}
func init() {
seed := time.Now().UnixNano()
println(seed)
rand.Seed(seed)
}
func expect(t testing.TB, expect bool) {
t.Helper()
if !expect {
t.Fatal("not what you expected")
}
}
func bounds(c *Collection) geometry.Rect {
minX, minY, maxX, maxY := c.Bounds()
return geometry.Rect{
Min: geometry.Point{X: minX, Y: minY},
Max: geometry.Point{X: maxX, Y: maxY},
}
}
func TestCollectionNewCollection(t *testing.T) {
const numItems = 10000
objs := make(map[string]geojson.Object)
c := New()
for i := 0; i < numItems; i++ {
id := strconv.FormatInt(int64(i), 10)
obj := PO(rand.Float64()*360-180, rand.Float64()*180-90)
objs[id] = obj
c.Set(object.New(id, obj, 0, field.List{}))
}
count := 0
bbox := geometry.Rect{
Min: geometry.Point{X: -180, Y: -90},
Max: geometry.Point{X: 180, Y: 90},
}
c.geoSearch(bbox, func(o *object.Object) bool {
count++
return true
})
if count != len(objs) {
t.Fatalf("count = %d, expect %d", count, len(objs))
}
count = c.Count()
if count != len(objs) {
t.Fatalf("c.Count() = %d, expect %d", count, len(objs))
}
testCollectionVerifyContents(t, c, objs)
}
func toFields(fNames, fValues []string) field.List {
var fields field.List
for i := 0; i < len(fNames); i++ {
fields = fields.Set(field.Make(fNames[i], fValues[i]))
}
return fields
}
func TestCollectionSet(t *testing.T) {
t.Run("AddString", func(t *testing.T) {
c := New()
str1 := String("hello")
old := c.Set(object.New("str", str1, 0, field.List{}))
expect(t, old == nil)
})
t.Run("UpdateString", func(t *testing.T) {
c := New()
str1 := String("hello")
str2 := String("world")
old := c.Set(object.New("str", str1, 0, field.List{}))
expect(t, old == nil)
old = c.Set(object.New("str", str2, 0, field.List{}))
expect(t, old.Geo() == str1)
})
t.Run("AddPoint", func(t *testing.T) {
c := New()
point1 := PO(-112.1, 33.1)
old := c.Set(object.New("point", point1, 0, field.List{}))
expect(t, old == nil)
})
t.Run("UpdatePoint", func(t *testing.T) {
c := New()
point1 := PO(-112.1, 33.1)
point2 := PO(-112.2, 33.2)
old := c.Set(object.New("point", point1, 0, field.List{}))
expect(t, old == nil)
old = c.Set(object.New("point", point2, 0, field.List{}))
expect(t, old.Geo().Center() == point1.Base())
})
t.Run("Fields", func(t *testing.T) {
c := New()
str1 := String("hello")
fNames := []string{"a", "b", "c"}
fValues := []string{"1", "2", "3"}
fields1 := toFields(fNames, fValues)
old := c.Set(object.New("str", str1, 0, fields1))
expect(t, old == nil)
str2 := String("hello")
fNames = []string{"d", "e", "f"}
fValues = []string{"4", "5", "6"}
fields2 := toFields(fNames, fValues)
old = c.Set(object.New("str", str2, 0, fields2))
expect(t, old.Geo() == str1)
expect(t, reflect.DeepEqual(old.Fields(), fields1))
fNames = []string{"a", "b", "c", "d", "e", "f"}
fValues = []string{"7", "8", "9", "10", "11", "12"}
fields3 := toFields(fNames, fValues)
old = c.Set(object.New("str", str1, 0, fields3))
expect(t, old.Geo() == str2)
expect(t, reflect.DeepEqual(old.Fields(), fields2))
})
t.Run("Delete", func(t *testing.T) {
c := New()
c.Set(object.New("1", String("1"), 0, field.List{}))
c.Set(object.New("2", String("2"), 0, field.List{}))
c.Set(object.New("3", PO(1, 2), 0, field.List{}))
expect(t, c.Count() == 3)
expect(t, c.StringCount() == 2)
expect(t, c.PointCount() == 1)
expect(t, bounds(c) == geometry.Rect{
Min: geometry.Point{X: 1, Y: 2},
Max: geometry.Point{X: 1, Y: 2}})
var prev *object.Object
prev = c.Delete("2")
expect(t, prev.Geo().String() == "2")
expect(t, c.Count() == 2)
expect(t, c.StringCount() == 1)
expect(t, c.PointCount() == 1)
prev = c.Delete("1")
expect(t, prev.Geo().String() == "1")
expect(t, c.Count() == 1)
expect(t, c.StringCount() == 0)
expect(t, c.PointCount() == 1)
prev = c.Delete("3")
expect(t, prev.Geo().String() == `{"type":"Point","coordinates":[1,2]}`)
expect(t, c.Count() == 0)
expect(t, c.StringCount() == 0)
expect(t, c.PointCount() == 0)
prev = c.Delete("3")
expect(t, prev == nil)
expect(t, c.Count() == 0)
expect(t, bounds(c) == geometry.Rect{})
expect(t, c.Get("3") == nil)
})
}
func fieldValueAt(fields field.List, index int) string {
if index < 0 || index >= fields.Len() {
panic("out of bounds")
}
var retval string
var i int
fields.Scan(func(f field.Field) bool {
if i == index {
retval = f.Value().Data()
}
i++
return true
})
return retval
}
func TestCollectionScan(t *testing.T) {
N := 256
c := New()
for _, i := range rand.Perm(N) {
id := fmt.Sprintf("%04d", i)
c.Set(object.New(id, String(id), 0, makeFields(
field.Make("ex", id),
)))
}
var n int
var prevID string
c.Scan(false, nil, nil, func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() > prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == c.Count())
n = 0
c.Scan(true, nil, nil, func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() < prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == c.Count())
n = 0
c.ScanRange("0060", "0070", false, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() > prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == 10)
n = 0
c.ScanRange("0070", "0060", true, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() < prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == 10)
n = 0
c.ScanGreaterOrEqual("0070", true, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() < prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == 71)
n = 0
c.ScanGreaterOrEqual("0070", false, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.ID() > prevID)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 0))
n++
prevID = o.ID()
return true
})
expect(t, n == c.Count()-70)
}
func makeFields(entries ...field.Field) field.List {
var fields field.List
for _, f := range entries {
fields = fields.Set(f)
}
return fields
}
func TestCollectionSearch(t *testing.T) {
N := 256
c := New()
for i, j := range rand.Perm(N) {
id := fmt.Sprintf("%04d", j)
ex := fmt.Sprintf("%04d", i)
c.Set(object.New(id, String(ex),
0, makeFields(
field.Make("i", ex),
field.Make("j", id),
)))
}
var n int
var prevValue string
c.SearchValues(false, nil, nil, func(o *object.Object) bool {
if n > 0 {
expect(t, o.Geo().String() > prevValue)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
n++
prevValue = o.Geo().String()
return true
})
expect(t, n == c.Count())
n = 0
c.SearchValues(true, nil, nil, func(o *object.Object) bool {
if n > 0 {
expect(t, o.Geo().String() < prevValue)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
n++
prevValue = o.Geo().String()
return true
})
expect(t, n == c.Count())
n = 0
c.SearchValuesRange("0060", "0070", false, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.Geo().String() > prevValue)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
n++
prevValue = o.Geo().String()
return true
})
expect(t, n == 10)
n = 0
c.SearchValuesRange("0070", "0060", true, nil, nil,
func(o *object.Object) bool {
if n > 0 {
expect(t, o.Geo().String() < prevValue)
}
expect(t, o.ID() == fieldValueAt(o.Fields(), 1))
n++
prevValue = o.Geo().String()
return true
})
expect(t, n == 10)
}
func TestCollectionWeight(t *testing.T) {
c := New()
c.Set(object.New("1", String("1"), 0, field.List{}))
expect(t, c.TotalWeight() > 0)
c.Delete("1")
expect(t, c.TotalWeight() == 0)
c.Set(object.New("1", String("1"), 0,
toFields(
[]string{"a", "b", "c"},
[]string{"1", "2", "3"},
),
))
expect(t, c.TotalWeight() > 0)
c.Delete("1")
expect(t, c.TotalWeight() == 0)
c.Set(object.New("1", String("1"), 0,
toFields(
[]string{"a", "b", "c"},
[]string{"1", "2", "3"},
),
))
c.Set(object.New("2", String("2"), 0,
toFields(
[]string{"d", "e", "f"},
[]string{"4", "5", "6"},
),
))
c.Set(object.New("1", String("1"), 0,
toFields(
[]string{"d", "e", "f"},
[]string{"4", "5", "6"},
),
))
c.Delete("1")
c.Delete("2")
expect(t, c.TotalWeight() == 0)
}
func TestSpatialSearch(t *testing.T) {
json := `
{"type":"FeatureCollection","features":[
{"type":"Feature","id":"p1","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4743041992187,42.51867517417283]}},
{"type":"Feature","id":"p2","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4056396484375,42.50197174319114]}},
{"type":"Feature","id":"p3","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4619445800781,42.49437779897246]}},
{"type":"Feature","id":"p4","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4337921142578,42.53891577257117]}},
{"type":"Feature","id":"r1","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates":[[[-71.4279556274414,42.48804880765346],[-71.37439727783203,42.48804880765346],[-71.37439727783203,42.52322988064187],[-71.4279556274414,42.52322988064187],[-71.4279556274414,42.48804880765346]]]}},
{"type":"Feature","id":"r2","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates":[[[-71.4825439453125,42.53588010092859],[-71.45027160644531,42.53588010092859],[-71.45027160644531,42.55839115400447],[-71.4825439453125,42.55839115400447],[-71.4825439453125,42.53588010092859]]]}},
{"type":"Feature","id":"r3","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates": [[[-71.4111328125,42.53512115995963],[-71.3833236694336,42.53512115995963],[-71.3833236694336,42.54953946116446],[-71.4111328125,42.54953946116446],[-71.4111328125,42.53512115995963]]]}},
{"type":"Feature","id":"q1","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.55258178710938,42.51361399979923],[-71.42074584960938,42.51361399979923],[-71.42074584960938,42.59100512331456],[-71.55258178710938,42.59100512331456],[-71.55258178710938,42.51361399979923]]]}},
{"type":"Feature","id":"q2","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.52992248535156,42.48121277771616],[-71.36375427246092,42.48121277771616],[-71.36375427246092,42.57786045892046],[-71.52992248535156,42.57786045892046],[-71.52992248535156,42.48121277771616]]]}},
{"type":"Feature","id":"q3","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.49490356445312,42.56673588590953],[-71.52236938476562,42.47462922809497],[-71.42898559570312,42.464499337722344],[-71.43241882324219,42.522217752342236],[-71.37954711914061,42.56420729713456],[-71.49490356445312,42.56673588590953]]]}},
{"type":"Feature","id":"q4","properties":{},"geometry":{"type":"Point","coordinates": [-71.46366119384766,42.54043355305221]}}
]}
`
p1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p1"]`).Raw, nil)
p2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p2"]`).Raw, nil)
p3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p3"]`).Raw, nil)
p4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p4"]`).Raw, nil)
r1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r1"]`).Raw, nil)
r2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r2"]`).Raw, nil)
r3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r3"]`).Raw, nil)
q1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q1"]`).Raw, nil)
q2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q2"]`).Raw, nil)
q3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q3"]`).Raw, nil)
q4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q4"]`).Raw, nil)
c := New()
c.Set(object.New("p1", p1, 0, field.List{}))
c.Set(object.New("p2", p2, 0, field.List{}))
c.Set(object.New("p3", p3, 0, field.List{}))
c.Set(object.New("p4", p4, 0, field.List{}))
c.Set(object.New("r1", r1, 0, field.List{}))
c.Set(object.New("r2", r2, 0, field.List{}))
c.Set(object.New("r3", r3, 0, field.List{}))
var n int
n = 0
c.Within(q1, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 3)
n = 0
c.Within(q2, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 7)
n = 0
c.Within(q3, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 4)
n = 0
c.Intersects(q1, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 4)
n = 0
c.Intersects(q2, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 7)
n = 0
c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 5)
n = 0
c.Intersects(q3, 0, nil, nil, func(o *object.Object) bool {
n++
return n <= 1
})
expect(t, n == 2)
var items []geojson.Object
exitems := []geojson.Object{
r2, p4, p1, r1, r3, p3, p2,
}
lastDist := float64(-1)
distsMonotonic := true
c.Nearby(q4, nil, nil, func(o *object.Object, dist float64) bool {
if dist < lastDist {
distsMonotonic = false
}
items = append(items, o.Geo())
return true
})
expect(t, len(items) == 7)
expect(t, distsMonotonic)
expect(t, reflect.DeepEqual(items, exitems))
}
func TestCollectionSparse(t *testing.T) {
rect := geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: -71.598930, Y: 42.4586739},
Max: geometry.Point{X: -71.37302, Y: 42.607937},
})
N := 10000
c := New()
r := rect.Rect()
for i := 0; i < N; i++ {
x := (r.Max.X-r.Min.X)*rand.Float64() + r.Min.X
y := (r.Max.Y-r.Min.Y)*rand.Float64() + r.Min.Y
point := PO(x, y)
c.Set(object.New(fmt.Sprintf("%d", i), point, 0, field.List{}))
}
var n int
n = 0
c.Within(rect, 1, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 4)
n = 0
c.Within(rect, 2, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 16)
n = 0
c.Within(rect, 3, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 64)
n = 0
c.Within(rect, 3, nil, nil, func(o *object.Object) bool {
n++
return n <= 30
})
expect(t, n == 31)
n = 0
c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool {
n++
return true
})
expect(t, n == 64)
n = 0
c.Intersects(rect, 3, nil, nil, func(o *object.Object) bool {
n++
return n <= 30
})
expect(t, n == 31)
}
func testCollectionVerifyContents(t *testing.T, c *Collection, objs map[string]geojson.Object) {
for id, o2 := range objs {
o := c.Get(id)
if o == nil {
t.Fatalf("ok[%s] = false, expect true", id)
}
j1 := string(o.Geo().AppendJSON(nil))
j2 := string(o2.AppendJSON(nil))
if j1 != j2 {
t.Fatalf("j1 == %s, expect %s", j1, j2)
}
}
}
func TestManyCollections(t *testing.T) {
colsM := make(map[string]*Collection)
cols := 100
objs := 1000
k := 0
for i := 0; i < cols; i++ {
key := strconv.FormatInt(int64(i), 10)
for j := 0; j < objs; j++ {
id := strconv.FormatInt(int64(j), 10)
p := geometry.Point{
X: rand.Float64()*360 - 180,
Y: rand.Float64()*180 - 90,
}
obj := geojson.Object(PO(p.X, p.Y))
col, ok := colsM[key]
if !ok {
col = New()
colsM[key] = col
}
col.Set(object.New(id, obj, 0, field.List{}))
k++
}
}
col := colsM["13"]
//println(col.Count())
bbox := geometry.Rect{
Min: geometry.Point{X: -180, Y: 30},
Max: geometry.Point{X: 34, Y: 100},
}
col.geoSearch(bbox, func(o *object.Object) bool {
//println(id)
return true
})
}
type testPointItem struct {
id string
object geojson.Object
fields field.List
}
func makeBenchFields(nFields int) field.List {
var fields field.List
for i := 0; i < nFields; i++ {
key := fmt.Sprintf("%d", i)
val := key
fields = fields.Set(field.Make(key, val))
}
return fields
}
func BenchmarkInsert_Fields(t *testing.B) {
benchmarkInsert(t, 1)
}
func BenchmarkInsert_NoFields(t *testing.B) {
benchmarkInsert(t, 0)
}
func benchmarkInsert(t *testing.B, nFields int) {
rand.Seed(time.Now().UnixNano())
items := make([]testPointItem, t.N)
for i := 0; i < t.N; i++ {
items[i] = testPointItem{
fmt.Sprintf("%d", i),
PO(rand.Float64()*360-180, rand.Float64()*180-90),
makeBenchFields(nFields),
}
}
col := New()
t.ResetTimer()
for i := 0; i < t.N; i++ {
col.Set(object.New(items[i].id, items[i].object, 0, items[i].fields))
}
}
func BenchmarkReplace_Fields(t *testing.B) {
benchmarkReplace(t, 1)
}
func BenchmarkReplace_NoFields(t *testing.B) {
benchmarkReplace(t, 0)
}
func benchmarkReplace(t *testing.B, nFields int) {
rand.Seed(time.Now().UnixNano())
items := make([]testPointItem, t.N)
for i := 0; i < t.N; i++ {
items[i] = testPointItem{
fmt.Sprintf("%d", i),
PO(rand.Float64()*360-180, rand.Float64()*180-90),
makeBenchFields(nFields),
}
}
col := New()
for i := 0; i < t.N; i++ {
col.Set(object.New(items[i].id, items[i].object, 0, items[i].fields))
}
t.ResetTimer()
for _, i := range rand.Perm(t.N) {
o := col.Set(object.New(items[i].id, items[i].object, 0, field.List{}))
if o.Geo() != items[i].object {
t.Fatal("shoot!")
}
}
}
func BenchmarkGet_Fields(t *testing.B) {
benchmarkGet(t, 1)
}
func BenchmarkGet_NoFields(t *testing.B) {
benchmarkGet(t, 0)
}
func benchmarkGet(t *testing.B, nFields int) {
rand.Seed(time.Now().UnixNano())
items := make([]testPointItem, t.N)
for i := 0; i < t.N; i++ {
items[i] = testPointItem{
fmt.Sprintf("%d", i),
PO(rand.Float64()*360-180, rand.Float64()*180-90),
makeBenchFields(nFields),
}
}
col := New()
for i := 0; i < t.N; i++ {
col.Set(object.New(items[i].id, items[i].object, 0, items[i].fields))
}
t.ResetTimer()
for _, i := range rand.Perm(t.N) {
o := col.Get(items[i].id)
if o.Geo() != items[i].object {
t.Fatal("shoot!")
}
}
}
func BenchmarkRemove_Fields(t *testing.B) {
benchmarkRemove(t, 1)
}
func BenchmarkRemove_NoFields(t *testing.B) {
benchmarkRemove(t, 0)
}
func benchmarkRemove(t *testing.B, nFields int) {
rand.Seed(time.Now().UnixNano())
items := make([]testPointItem, t.N)
for i := 0; i < t.N; i++ {
items[i] = testPointItem{
fmt.Sprintf("%d", i),
PO(rand.Float64()*360-180, rand.Float64()*180-90),
makeBenchFields(nFields),
}
}
col := New()
for i := 0; i < t.N; i++ {
col.Set(object.New(items[i].id, items[i].object, 0, items[i].fields))
}
t.ResetTimer()
for _, i := range rand.Perm(t.N) {
prev := col.Delete(items[i].id)
if prev.Geo() != items[i].object {
t.Fatal("shoot!")
}
}
}
func BenchmarkScan_Fields(t *testing.B) {
benchmarkScan(t, 1)
}
func BenchmarkScan_NoFields(t *testing.B) {
benchmarkScan(t, 0)
}
func benchmarkScan(t *testing.B, nFields int) {
rand.Seed(time.Now().UnixNano())
items := make([]testPointItem, t.N)
for i := 0; i < t.N; i++ {
items[i] = testPointItem{
fmt.Sprintf("%d", i),
PO(rand.Float64()*360-180, rand.Float64()*180-90),
makeBenchFields(nFields),
}
}
col := New()
for i := 0; i < t.N; i++ {
col.Set(object.New(items[i].id, items[i].object, 0, items[i].fields))
}
t.ResetTimer()
for i := 0; i < t.N; i++ {
var scanIteration int
col.Scan(true, nil, nil, func(o *object.Object) bool {
scanIteration++
return scanIteration <= 500
})
}
}
================================================
FILE: internal/collection/geodesic.go
================================================
package collection
import (
"math"
"github.com/tidwall/tile38/internal/object"
)
func geodeticDistAlgo(center [2]float64) (
algo func(min, max [2]float64, obj *object.Object, item bool) (dist float64),
) {
const earthRadius = 6371e3
return func(min, max [2]float64, obj *object.Object, item bool) (dist float64) {
if item {
r := obj.Rect()
min[0] = r.Min.X
min[1] = r.Min.Y
max[0] = r.Max.X
max[1] = r.Max.Y
}
return earthRadius * pointRectDistGeodeticDeg(
center[1], center[0],
min[1], min[0],
max[1], max[0],
)
}
}
func pointRectDistGeodeticDeg(pLat, pLng, minLat, minLng, maxLat, maxLng float64) float64 {
result := pointRectDistGeodeticRad(
pLat*math.Pi/180, pLng*math.Pi/180,
minLat*math.Pi/180, minLng*math.Pi/180,
maxLat*math.Pi/180, maxLng*math.Pi/180,
)
return result
}
func pointRectDistGeodeticRad(φq, λq, φl, λl, φh, λh float64) float64 {
// Algorithm from:
// Schubert, E., Zimek, A., & Kriegel, H.-P. (2013).
// Geodetic Distance Queries on R-Trees for Indexing Geographic Data.
// Lecture Notes in Computer Science, 146–164.
// doi:10.1007/978-3-642-40235-7_9
const (
twoΠ = 2 * math.Pi
halfΠ = math.Pi / 2
)
// distance on the unit sphere computed using Haversine formula
distRad := func(φa, λa, φb, λb float64) float64 {
if φa == φb && λa == λb {
return 0
}
Δφ := φa - φb
Δλ := λa - λb
sinΔφ := math.Sin(Δφ / 2)
sinΔλ := math.Sin(Δλ / 2)
cosφa := math.Cos(φa)
cosφb := math.Cos(φb)
return 2 * math.Asin(math.Sqrt(sinΔφ*sinΔφ+sinΔλ*sinΔλ*cosφa*cosφb))
}
// Simple case, point or invalid rect
if φl >= φh && λl >= λh {
return distRad(φl, λl, φq, λq)
}
if λl <= λq && λq <= λh {
// q is between the bounding meridians of r
// hence, q is north, south or within r
if φl <= φq && φq <= φh { // Inside
return 0
}
if φq < φl { // South
return φl - φq
}
return φq - φh // North
}
// determine if q is closer to the east or west edge of r to select edge for
// tests below
Δλe := λl - λq
Δλw := λq - λh
if Δλe < 0 {
Δλe += twoΠ
}
if Δλw < 0 {
Δλw += twoΠ
}
var Δλ float64 // distance to closest edge
var λedge float64 // longitude of closest edge
if Δλe <= Δλw {
Δλ = Δλe
λedge = λl
} else {
Δλ = Δλw
λedge = λh
}
sinΔλ, cosΔλ := math.Sincos(Δλ)
tanφq := math.Tan(φq)
if Δλ >= halfΠ {
// If Δλ > 90 degrees (1/2 pi in radians) we're in one of the corners
// (NW/SW or NE/SE depending on the edge selected). Compare against the
// center line to decide which case we fall into
φmid := (φh + φl) / 2
if tanφq >= math.Tan(φmid)*cosΔλ {
return distRad(φq, λq, φh, λedge) // North corner
}
return distRad(φq, λq, φl, λedge) // South corner
}
if tanφq >= math.Tan(φh)*cosΔλ {
return distRad(φq, λq, φh, λedge) // North corner
}
if tanφq <= math.Tan(φl)*cosΔλ {
return distRad(φq, λq, φl, λedge) // South corner
}
// We're to the East or West of the rect, compute distance using cross-track
// Note that this is a simplification of the cross track distance formula
// valid since the track in question is a meridian.
return math.Asin(math.Cos(φq) * sinΔλ)
}
================================================
FILE: internal/collection/string.go
================================================
package collection
import (
"encoding/json"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
)
type String string
var _ geojson.Object = String("")
func (s String) Spatial() geojson.Spatial {
return geojson.EmptySpatial{}
}
func (s String) ForEach(iter func(geom geojson.Object) bool) bool {
return iter(s)
}
func (s String) Empty() bool {
return true
}
func (s String) Valid() bool {
return false
}
func (s String) Rect() geometry.Rect {
return geometry.Rect{}
}
func (s String) Center() geometry.Point {
return geometry.Point{}
}
func (s String) AppendJSON(dst []byte) []byte {
data, _ := json.Marshal(string(s))
return append(dst, data...)
}
func (s String) String() string {
return string(s)
}
func (s String) JSON() string {
return string(s.AppendJSON(nil))
}
func (s String) MarshalJSON() ([]byte, error) {
return s.AppendJSON(nil), nil
}
func (s String) Within(obj geojson.Object) bool {
return false
}
func (s String) Contains(obj geojson.Object) bool {
return false
}
func (s String) Intersects(obj geojson.Object) bool {
return false
}
func (s String) NumPoints() int {
return 0
}
func (s String) Distance(obj geojson.Object) float64 {
return 0
}
func (s String) Members() string {
return ""
}
================================================
FILE: internal/deadline/deadline.go
================================================
package deadline
import "time"
// Deadline allows for commands to expire when they run too long
type Deadline struct {
unixNano int64
hit bool
}
// New returns a new deadline object
func New(dl time.Time) *Deadline {
return &Deadline{unixNano: dl.UnixNano()}
}
// Check the deadline and panic when reached
//
//go:noinline
func (dl *Deadline) Check() {
if dl == nil || dl.unixNano == 0 {
return
}
if !dl.hit && time.Now().UnixNano() > dl.unixNano {
dl.hit = true
panic("deadline")
}
}
// Hit returns true if the deadline has been hit
func (dl *Deadline) Hit() bool {
return dl.hit
}
// GetDeadlineTime returns the time object for the deadline, and an
// "empty" boolean
func (dl *Deadline) GetDeadlineTime() time.Time {
return time.Unix(0, dl.unixNano)
}
================================================
FILE: internal/endpoint/amqp.go
================================================
package endpoint
import (
"fmt"
"net"
"sync"
"time"
"github.com/streadway/amqp"
)
const amqpExpiresAfter = time.Second * 30
// AMQPConn is an endpoint connection
type AMQPConn struct {
mu sync.Mutex
ep Endpoint
conn *amqp.Connection
channel *amqp.Channel
ex bool
t time.Time
}
// Expired returns true if the connection has expired
func (conn *AMQPConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > amqpExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *AMQPConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *AMQPConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
conn.channel = nil
}
}
// Send sends a message
func (conn *AMQPConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
prefix := "amqp://"
if conn.ep.AMQP.SSL {
prefix = "amqps://"
}
var cfg amqp.Config
cfg.Dial = func(network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, time.Second)
}
c, err := amqp.DialConfig(fmt.Sprintf("%s%s", prefix, conn.ep.AMQP.URI), cfg)
if err != nil {
return err
}
channel, err := c.Channel()
if err != nil {
return err
}
// Declare new exchange
if err := channel.ExchangeDeclare(
conn.ep.AMQP.QueueName,
conn.ep.AMQP.Type,
conn.ep.AMQP.Durable,
conn.ep.AMQP.AutoDelete,
conn.ep.AMQP.Internal,
conn.ep.AMQP.NoWait,
nil,
); err != nil {
return err
}
if conn.ep.AMQP.Type != "topic" {
// Create queue if queue don't exists
if _, err := channel.QueueDeclare(
conn.ep.AMQP.QueueName,
conn.ep.AMQP.Durable,
conn.ep.AMQP.AutoDelete,
false,
conn.ep.AMQP.NoWait,
nil,
); err != nil {
return err
}
// Binding exchange to queue
if err := channel.QueueBind(
conn.ep.AMQP.QueueName,
conn.ep.AMQP.RouteKey,
conn.ep.AMQP.QueueName,
conn.ep.AMQP.NoWait,
nil,
); err != nil {
return err
}
}
conn.conn = c
conn.channel = channel
}
return conn.channel.Publish(
conn.ep.AMQP.QueueName,
conn.ep.AMQP.RouteKey,
conn.ep.AMQP.Mandatory,
conn.ep.AMQP.Immediate,
amqp.Publishing{
Headers: amqp.Table{},
ContentType: "application/json",
ContentEncoding: "",
Body: []byte(msg),
DeliveryMode: conn.ep.AMQP.DeliveryMode,
Priority: conn.ep.AMQP.Priority,
},
)
}
func newAMQPConn(ep Endpoint) *AMQPConn {
return &AMQPConn{
ep: ep,
t: time.Now(),
}
}
================================================
FILE: internal/endpoint/cfqueue.go
================================================
package endpoint
import (
"context"
"sync"
"time"
"github.com/cloudflare/cloudflare-go/v4"
"github.com/cloudflare/cloudflare-go/v4/option"
"github.com/cloudflare/cloudflare-go/v4/queues"
)
const cfqueueExpiresAfter = time.Second * 30
// CFQueueConn is an endpoint connection
type CFQueueConn struct {
mu sync.Mutex
ep Endpoint
client *cloudflare.Client
ex bool
t time.Time
}
// Expired returns true if the connection has expired
func (conn *CFQueueConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > cfqueueExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *CFQueueConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *CFQueueConn) close() {
if conn.client != nil {
conn.client = nil
}
}
// Send sends a message
func (conn *CFQueueConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
// Initialize client if not already done
if conn.client == nil {
conn.client = cloudflare.NewClient(
option.WithAPIToken(conn.ep.CFQueue.APIToken),
)
}
// Push message to CF Queue
_, err := conn.client.Queues.Messages.Push(
context.TODO(),
conn.ep.CFQueue.QueueID,
queues.MessagePushParams{
AccountID: cloudflare.String(conn.ep.CFQueue.AccountID),
Body: queues.MessagePushParamsBodyMqQueueMessageText{
Body: cloudflare.String(msg),
ContentType: cloudflare.F(queues.MessagePushParamsBodyMqQueueMessageTextContentTypeText),
},
},
)
if err != nil {
return err
}
return nil
}
func newCFQueueConn(ep Endpoint) *CFQueueConn {
return &CFQueueConn{
ep: ep,
t: time.Now(),
}
}
================================================
FILE: internal/endpoint/disque.go
================================================
package endpoint
import (
"fmt"
"sync"
"time"
"github.com/gomodule/redigo/redis"
"github.com/tidwall/tile38/internal/log"
)
const disqueExpiresAfter = time.Second * 30
// DisqueConn is an endpoint connection
type DisqueConn struct {
mu sync.Mutex
ep Endpoint
ex bool
t time.Time
conn redis.Conn
}
func newDisqueConn(ep Endpoint) *DisqueConn {
return &DisqueConn{
ep: ep,
t: time.Now(),
}
}
// Expired returns true if the connection has expired
func (conn *DisqueConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > disqueExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *DisqueConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *DisqueConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
}
}
// Send sends a message
func (conn *DisqueConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.Disque.Host, conn.ep.Disque.Port)
var err error
conn.conn, err = redis.Dial("tcp", addr)
if err != nil {
return err
}
}
var args []interface{}
args = append(args, conn.ep.Disque.QueueName, msg, 0)
if conn.ep.Disque.Options.Replicate > 0 {
args = append(args, "REPLICATE", conn.ep.Disque.Options.Replicate)
}
reply, err := redis.String(conn.conn.Do("ADDJOB", args...))
if err != nil {
conn.close()
return err
}
log.Debugf("Disque: ADDJOB '%s'", reply)
return nil
}
================================================
FILE: internal/endpoint/endpoint.go
================================================
package endpoint
import (
"errors"
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/streadway/amqp"
)
var errExpired = errors.New("expired")
// Protocol is the type of protocol that the endpoint represents.
type Protocol string
const (
// Local protocol
Local = Protocol("local")
// HTTP protocol
HTTP = Protocol("http")
// Disque protocol
Disque = Protocol("disque")
// GRPC protocol
GRPC = Protocol("grpc")
// Redis protocol
Redis = Protocol("redis")
// Kafka protocol
Kafka = Protocol("kafka")
// MQTT protocol
MQTT = Protocol("mqtt")
// AMQP protocol
AMQP = Protocol("amqp")
// SQS protocol
SQS = Protocol("sqs")
// Google Cloud Pubsub protocol
PubSub = Protocol("pubsub")
// NATS protocol
NATS = Protocol("nats")
// EventHub protocol
EventHub = Protocol("sb")
// CFQueue protocol
CFQueue = Protocol("cf-queue")
)
// Endpoint represents an endpoint.
type Endpoint struct {
Protocol Protocol
Original string
GRPC struct {
Host string
Port int
}
Disque struct {
Host string
Port int
QueueName string
Options struct {
Replicate int
}
}
Redis struct {
Host string
Port int
Channel string
}
Kafka struct {
Host string
Port int
TopicName string
Auth string
SSL bool
SASLSHA256 bool
SASLSHA512 bool
CACertFile string
CertFile string
KeyFile string
}
AMQP struct {
URI string
SSL bool
QueueName string
RouteKey string
Type string
Durable bool
AutoDelete bool
Internal bool
NoWait bool
Mandatory bool
Immediate bool
DeliveryMode uint8
Priority uint8
}
MQTT struct {
Host string
Port int
QueueName string
Qos byte
Retained bool
CACertFile string
CertFile string
KeyFile string
}
PubSub struct {
Project string
Topic string
CredPath string
}
SQS struct {
PlainURL string
QueueID string
Region string
CredPath string
CredProfile string
QueueName string
CreateQueue bool
}
NATS struct {
Host string
Port int
User string
Pass string
Topic string
Token string
TLS bool
TLSCert string
TLSKey string
Secure bool
// Jetstream indicates publishing via jetstream acknowledgements.
Jetstream bool
UserCredentialPath string
}
EventHub struct {
ConnectionString string
}
CFQueue struct {
AccountID string
QueueID string
APIToken string
}
Local struct {
Channel string
}
}
// Conn is an endpoint connection
type Conn interface {
ExpireNow()
Expired() bool
Send(val string) error
}
// Manager manages all endpoints
type Manager struct {
mu sync.RWMutex
conns map[string]Conn
publisher LocalPublisher
shutdown atomic.Bool // atomic bool
wg sync.WaitGroup // run wait group
}
// NewManager returns a new manager
func NewManager(publisher LocalPublisher) *Manager {
epc := &Manager{
conns: make(map[string]Conn),
publisher: publisher,
}
epc.wg.Add(1)
go epc.run()
return epc
}
func (epc *Manager) Shutdown() {
defer epc.wg.Wait()
epc.shutdown.Store(true)
// expire the connections
epc.mu.Lock()
defer epc.mu.Unlock()
for _, conn := range epc.conns {
conn.ExpireNow()
}
}
// Run starts the managing of endpoints
func (epc *Manager) run() {
defer epc.wg.Done()
for {
if epc.shutdown.Load() {
return
}
time.Sleep(time.Second)
func() {
epc.mu.Lock()
defer epc.mu.Unlock()
for endpoint, conn := range epc.conns {
if conn.Expired() {
delete(epc.conns, endpoint)
}
}
}()
}
}
// Validate an endpoint url
func (epc *Manager) Validate(url string) error {
_, err := parseEndpoint(url)
return err
}
// Send send a message to an endpoint
func (epc *Manager) Send(endpoint, msg string) error {
for {
epc.mu.Lock()
conn, exists := epc.conns[endpoint]
if !exists || conn.Expired() {
ep, err := parseEndpoint(endpoint)
if err != nil {
epc.mu.Unlock()
return err
}
switch ep.Protocol {
default:
return errors.New("invalid protocol")
case HTTP:
conn = newHTTPConn(ep)
case Disque:
conn = newDisqueConn(ep)
case GRPC:
conn = newGRPCConn(ep)
case Redis:
conn = newRedisConn(ep)
case Kafka:
conn = newKafkaConn(ep)
case MQTT:
conn = newMQTTConn(ep)
case AMQP:
conn = newAMQPConn(ep)
case PubSub:
conn = newPubSubConn(ep)
case SQS:
conn = newSQSConn(ep)
case NATS:
conn = newNATSConn(ep)
case Local:
conn = newLocalConn(ep, epc.publisher)
case EventHub:
conn = newEventHubConn(ep)
case CFQueue:
conn = newCFQueueConn(ep)
}
epc.conns[endpoint] = conn
}
epc.mu.Unlock()
err := conn.Send(msg)
if err != nil {
if err == errExpired {
// it's possible that the connection has expired in-between
// the last conn.Expired() check and now. If so, we should
// just try the send again.
continue
}
return err
}
return nil
}
}
func parseEndpoint(s string) (Endpoint, error) {
var endpoint Endpoint
endpoint.Original = s
switch {
default:
return endpoint, errors.New("unknown scheme")
case strings.HasPrefix(s, "local:"):
endpoint.Protocol = Local
case strings.HasPrefix(s, "http:"):
endpoint.Protocol = HTTP
case strings.HasPrefix(s, "https:"):
if probeSQS(s) {
endpoint.SQS.PlainURL = s
endpoint.Protocol = SQS
} else {
endpoint.Protocol = HTTP
}
case strings.HasPrefix(s, "disque:"):
endpoint.Protocol = Disque
case strings.HasPrefix(s, "grpc:"):
endpoint.Protocol = GRPC
case strings.HasPrefix(s, "redis:"):
endpoint.Protocol = Redis
case strings.HasPrefix(s, "kafka:"):
endpoint.Protocol = Kafka
case strings.HasPrefix(s, "amqp:"):
endpoint.Protocol = AMQP
case strings.HasPrefix(s, "amqps:"):
endpoint.Protocol = AMQP
case strings.HasPrefix(s, "mqtt:"):
endpoint.Protocol = MQTT
case strings.HasPrefix(s, "pubsub:"):
endpoint.Protocol = PubSub
case strings.HasPrefix(s, "sqs:"):
endpoint.Protocol = SQS
case strings.HasPrefix(s, "nats:"):
endpoint.Protocol = NATS
case strings.HasPrefix(s, "Endpoint="):
endpoint.Protocol = EventHub
case strings.HasPrefix(s, "cf-queue:"):
endpoint.Protocol = CFQueue
}
s = s[strings.Index(s, ":")+1:]
if !strings.HasPrefix(s, "//") {
return endpoint, errors.New("missing the two slashes")
}
sqp := strings.Split(s[2:], "?")
sp := strings.Split(sqp[0], "/")
s = sp[0]
if s == "" {
if endpoint.Protocol == Local {
return endpoint, errors.New("missing channel")
}
return endpoint, errors.New("missing host")
}
// Local PubSub channel
// local://
if endpoint.Protocol == Local {
endpoint.Local.Channel = s
}
if endpoint.Protocol == GRPC {
dp := strings.Split(s, ":")
switch len(dp) {
default:
return endpoint, errors.New("invalid grpc url")
case 1:
endpoint.GRPC.Host = dp[0]
endpoint.GRPC.Port = 80
case 2:
endpoint.GRPC.Host = dp[0]
n, err := strconv.ParseUint(dp[1], 10, 16)
if err != nil {
return endpoint, errors.New("invalid grpc url")
}
endpoint.GRPC.Port = int(n)
}
}
if endpoint.Protocol == Redis {
dp := strings.Split(s, ":")
switch len(dp) {
default:
return endpoint, errors.New("invalid redis url")
case 1:
endpoint.Redis.Host = dp[0]
endpoint.Redis.Port = 6379
case 2:
endpoint.Redis.Host = dp[0]
n, err := strconv.ParseUint(dp[1], 10, 16)
if err != nil {
return endpoint, errors.New("invalid redis url port")
}
endpoint.Redis.Port = int(n)
}
if len(sp) > 1 {
var err error
endpoint.Redis.Channel, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid redis channel name")
}
}
}
if endpoint.Protocol == Disque {
dp := strings.Split(s, ":")
switch len(dp) {
default:
return endpoint, errors.New("invalid disque url")
case 1:
endpoint.Disque.Host = dp[0]
endpoint.Disque.Port = 7711
case 2:
endpoint.Disque.Host = dp[0]
n, err := strconv.ParseUint(dp[1], 10, 16)
if err != nil {
return endpoint, errors.New("invalid disque url")
}
endpoint.Disque.Port = int(n)
}
if len(sp) > 1 {
var err error
endpoint.Disque.QueueName, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid disque queue name")
}
}
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid disque url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "replicate":
n, err := strconv.ParseUint(val[0], 10, 8)
if err != nil {
return endpoint, errors.New("invalid disque replicate value")
}
endpoint.Disque.Options.Replicate = int(n)
}
}
}
if endpoint.Disque.QueueName == "" {
return endpoint, errors.New("missing disque queue name")
}
}
if endpoint.Protocol == Kafka {
// Parsing connection from URL string
hp := strings.Split(s, ":")
switch len(hp) {
default:
return endpoint, errors.New("invalid kafka url")
case 1:
endpoint.Kafka.Host = hp[0]
endpoint.Kafka.Port = 9092
case 2:
n, err := strconv.ParseUint(hp[1], 10, 16)
if err != nil {
return endpoint, errors.New("invalid kafka url port")
}
endpoint.Kafka.Host = hp[0]
endpoint.Kafka.Port = int(n)
}
// Parsing Kafka queue name
if len(sp) > 1 {
var err error
endpoint.Kafka.TopicName, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid kafka topic name")
}
}
// Throw error if we not provide any queue name
if endpoint.Kafka.TopicName == "" {
return endpoint, errors.New("missing kafka topic name")
}
// Parsing additional params
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid kafka url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "auth":
endpoint.Kafka.Auth = val[0]
case "ssl":
endpoint.Kafka.SSL, _ = strconv.ParseBool(val[0])
case "cacert":
endpoint.Kafka.CACertFile = val[0]
case "cert":
endpoint.Kafka.CertFile = val[0]
case "key":
endpoint.Kafka.KeyFile = val[0]
case "sha256":
endpoint.Kafka.SASLSHA256, _ = strconv.ParseBool(val[0])
case "sha512":
endpoint.Kafka.SASLSHA512, _ = strconv.ParseBool(val[0])
}
}
}
}
if endpoint.Protocol == MQTT {
// Parsing connection from URL string
hp := strings.Split(s, ":")
switch len(hp) {
default:
return endpoint, errors.New("invalid MQTT url")
case 1:
endpoint.MQTT.Host = hp[0]
endpoint.MQTT.Port = 1883
case 2:
n, err := strconv.ParseUint(hp[1], 10, 16)
if err != nil {
return endpoint, errors.New("invalid MQTT url port")
}
endpoint.MQTT.Host = hp[0]
endpoint.MQTT.Port = int(n)
}
// Parsing MQTT queue name
if len(sp) > 1 {
var err error
var parts []string
for _, part := range sp[1:] {
part, err = url.QueryUnescape(part)
if err != nil {
return endpoint, errors.New("invalid MQTT topic name")
}
parts = append(parts, part)
}
endpoint.MQTT.QueueName = strings.Join(parts, "/")
}
// Parsing additional params
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid MQTT url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "qos":
n, err := strconv.ParseUint(val[0], 10, 8)
if err != nil {
return endpoint, errors.New("invalid MQTT qos value")
}
endpoint.MQTT.Qos = byte(n)
case "retained":
n, err := strconv.ParseUint(val[0], 10, 8)
if err != nil {
return endpoint, errors.New("invalid MQTT retained value")
}
if n != 1 && n != 0 {
return endpoint, errors.New("invalid MQTT retained, should be [0, 1]")
}
if n == 1 {
endpoint.MQTT.Retained = true
}
case "cacert":
endpoint.MQTT.CACertFile = val[0]
case "cert":
endpoint.MQTT.CertFile = val[0]
case "key":
endpoint.MQTT.KeyFile = val[0]
}
}
}
// Throw error if we not provide any queue name
if endpoint.MQTT.QueueName == "" {
return endpoint, errors.New("missing MQTT topic name")
}
}
// Basic SQS connection strings in HOOKS interface
// sqs://://?params=value
//
// params are:
//
// credpath - path where aws credentials are located
// credprofile - credential profile
if endpoint.Protocol == SQS {
if endpoint.SQS.PlainURL == "" {
// Parsing connection from URL string
hp := strings.Split(s, ":")
switch len(hp) {
default:
return endpoint, errors.New("invalid SQS url")
case 2:
endpoint.SQS.Region = hp[0]
endpoint.SQS.QueueID = hp[1]
}
// Parsing SQS queue name
if len(sp) > 1 {
var err error
endpoint.SQS.QueueName, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid SQS queue name")
}
}
// Throw error if we not provide any queue name
if endpoint.SQS.QueueName == "" {
return endpoint, errors.New("missing SQS queue name")
}
}
// Parsing additional params
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid SQS url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "credpath":
endpoint.SQS.CredPath = val[0]
case "credprofile":
endpoint.SQS.CredProfile = val[0]
case "createqueue":
switch strings.ToLower(val[0]) {
case "0", "false":
default:
endpoint.SQS.CreateQueue = true
}
}
}
}
}
// Basic Pubsub connection strings in HOOKS interface
// pubsub://:?params=value
//
// params are:
//
// credpath - path where gcp credentials are located
if endpoint.Protocol == PubSub {
split := strings.Split(s, ":")
if len(split) != 2 {
return endpoint, errors.New("invalid PubSub format should be project/topic")
}
endpoint.PubSub.Project = split[0]
endpoint.PubSub.Topic = split[1]
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid Pubsub url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "credpath":
endpoint.PubSub.CredPath = val[0]
}
}
}
}
// Basic AMQP connection strings in HOOKS interface
// amqp://guest:guest@localhost:5672//?params=value
// or amqp://guest:guest@localhost:5672///?params=value
//
// Default params are:
//
// Mandatory - false
// Immeditate - false
// Durable - true
// Routing-Key - tile38
//
// - "route" - [string] routing key
//
if endpoint.Protocol == AMQP {
// Bind connection information
endpoint.AMQP.URI = s
endpoint.AMQP.Type = "direct"
endpoint.AMQP.Durable = true
endpoint.AMQP.DeliveryMode = amqp.Transient
// Fix incase of namespace, e.g. example.com/namespace/queue
// but not example.com/queue/ - with an endslash.
if len(sp) > 2 && len(sp[2]) > 0 {
endpoint.AMQP.URI = endpoint.AMQP.URI + "/" + sp[1]
sp = append([]string{endpoint.AMQP.URI}, sp[2:]...)
}
// Bind queue name with no namespace
if len(sp) > 1 {
var err error
endpoint.AMQP.QueueName, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid AMQP queue name")
}
}
// Parsing additional attributes
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid AMQP url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "route":
endpoint.AMQP.RouteKey = val[0]
case "type":
endpoint.AMQP.Type = val[0]
case "durable":
endpoint.AMQP.Durable = queryBool(val[0])
case "internal":
endpoint.AMQP.Internal = queryBool(val[0])
case "no_wait":
endpoint.AMQP.NoWait = queryBool(val[0])
case "auto_delete":
endpoint.AMQP.AutoDelete = queryBool(val[0])
case "immediate":
endpoint.AMQP.Immediate = queryBool(val[0])
case "mandatory":
endpoint.AMQP.Mandatory = queryBool(val[0])
case "delivery_mode":
endpoint.AMQP.DeliveryMode = uint8(queryInt(val[0]))
case "priority":
endpoint.AMQP.Priority = uint8(queryInt(val[0]))
}
}
}
if strings.HasPrefix(endpoint.Original, "amqps:") {
endpoint.AMQP.SSL = true
}
if endpoint.AMQP.QueueName == "" {
return endpoint, errors.New("missing AMQP queue name")
}
if endpoint.AMQP.RouteKey == "" {
endpoint.AMQP.RouteKey = "tile38"
}
}
// Basic NATS connection strings in HOOKS interface
// nats://://?params=value
//
// params are:
//
// user - username
// pass - password
// when user or pass is not set then login without password is used
if endpoint.Protocol == NATS {
// Parsing connection from URL string
hp := strings.Split(s, ":")
switch len(hp) {
default:
return endpoint, errors.New("invalid SQS url")
case 2:
endpoint.NATS.Host = hp[0]
port, err := strconv.Atoi(hp[1])
if err != nil {
endpoint.NATS.Port = 4222 // default nats port
} else {
endpoint.NATS.Port = port
}
}
// Parsing NATS topic name
if len(sp) > 1 {
var err error
endpoint.NATS.Topic, err = url.QueryUnescape(sp[1])
if err != nil {
return endpoint, errors.New("invalid NATS topic name")
}
}
// Parsing additional params
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid NATS url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "user":
endpoint.NATS.User = val[0]
case "pass":
endpoint.NATS.Pass = val[0]
case "token":
endpoint.NATS.Token = val[0]
case "secure":
endpoint.NATS.Secure = queryBool(val[0])
case "credential":
endpoint.NATS.UserCredentialPath = val[0]
case "jetstream":
endpoint.NATS.Jetstream = queryBool(val[0])
case "tls":
endpoint.NATS.TLS = queryBool(val[0])
case "tlscert":
endpoint.NATS.TLSCert = val[0]
case "tlskey":
endpoint.NATS.TLSKey = val[0]
}
}
}
}
if endpoint.Protocol == EventHub {
dp := strings.Split(endpoint.Original, ";")
if len(dp) != 4 {
return endpoint, errors.New("malformed EventHub connection string")
}
sakn := strings.Split(dp[1], "=")
if sakn[0] != "SharedAccessKeyName" {
return endpoint, errors.New("missing SharedAccessKeyName")
}
sak := strings.Split(dp[2], "=")
if sak[0] != "SharedAccessKey" {
return endpoint, errors.New("missing SharedAccessKey")
}
ep := strings.Split(dp[3], "=")
if ep[0] != "EntityPath" {
return endpoint, errors.New("missing EntityPath")
}
endpoint.EventHub.ConnectionString = endpoint.Original
}
// Basic CF Queue connection strings in HOOKS interface
// cf-queue:///?token=
//
// params are:
//
// token - API token
if endpoint.Protocol == CFQueue {
// Parse account_id/queue_id from the path parts
if len(sp) < 2 {
return endpoint, errors.New("invalid CF Queue format, should be account_id/queue_id")
}
endpoint.CFQueue.AccountID = sp[0]
endpoint.CFQueue.QueueID = sp[1]
// Parse query parameters for API token
if len(sqp) > 1 {
m, err := url.ParseQuery(sqp[1])
if err != nil {
return endpoint, errors.New("invalid CF Queue url")
}
for key, val := range m {
if len(val) == 0 {
continue
}
switch key {
case "token":
endpoint.CFQueue.APIToken = val[0]
}
}
}
if endpoint.CFQueue.AccountID == "" {
return endpoint, errors.New("missing CF Queue account ID")
}
if endpoint.CFQueue.QueueID == "" {
return endpoint, errors.New("missing CF Queue queue ID")
}
if endpoint.CFQueue.APIToken == "" {
return endpoint, errors.New("missing CF Queue API token")
}
}
return endpoint, nil
}
func queryInt(s string) int {
x, _ := strconv.ParseInt(s, 10, 64)
return int(x)
}
func queryBool(s string) bool {
if len(s) > 0 {
if s[0] >= '1' && s[0] <= '9' {
return true
}
switch s[0] {
case 'Y', 'y', 'T', 't':
return true
}
}
return false
}
================================================
FILE: internal/endpoint/eventHub.go
================================================
package endpoint
import (
"context"
"fmt"
"time"
"github.com/tidwall/gjson"
eventhub "github.com/Azure/azure-event-hubs-go/v3"
)
const ()
// HTTPConn is an endpoint connection
type EvenHubConn struct {
ep Endpoint
}
func newEventHubConn(ep Endpoint) *EvenHubConn {
return &EvenHubConn{
ep: ep,
}
}
// Expired returns true if the connection has expired
func (conn *EvenHubConn) Expired() bool {
return false
}
// ExpireNow forces the connection to expire
func (conn *EvenHubConn) ExpireNow() {
}
// Send sends a message
func (conn *EvenHubConn) Send(msg string) error {
hub, err := eventhub.NewHubFromConnectionString(conn.ep.EventHub.ConnectionString)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
// parse json again to get out info for our kafka key
key := gjson.Get(msg, "key")
id := gjson.Get(msg, "id")
keyValue := fmt.Sprintf("%s-%s", key.String(), id.String())
evtHubMsg := eventhub.NewEventFromString(msg)
evtHubMsg.PartitionKey = &keyValue
err = hub.Send(ctx, evtHubMsg)
if err != nil {
return err
}
return nil
}
================================================
FILE: internal/endpoint/grpc.go
================================================
package endpoint
import (
"errors"
"fmt"
"sync"
"time"
"github.com/tidwall/tile38/internal/hservice"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const grpcExpiresAfter = time.Second * 30
// GRPCConn is an endpoint connection
type GRPCConn struct {
mu sync.Mutex
ep Endpoint
ex bool
t time.Time
conn *grpc.ClientConn
sconn hservice.HookServiceClient
}
func newGRPCConn(ep Endpoint) *GRPCConn {
return &GRPCConn{
ep: ep,
t: time.Now(),
}
}
// Expired returns true if the connection has expired
func (conn *GRPCConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > grpcExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *GRPCConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *GRPCConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
}
}
// Send sends a message
func (conn *GRPCConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.GRPC.Host, conn.ep.GRPC.Port)
var err error
conn.conn, err = grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
conn.close()
return err
}
conn.sconn = hservice.NewHookServiceClient(conn.conn)
}
r, err := conn.sconn.Send(context.Background(), &hservice.MessageRequest{Value: msg})
if err != nil {
conn.close()
return err
}
if !r.Ok {
conn.close()
return errors.New("invalid grpc reply")
}
return nil
}
================================================
FILE: internal/endpoint/http.go
================================================
package endpoint
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
)
const (
httpExpiresAfter = time.Second * 30
httpRequestTimeout = time.Second * 5
httpMaxIdleConnections = 20
)
// HTTPConn is an endpoint connection
type HTTPConn struct {
ep Endpoint
client *http.Client
}
func newHTTPConn(ep Endpoint) *HTTPConn {
return &HTTPConn{
ep: ep,
client: &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: httpMaxIdleConnections,
IdleConnTimeout: httpExpiresAfter,
},
Timeout: httpRequestTimeout,
},
}
}
// Expired returns true if the connection has expired
func (conn *HTTPConn) Expired() bool {
return false
}
// ExpireNow forces the connection to expire
func (conn *HTTPConn) ExpireNow() {
}
// Send sends a message
func (conn *HTTPConn) Send(msg string) error {
req, err := http.NewRequest("POST", conn.ep.Original, bytes.NewBufferString(msg))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := conn.client.Do(req)
if err != nil {
return err
}
// close the connection to reuse it
defer resp.Body.Close()
// discard response
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
return err
}
// Only allow responses with status code 200, 201, and 202
if resp.StatusCode != http.StatusOK &&
resp.StatusCode != http.StatusCreated &&
resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("invalid status: %s", resp.Status)
}
return nil
}
================================================
FILE: internal/endpoint/kafka.go
================================================
package endpoint
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"sync"
"time"
lg "log"
"github.com/IBM/sarama"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/internal/log"
)
const kafkaExpiresAfter = time.Second * 30
// KafkaConn is an endpoint connection
type KafkaConn struct {
mu sync.Mutex
ep Endpoint
conn sarama.SyncProducer
cfg *sarama.Config
ex bool
t time.Time
}
// Expired returns true if the connection has expired
func (conn *KafkaConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > kafkaExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *KafkaConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *KafkaConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
conn.cfg.MetricRegistry.UnregisterAll()
conn.cfg = nil
}
}
// Send sends a message
func (conn *KafkaConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if log.Level() > 2 {
sarama.Logger = lg.New(log.Output(), "[sarama] ", 0)
}
uri := fmt.Sprintf("%s:%d", conn.ep.Kafka.Host, conn.ep.Kafka.Port)
if conn.conn == nil {
cfg := sarama.NewConfig()
cfg.Net.DialTimeout = time.Second
cfg.Net.ReadTimeout = time.Second * 5
cfg.Net.WriteTimeout = time.Second * 5
// Fix #333 : fix backward incompatibility introduced by sarama library
cfg.Producer.Return.Successes = true
// Sarama now sets the version based on the broker version in the release 1.46.0 as of August 25 2025.
// There is no need to force this anymore as it is breaking the version check in Kafka 4.0 and above.
// cfg.Version = sarama.V0_10_0_0
switch conn.ep.Kafka.Auth {
case "sasl":
// This path allows to either provide a custom ca certificate
// or, because RootCAs is nil, is using the hosts ca set
// to verify the server certificate
if conn.ep.Kafka.SSL {
tlsConfig := tls.Config{}
if conn.ep.Kafka.CACertFile != "" {
caCertPool, err := loadRootTLSCert(conn.ep.Kafka.CACertFile)
if err != nil {
return err
}
tlsConfig.RootCAs = &caCertPool
}
cfg.Net.TLS.Enable = true
cfg.Net.TLS.Config = &tlsConfig
}
cfg.Net.SASL.Enable = true
cfg.Net.SASL.User = os.Getenv("KAFKA_USERNAME")
cfg.Net.SASL.Password = os.Getenv("KAFKA_PASSWORD")
cfg.Net.SASL.Handshake = true
cfg.Net.SASL.Mechanism = sarama.SASLTypePlaintext
if conn.ep.Kafka.SASLSHA256 {
cfg.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA256} }
cfg.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA256
}
if conn.ep.Kafka.SASLSHA512 {
cfg.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA512} }
cfg.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA512
}
case "tls":
tlsConfig := tls.Config{}
cfg.Net.TLS.Enable = true
certificates, err := loadClientTLSCert(conn.ep.Kafka.KeyFile, conn.ep.Kafka.CertFile)
if err != nil {
cfg.MetricRegistry.UnregisterAll()
return err
}
tlsConfig.Certificates = certificates
// This path allows to either provide a custom ca certificate
// or, because RootCAs is nil, is using the hosts ca set
// to verify server certificate
if conn.ep.Kafka.CACertFile != "" {
caCertPool, err := loadRootTLSCert(conn.ep.Kafka.CACertFile)
if err != nil {
return err
}
tlsConfig.RootCAs = &caCertPool
}
cfg.Net.TLS.Config = &tlsConfig
case "none":
// This path allows to either provide a custom ca certificate
// or, because RootCAs is nil, is using the hosts ca set
// to verify the server certificate
if conn.ep.Kafka.SSL {
tlsConfig := tls.Config{}
if conn.ep.Kafka.CACertFile != "" {
caCertPool, err := loadRootTLSCert(conn.ep.Kafka.CACertFile)
if err != nil {
return err
}
tlsConfig.RootCAs = &caCertPool
}
cfg.Net.TLS.Enable = true
cfg.Net.TLS.Config = &tlsConfig
}
}
c, err := sarama.NewSyncProducer([]string{uri}, cfg)
if err != nil {
cfg.MetricRegistry.UnregisterAll()
return err
}
conn.conn = c
conn.cfg = cfg
}
// parse json again to get out info for our kafka key
key := gjson.Get(msg, "key")
id := gjson.Get(msg, "id")
keyValue := fmt.Sprintf("%s-%s", key.String(), id.String())
message := &sarama.ProducerMessage{
Topic: conn.ep.Kafka.TopicName,
Key: sarama.StringEncoder(keyValue),
Value: sarama.StringEncoder(msg),
}
_, offset, err := conn.conn.SendMessage(message)
if err != nil {
conn.close()
return err
}
if offset < 0 {
conn.close()
return errors.New("invalid kafka reply")
}
return nil
}
func newKafkaConn(ep Endpoint) *KafkaConn {
return &KafkaConn{
ep: ep,
t: time.Now(),
}
}
func loadClientTLSCert(KeyFile, CertFile string) ([]tls.Certificate, error) {
// load client cert
cert, err := tls.LoadX509KeyPair(CertFile, KeyFile)
if err != nil {
return []tls.Certificate{cert}, err
}
return []tls.Certificate{cert}, err
}
func loadRootTLSCert(CACertFile string) (x509.CertPool, error) {
// Load CA cert
caCert, err := os.ReadFile(CACertFile)
if err != nil {
return x509.CertPool{}, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
return *caCertPool, err
}
================================================
FILE: internal/endpoint/local.go
================================================
package endpoint
// LocalPublisher is used to publish local notifications
type LocalPublisher interface {
Publish(channel string, message ...string) int
}
// LocalConn is an endpoint connection
type LocalConn struct {
ep Endpoint
publisher LocalPublisher
}
func newLocalConn(ep Endpoint, publisher LocalPublisher) *LocalConn {
return &LocalConn{
ep: ep,
publisher: publisher,
}
}
// Expired returns true if the connection has expired
func (conn *LocalConn) Expired() bool {
return false
}
// ExpireNow forces the connection to expire
func (conn *LocalConn) ExpireNow() {
}
// Send sends a message
func (conn *LocalConn) Send(msg string) error {
conn.publisher.Publish(conn.ep.Local.Channel, msg)
return nil
}
================================================
FILE: internal/endpoint/mqtt.go
================================================
package endpoint
import (
"crypto/tls"
"crypto/x509"
"fmt"
"math/rand"
"os"
"sync"
"time"
paho "github.com/eclipse/paho.mqtt.golang"
"github.com/tidwall/tile38/internal/log"
)
const (
mqttExpiresAfter = time.Second * 30
mqttPublishTimeout = time.Second * 5
)
// MQTTConn is an endpoint connection
type MQTTConn struct {
mu sync.Mutex
ep Endpoint
conn paho.Client
ex bool
t time.Time
}
// Expired returns true if the connection has expired
func (conn *MQTTConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > mqttExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *MQTTConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *MQTTConn) close() {
if conn.conn != nil {
if conn.conn.IsConnected() {
conn.conn.Disconnect(250)
}
conn.conn = nil
}
}
// Send sends a message
func (conn *MQTTConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
uri := fmt.Sprintf("tcp://%s:%d", conn.ep.MQTT.Host, conn.ep.MQTT.Port)
ops := paho.NewClientOptions()
if conn.ep.MQTT.CertFile != "" || conn.ep.MQTT.KeyFile != "" ||
conn.ep.MQTT.CACertFile != "" {
var config tls.Config
if conn.ep.MQTT.CertFile != "" || conn.ep.MQTT.KeyFile != "" {
cert, err := tls.LoadX509KeyPair(conn.ep.MQTT.CertFile,
conn.ep.MQTT.KeyFile)
if err != nil {
return err
}
config.Certificates = append(config.Certificates, cert)
}
if conn.ep.MQTT.CACertFile != "" {
// Load CA cert
caCert, err := os.ReadFile(conn.ep.MQTT.CACertFile)
if err != nil {
return err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config.RootCAs = caCertPool
}
ops = ops.SetTLSConfig(&config)
}
//generate UUID for the client-id.
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Debugf("Failed to generate guid for the mqtt client. The endpoint will not work")
return err
}
uuid := fmt.Sprintf("tile38-%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
ops = ops.SetClientID(uuid).AddBroker(uri)
c := paho.NewClient(ops)
if token := c.Connect(); token.Wait() && token.Error() != nil {
return token.Error()
}
conn.conn = c
}
t := conn.conn.Publish(conn.ep.MQTT.QueueName, conn.ep.MQTT.Qos,
conn.ep.MQTT.Retained, msg)
if !t.WaitTimeout(mqttPublishTimeout) || t.Error() != nil {
conn.close()
return t.Error()
}
return nil
}
func newMQTTConn(ep Endpoint) *MQTTConn {
return &MQTTConn{
ep: ep,
t: time.Now(),
}
}
================================================
FILE: internal/endpoint/nats.go
================================================
package endpoint
import (
"context"
"fmt"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
const natsExpiresAfter = time.Second * 30
// NATSConn is an endpoint connection
type NATSConn struct {
mu sync.Mutex
ep Endpoint
ex bool
t time.Time
conn *nats.Conn
js jetstream.JetStream
}
func newNATSConn(ep Endpoint) *NATSConn {
return &NATSConn{
ep: ep,
t: time.Now(),
}
}
// Expired returns true if the connection has expired
func (conn *NATSConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > natsExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *NATSConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *NATSConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
}
}
// Send sends a message
func (conn *NATSConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.NATS.Host, conn.ep.NATS.Port)
scheme := "nats" // 'nats://' by default
var err error
var opts []nats.Option
if conn.ep.NATS.User != "" && conn.ep.NATS.Pass != "" {
opts = append(opts, nats.UserInfo(conn.ep.NATS.User, conn.ep.NATS.Pass))
}
if conn.ep.NATS.TLS {
opts = append(opts, nats.ClientCert(
conn.ep.NATS.TLSCert, conn.ep.NATS.TLSKey,
))
}
if conn.ep.NATS.Token != "" {
opts = append(opts, nats.Token(conn.ep.NATS.Token))
}
if conn.ep.NATS.UserCredentialPath != "" {
opts = append(opts, nats.UserCredentials(conn.ep.NATS.UserCredentialPath))
}
if conn.ep.NATS.Secure {
scheme = "tls"
}
addr = fmt.Sprintf("%s://%s", scheme, addr)
conn.conn, err = nats.Connect(addr, opts...)
if err != nil {
conn.close()
return err
}
if conn.ep.NATS.Jetstream {
conn.js, err = jetstream.New(conn.conn)
if err != nil {
conn.close()
return err
}
}
}
if conn.js == nil {
return conn.publish(msg)
}
return conn.publishJS(msg)
}
// publishJS will publish the message to the subject using core nats.
func (conn *NATSConn) publish(msg string) error {
err := conn.conn.Publish(conn.ep.NATS.Topic, []byte(msg))
if err != nil {
conn.close()
return err
}
return nil
}
// publishJS will publish the message expecting a jetstream acknowledgement.
func (conn *NATSConn) publishJS(msg string) error {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
_, err := conn.js.Publish(ctx, conn.ep.NATS.Topic, []byte(msg))
if err != nil {
conn.close()
return err
}
return nil
}
================================================
FILE: internal/endpoint/pubsub.go
================================================
package endpoint
import (
"context"
"fmt"
"sync"
"time"
"cloud.google.com/go/pubsub"
"google.golang.org/api/option"
)
const pubsubExpiresAfter = time.Second * 30
// SQSConn is an endpoint connection
type PubSubConn struct {
mu sync.Mutex
ep Endpoint
svc *pubsub.Client
topic *pubsub.Topic
ex bool
t time.Time
}
func (conn *PubSubConn) close() {
if conn.svc != nil {
conn.svc.Close()
conn.svc = nil
}
}
// Send sends a message
func (conn *PubSubConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
ctx := context.Background()
conn.t = time.Now()
if conn.svc == nil {
var creds option.ClientOption
var svc *pubsub.Client
var err error
credPath := conn.ep.PubSub.CredPath
if credPath != "" {
creds = option.WithCredentialsFile(credPath)
svc, err = pubsub.NewClient(ctx, conn.ep.PubSub.Project, creds)
} else {
svc, err = pubsub.NewClient(ctx, conn.ep.PubSub.Project)
}
if err != nil {
fmt.Println(err)
return err
}
topic := svc.Topic(conn.ep.PubSub.Topic)
conn.svc = svc
conn.topic = topic
}
// Send message
res := conn.topic.Publish(ctx, &pubsub.Message{
Data: []byte(msg),
})
_, err := res.Get(ctx)
if err != nil {
fmt.Println(err)
return err
}
return nil
}
func (conn *PubSubConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > pubsubExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *PubSubConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func newPubSubConn(ep Endpoint) *PubSubConn {
return &PubSubConn{
ep: ep,
t: time.Now(),
}
}
================================================
FILE: internal/endpoint/redis.go
================================================
package endpoint
import (
"fmt"
"sync"
"time"
"github.com/gomodule/redigo/redis"
)
const redisExpiresAfter = time.Second * 30
// RedisConn is an endpoint connection
type RedisConn struct {
mu sync.Mutex
ep Endpoint
ex bool
t time.Time
conn redis.Conn
}
func newRedisConn(ep Endpoint) *RedisConn {
return &RedisConn{
ep: ep,
t: time.Now(),
}
}
// Expired returns true if the connection has expired
func (conn *RedisConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > redisExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *RedisConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *RedisConn) close() {
if conn.conn != nil {
conn.conn.Close()
conn.conn = nil
}
}
// Send sends a message
func (conn *RedisConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.conn == nil {
addr := fmt.Sprintf("%s:%d", conn.ep.Redis.Host, conn.ep.Redis.Port)
var err error
conn.conn, err = redis.Dial("tcp", addr)
if err != nil {
conn.close()
return err
}
}
_, err := redis.Int(conn.conn.Do("PUBLISH", conn.ep.Redis.Channel, msg))
if err != nil {
conn.close()
return err
}
return nil
}
================================================
FILE: internal/endpoint/scram_client.go
================================================
package endpoint
import (
"crypto/sha256"
"crypto/sha512"
"github.com/xdg-go/scram"
)
var (
SHA256 scram.HashGeneratorFcn = sha256.New
SHA512 scram.HashGeneratorFcn = sha512.New
)
type XDGSCRAMClient struct {
*scram.Client
*scram.ClientConversation
scram.HashGeneratorFcn
}
func (x *XDGSCRAMClient) Begin(userName, password, authzID string) (err error) {
x.Client, err = x.HashGeneratorFcn.NewClient(userName, password, authzID)
if err != nil {
return err
}
x.ClientConversation = x.Client.NewConversation()
return nil
}
func (x *XDGSCRAMClient) Step(challenge string) (response string, err error) {
response, err = x.ClientConversation.Step(challenge)
return
}
func (x *XDGSCRAMClient) Done() bool {
return x.ClientConversation.Done()
}
================================================
FILE: internal/endpoint/sqs.go
================================================
package endpoint
import (
"fmt"
"strings"
"sync"
"time"
"github.com/tidwall/gjson"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sqs"
"github.com/tidwall/tile38/internal/log"
)
const sqsExpiresAfter = time.Second * 30
// SQSConn is an endpoint connection
type SQSConn struct {
mu sync.Mutex
ep Endpoint
session *session.Session
svc *sqs.SQS
ex bool
t time.Time
}
func (conn *SQSConn) generateSQSURL() string {
if conn.ep.SQS.PlainURL != "" {
return conn.ep.SQS.PlainURL
}
return "https://sqs." + conn.ep.SQS.Region + ".amazonaws.com/" +
conn.ep.SQS.QueueID + "/" + conn.ep.SQS.QueueName
}
// Expired returns true if the connection has expired
func (conn *SQSConn) Expired() bool {
conn.mu.Lock()
defer conn.mu.Unlock()
if !conn.ex {
if time.Since(conn.t) > sqsExpiresAfter {
conn.close()
conn.ex = true
}
}
return conn.ex
}
// ExpireNow forces the connection to expire
func (conn *SQSConn) ExpireNow() {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.close()
conn.ex = true
}
func (conn *SQSConn) close() {
if conn.svc != nil {
conn.svc = nil
conn.session = nil
}
}
// Send sends a message
func (conn *SQSConn) Send(msg string) error {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ex {
return errExpired
}
conn.t = time.Now()
if conn.svc == nil && conn.session == nil {
var creds *credentials.Credentials
credPath := conn.ep.SQS.CredPath
if credPath != "" {
credProfile := conn.ep.SQS.CredProfile
if credProfile == "" {
credProfile = "default"
}
creds = credentials.NewSharedCredentials(credPath, credProfile)
}
var region string
if conn.ep.SQS.Region != "" {
region = conn.ep.SQS.Region
} else {
region = sqsRegionFromPlainURL(conn.ep.SQS.PlainURL)
}
sess := session.Must(session.NewSession(&aws.Config{
Region: ®ion,
Credentials: creds,
CredentialsChainVerboseErrors: aws.Bool(log.Level() >= 3),
MaxRetries: aws.Int(5),
}))
svc := sqs.New(sess)
if conn.ep.SQS.CreateQueue {
svc.CreateQueue(&sqs.CreateQueueInput{
QueueName: aws.String(conn.ep.SQS.QueueName),
Attributes: map[string]*string{
"DelaySeconds": aws.String("60"),
"MessageRetentionPeriod": aws.String("86400"),
},
})
}
conn.session = sess
conn.svc = svc
}
queueURL := conn.generateSQSURL()
// Create message
sendParams := &sqs.SendMessageInput{
MessageBody: aws.String(msg),
QueueUrl: aws.String(queueURL),
}
if isFifoQueue(queueURL) {
key := gjson.Get(msg, "key")
id := gjson.Get(msg, "id")
keyValue := fmt.Sprintf("%s#%s", key.String(), id.String())
sendParams.MessageGroupId = aws.String(keyValue)
}
_, err := conn.svc.SendMessage(sendParams)
if err != nil {
fmt.Println(err)
return err
}
return nil
}
func newSQSConn(ep Endpoint) *SQSConn {
return &SQSConn{
ep: ep,
t: time.Now(),
}
}
func probeSQS(s string) bool {
// https://sqs.eu-central-1.amazonaws.com/123456789/myqueue
return strings.HasPrefix(s, "https://sqs.") &&
strings.Contains(s, ".amazonaws.com")
}
func sqsRegionFromPlainURL(s string) string {
parts := strings.Split(s, "https://sqs.")
if len(parts) > 1 {
parts = strings.Split(parts[1], ".amazonaws.com")
if len(parts) > 1 {
return parts[0]
}
}
return ""
}
func isFifoQueue(s string) bool {
return strings.HasSuffix(s, ".fifo")
}
================================================
FILE: internal/field/field.go
================================================
package field
import (
"math"
"strconv"
"strings"
"github.com/tidwall/gjson"
"github.com/tidwall/pretty"
)
var ZeroValue = Value{kind: Number, data: "0", num: 0}
var ZeroField = Field{name: "", value: ZeroValue}
type Kind byte
const (
Null = Kind(gjson.Null)
False = Kind(gjson.False)
Number = Kind(gjson.Number)
String = Kind(gjson.String)
True = Kind(gjson.True)
JSON = Kind(gjson.JSON)
)
type Value struct {
kind Kind
data string
num float64
}
func (v Value) IsZero() bool {
return (v.kind == Number && v.data == "0" && v.num == 0) || v == (Value{})
}
func (v Value) Equals(b Value) bool {
return !v.Less(b) && !b.Less(v)
}
func (v Value) Kind() Kind {
return v.kind
}
func (v Value) Data() string {
return v.data
}
func (v Value) Num() float64 {
return v.num
}
func (v Value) JSON() string {
switch v.Kind() {
case Number:
switch v.Data() {
case "NaN":
return `"NaN"`
case "+Inf":
return `"+Inf"`
case "-Inf":
return `"-Inf"`
default:
return v.Data()
}
case String:
return string(gjson.AppendJSONString(nil, v.Data()))
case True:
return "true"
case False:
return "false"
case Null:
if v != (Value{}) {
return "null"
}
case JSON:
return v.Data()
}
return "0"
}
func stringLessInsensitive(a, b string) bool {
for i := 0; i < len(a) && i < len(b); i++ {
if a[i] >= 'A' && a[i] <= 'Z' {
if b[i] >= 'A' && b[i] <= 'Z' {
// both are uppercase, do nothing
if a[i] < b[i] {
return true
} else if a[i] > b[i] {
return false
}
} else {
// a is uppercase, convert a to lowercase
if a[i]+32 < b[i] {
return true
} else if a[i]+32 > b[i] {
return false
}
}
} else if b[i] >= 'A' && b[i] <= 'Z' {
// b is uppercase, convert b to lowercase
if a[i] < b[i]+32 {
return true
} else if a[i] > b[i]+32 {
return false
}
} else {
// neither are uppercase
if a[i] < b[i] {
return true
} else if a[i] > b[i] {
return false
}
}
}
return len(a) < len(b)
}
// Less return true if a value is less than another value.
// The caseSensitive parameter is used when the value are Strings.
// The order when comparing two different kinds is:
//
// Null < False < Number < String < True < JSON
//
// Pulled from github.com/tidwall/gjson
func (v Value) LessCase(b Value, caseSensitive bool) bool {
if v.kind < b.kind {
return true
}
if v.kind > b.kind {
return false
}
if v.kind == Number {
return v.num < b.num
}
if v.kind == String {
if caseSensitive {
return v.data < b.data
}
return stringLessInsensitive(v.data, b.data)
}
return v.data < b.data
}
// Less return true if a value is less than another value.
//
// Null < False < Number < String < True < JSON
//
// Pulled from github.com/tidwall/gjson
func (v Value) Less(b Value) bool {
return v.LessCase(b, false)
}
type Field struct {
name string
value Value
}
func (f Field) Name() string {
return f.name
}
func (f Field) Value() Value {
return f.value
}
func (f Field) Weight() int {
return len(f.name) + 8 + len(f.value.data)
}
var nan = math.NaN()
var pinf = math.Inf(+1)
var ninf = math.Inf(-1)
func ValueOf(data string) Value {
data = strings.TrimSpace(data)
num, err := strconv.ParseFloat(data, 64)
if err == nil {
if math.IsInf(num, 0) {
if math.IsInf(num, +1) {
return Value{kind: Number, data: "+Inf", num: pinf}
} else {
return Value{kind: Number, data: "-Inf", num: ninf}
}
} else if math.IsNaN(num) {
return Value{kind: Number, data: "NaN", num: nan}
}
// Make sure that this is a JSON compatible number.
// For example, "000123" and "000_123" both parse as floats but aren't
// really Numbers that can be represents in JSON.
if gjson.Valid(data) {
return Value{kind: Number, data: data, num: num}
}
} else if gjson.Valid(data) {
data = strings.TrimSpace(data)
r := gjson.Parse(data)
switch r.Type {
case gjson.Null:
return Value{kind: Null, data: "null"}
case gjson.JSON:
return Value{kind: JSON, data: string(pretty.Ugly([]byte(data)))}
case gjson.True:
return Value{kind: True, data: "true"}
case gjson.False:
return Value{kind: False, data: "false"}
case gjson.Number:
// Ignore. Numbers will always be picked up by the ParseFloat above.
case gjson.String:
// Ignore. Strings fallthrough by default
}
// Extract String from JSON
data = r.String()
}
// Check if string is NaN, Inf(inity), +Inf(inity), -Inf(inity)
if len(data) >= 3 && len(data) <= 9 {
switch data[0] {
case '-', '+', 'I', 'i', 'N', 'n':
switch strings.ToLower(data) {
case "nan":
return Value{kind: Number, data: "NaN", num: nan}
case "inf", "+inf", "infinity", "+infinity":
return Value{kind: Number, data: "+Inf", num: pinf}
case "-inf", "-infinity":
return Value{kind: Number, data: "-Inf", num: ninf}
}
}
}
return Value{kind: String, data: data}
}
func Make(name, data string) Field {
return Field{
strings.TrimSpace(name),
ValueOf(data),
}
}
================================================
FILE: internal/field/field_test.go
================================================
package field
import (
"testing"
"github.com/tidwall/assert"
)
func mLT(a, b Value) bool { return a.Less(b) }
func mLTE(a, b Value) bool { return !mLT(b, a) }
func mGT(a, b Value) bool { return mLT(b, a) }
func mGTE(a, b Value) bool { return !mLT(a, b) }
func mEQ(a, b Value) bool { return !mLT(a, b) && !mLT(b, a) }
func TestOrder(t *testing.T) {
assert.Assert(mLT(ValueOf("hello"), ValueOf("jello")))
assert.Assert(mLT(ValueOf("hello"), ValueOf("JELLO")))
assert.Assert(mLT(ValueOf("HELLO"), ValueOf("JELLO")))
assert.Assert(mLT(ValueOf("HELLO"), ValueOf("jello")))
assert.Assert(!mLT(ValueOf("hello"), ValueOf("hello")))
assert.Assert(!mLT(ValueOf("jello"), ValueOf("hello")))
assert.Assert(!mLT(ValueOf("Jello"), ValueOf("Hello")))
assert.Assert(!mLT(ValueOf("Jello"), ValueOf("hello")))
assert.Assert(!mLT(ValueOf("jello"), ValueOf("Hello")))
assert.Assert(mGT(ValueOf("jello"), ValueOf("hello")))
assert.Assert(!mGT(ValueOf("jello"), ValueOf("jello")))
assert.Assert(!mGT(ValueOf("hello"), ValueOf("jello")))
assert.Assert(mLTE(ValueOf("hello"), ValueOf("jello")))
assert.Assert(mLTE(ValueOf("hello"), ValueOf("hello")))
assert.Assert(mLTE(ValueOf("hello"), ValueOf("HELLO")))
assert.Assert(!mLTE(ValueOf("jello"), ValueOf("hello")))
assert.Assert(mGTE(ValueOf("jello"), ValueOf("jello")))
assert.Assert(mGTE(ValueOf("jello"), ValueOf("hello")))
assert.Assert(mGTE(ValueOf("jello"), ValueOf("JELLO")))
assert.Assert(!mGTE(ValueOf("hello"), ValueOf("jello")))
assert.Assert(mEQ(ValueOf("jello"), ValueOf("jello")))
assert.Assert(mEQ(ValueOf("jello"), ValueOf("JELLO")))
assert.Assert(!mEQ(ValueOf("jello"), ValueOf("hello")))
}
func TestLess(t *testing.T) {
assert.Assert(mLT(ValueOf("null"), ValueOf("false")))
assert.Assert(mLT(ValueOf("null"), ValueOf("123")))
assert.Assert(mLT(ValueOf("null"), ValueOf("hello")))
assert.Assert(mLT(ValueOf("null"), ValueOf("true")))
assert.Assert(mLT(ValueOf("null"), ValueOf("[]")))
assert.Assert(mLT(ValueOf("false"), ValueOf("123")))
assert.Assert(mLT(ValueOf("false"), ValueOf("hello")))
assert.Assert(mLT(ValueOf("false"), ValueOf("true")))
assert.Assert(mLT(ValueOf("false"), ValueOf("[]")))
assert.Assert(mLT(ValueOf("123"), ValueOf("hello")))
assert.Assert(mLT(ValueOf("123"), ValueOf("true")))
assert.Assert(mLT(ValueOf("123"), ValueOf("[]")))
assert.Assert(mLT(ValueOf("hello"), ValueOf("true")))
assert.Assert(mLT(ValueOf("hello"), ValueOf("[]")))
assert.Assert(mLT(ValueOf("true"), ValueOf("[]")))
assert.Assert(!mLT(ValueOf("false"), ValueOf("null")))
assert.Assert(!mLT(ValueOf("123"), ValueOf("null")))
assert.Assert(!mLT(ValueOf("hello"), ValueOf("null")))
assert.Assert(!mLT(ValueOf("true"), ValueOf("null")))
assert.Assert(!mLT(ValueOf("[]"), ValueOf("null")))
assert.Assert(!mLT(ValueOf("123"), ValueOf("false")))
assert.Assert(!mLT(ValueOf("hello"), ValueOf("false")))
assert.Assert(!mLT(ValueOf("true"), ValueOf("false")))
assert.Assert(!mLT(ValueOf("[]"), ValueOf("false")))
assert.Assert(!mLT(ValueOf("hello"), ValueOf("123")))
assert.Assert(!mLT(ValueOf("true"), ValueOf("123")))
assert.Assert(!mLT(ValueOf("[]"), ValueOf("123")))
assert.Assert(!mLT(ValueOf("true"), ValueOf("hello")))
assert.Assert(!mLT(ValueOf("[]"), ValueOf("hello")))
assert.Assert(!mLT(ValueOf("[]"), ValueOf("true")))
assert.Assert(mLT(ValueOf("123"), ValueOf("456")))
assert.Assert(mLT(ValueOf("[1]"), ValueOf("[2]")))
}
func TestLessCase(t *testing.T) {
assert.Assert(ValueOf("A").LessCase(ValueOf("B"), true))
assert.Assert(!ValueOf("A").LessCase(ValueOf("A"), true))
assert.Assert(!ValueOf("B").LessCase(ValueOf("A"), true))
}
func TestVarious(t *testing.T) {
assert.Assert(!ValueOf("A").IsZero())
assert.Assert(ValueOf("0").IsZero())
assert.Assert(Value{}.IsZero())
assert.Assert(ZeroValue.IsZero())
assert.Assert(ZeroValue.Equals(ZeroValue))
assert.Assert(ZeroValue.Kind() == Number)
assert.Assert(ValueOf("0").Kind() == Number)
assert.Assert(ValueOf("hello").Kind() == String)
assert.Assert(ValueOf(`"hello"`).Kind() == String)
assert.Assert(ValueOf(`"123"`).Kind() == String)
assert.Assert(ValueOf(`"123"`).Data() == `123`)
assert.Assert(ValueOf(`"123"`).Num() == 0)
}
func TestJSON(t *testing.T) {
assert.Assert(ValueOf(`A`).JSON() == `"A"`)
assert.Assert(ValueOf(`"A"`).JSON() == `"A"`)
assert.Assert(ValueOf(`123`).JSON() == `123`)
assert.Assert(ValueOf(`{}`).JSON() == `{}`)
assert.Assert(ValueOf(`{ }`).JSON() == `{}`)
assert.Assert(ValueOf(` -Inf `).JSON() == `"-Inf"`)
assert.Assert(ValueOf(` "-Inf" `).JSON() == `"-Inf"`)
assert.Assert(ValueOf(`+Inf`).JSON() == `"+Inf"`)
assert.Assert(ValueOf(`"+Inf"`).JSON() == `"+Inf"`)
assert.Assert(ValueOf(`Inf`).JSON() == `"+Inf"`)
assert.Assert(ValueOf(`"Inf"`).JSON() == `"+Inf"`)
assert.Assert(ValueOf(`NaN`).JSON() == `"NaN"`)
assert.Assert(ValueOf(`"NaN"`).JSON() == `"NaN"`)
assert.Assert(ValueOf(`nan`).JSON() == `"NaN"`)
assert.Assert(ValueOf(`infinity`).JSON() == `"+Inf"`)
assert.Assert(ValueOf(` true `).JSON() == `true`)
assert.Assert(ValueOf(` false `).JSON() == `false`)
assert.Assert(ValueOf(` null `).JSON() == `null`)
assert.Assert(Value{}.JSON() == `0`)
assert.Assert(Value{}.JSON() == `0`)
}
func TestField(t *testing.T) {
assert.Assert(Make("hello", "123").Name() == "hello")
assert.Assert(Make("HELLO", "123").Name() == "HELLO")
assert.Assert(Make("HELLO", "123").Value().Num() == 123)
assert.Assert(Make("HELLO", "123").Value().JSON() == "123")
assert.Assert(Make("HELLO", "123").Value().Num() == 123)
}
func TestWeight(t *testing.T) {
assert.Assert(Make("hello", "123").Weight() == 16)
}
func TestNumber(t *testing.T) {
assert.Assert(ValueOf("12").Num() == 12)
assert.Assert(ValueOf("012").Num() == 0)
}
================================================
FILE: internal/field/list_binary.go
================================================
package field
import (
"encoding/binary"
"strconv"
"strings"
"unsafe"
"github.com/tidwall/gjson"
"github.com/tidwall/pretty"
"github.com/tidwall/tile38/internal/sstring"
)
// binary format
// (size,entry,[entry...])
// size: uvarint -- size of the full byte slice, excluding itself.
// entry: (name,value) -- one field entry
// name: shared string num -- field name, string data, uses the shared library
// size: uvarint -- number of bytes in data
// value: (kind,vdata) -- field value
// kind: byte -- value kind
// vdata: (size,data) -- value data, string data
// useSharedNames will results in smaller memory usage by sharing the names
// of fields using the sstring package. Otherwise the names are embedded with
// the list.
const useSharedNames = true
// List of fields, ordered by Name.
type List struct {
p *byte
}
type bytes struct {
p *byte
l int
c int
}
func ptob(p *byte) []byte {
if p == nil {
return nil
}
// Get the size of the bytes (excluding the header)
x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{p, 10, 10})))
// Return the byte slice (excluding the header)
return (*(*[]byte)(unsafe.Pointer(&bytes{p, n + x, n + x})))[n:]
}
func btoa(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
// uvarint is a slightly modified version of binary.Uvarint, and it's a little
// faster. But it lacks overflow checks which are not needed for our use.
func uvarint(buf []byte) (int, int) {
var x uint64
for i := 0; i < len(buf); i++ {
b := buf[i]
if b < 0x80 {
return int(x | uint64(b)<<(i*7)), i + 1
}
x |= uint64(b&0x7f) << (i * 7)
}
return 0, 0
}
func datakind(kind Kind) bool {
switch kind {
case Number, String, JSON:
return true
}
return false
}
func bfield(name string, kind Kind, data string) Field {
var num float64
switch kind {
case Number:
num, _ = strconv.ParseFloat(data, 64)
case Null:
data = "null"
case False:
data = "false"
case True:
data = "true"
}
return Field{
name: name,
value: Value{
kind: Kind(kind),
data: data,
num: num,
},
}
}
// Set a field in the list.
// If the input field value is zero `f.Value().IsZero()` then the field is
// deleted or removed from the list since lists cannot have Zero values.
// Returns a newly allocated list the updated field.
// The original (receiver) list is not modified.
func (fields List) Set(field Field) List {
b := ptob(fields.p)
var i int
for {
s := i
// read the name
var name string
x, n := uvarint(b[i:])
if n == 0 {
break
}
if useSharedNames {
name = sstring.Load(x)
i += n
} else {
name = btoa(b[i+n : i+n+x])
i += n + x
}
kind := Kind(b[i])
i++
var data string
if datakind(kind) {
x, n = uvarint(b[i:])
data = btoa(b[i+n : i+n+x])
i += n + x
}
if field.name < name {
// insert before
i = s
break
}
if name == field.name {
if field.Value().IsZero() {
// delete
return List{delfield(b, s, i)}
}
prev := bfield(name, kind, data)
if prev.Value().Equals(field.Value()) {
// no change
return fields
}
// replace
return List{putfield(b, field, s, i)}
}
}
if field.Value().IsZero() {
return fields
}
// insert after
return List{putfield(b, field, i, i)}
}
func delfield(b []byte, s, e int) *byte {
totallen := s + (len(b) - e)
if totallen == 0 {
return nil
}
var psz [10]byte
pn := binary.PutUvarint(psz[:], uint64(totallen))
plen := pn + totallen
p := make([]byte, plen)
// copy each component
i := 0
// -- header size
copy(p[i:], psz[:pn])
i += pn
// -- head entries
copy(p[i:], b[:s])
i += s
// -- tail entries
copy(p[i:], b[e:])
return &p[0]
}
func putfield(b []byte, f Field, s, e int) *byte {
name := f.Name()
var namesz [10]byte
var namen int
if useSharedNames {
num := sstring.Store(name)
namen = binary.PutUvarint(namesz[:], uint64(num))
} else {
namen = binary.PutUvarint(namesz[:], uint64(len(name)))
}
value := f.Value()
kind := value.Kind()
isdatakind := datakind(kind)
var data string
var datasz [10]byte
var datan int
if isdatakind {
data = value.Data()
datan = binary.PutUvarint(datasz[:], uint64(len(data)))
}
var totallen int
if useSharedNames {
totallen = s + namen + 1 + (len(b) - e)
} else {
totallen = s + namen + len(name) + 1 + +(len(b) - e)
}
if isdatakind {
totallen += datan + len(data)
}
var psz [10]byte
pn := binary.PutUvarint(psz[:], uint64(totallen))
plen := pn + totallen
p := make([]byte, plen)
// copy each component
i := 0
// -- header size
copy(p[i:], psz[:pn])
i += pn
// -- head entries
copy(p[i:], b[:s])
i += s
// -- name
copy(p[i:], namesz[:namen])
i += namen
if !useSharedNames {
copy(p[i:], name)
i += len(name)
}
// -- kind
p[i] = byte(kind)
i++
if isdatakind {
// -- data
copy(p[i:], datasz[:datan])
i += datan
copy(p[i:], data)
i += len(data)
}
// -- tail entries
copy(p[i:], b[e:])
return &p[0]
}
// Get a field from the list. Or returns ZeroField if not found.
func (fields List) Get(name string) Field {
var isj bool
var jname string
var jpath string
dot := strings.IndexByte(name, '.')
if dot != -1 {
isj = true
jname = name[:dot]
jpath = name[dot+1:]
}
b := ptob(fields.p)
var i int
for {
// read the fname
var fname string
x, n := uvarint(b[i:])
if n == 0 {
break
}
if useSharedNames {
fname = sstring.Load(x)
i += n
} else {
fname = btoa(b[i+n : i+n+x])
i += n + x
}
kind := Kind(b[i])
i++
var data string
if datakind(kind) {
x, n = uvarint(b[i:])
data = btoa(b[i+n : i+n+x])
i += n + x
}
if kind == JSON && isj {
if jname < fname {
break
}
if fname == jname {
res := gjson.Get(data, jpath)
if res.Exists() {
return bfield(name, Kind(res.Type), res.String())
}
}
} else {
if name < fname {
break
}
if fname == name {
return bfield(name, kind, data)
}
}
}
return ZeroField
}
// Scan each field in list
func (fields List) Scan(iter func(field Field) bool) {
b := ptob(fields.p)
var i int
for {
// read the fname
var fname string
x, n := uvarint(b[i:])
if n == 0 {
break
}
if useSharedNames {
fname = sstring.Load(x)
i += n
} else {
fname = btoa(b[i+n : i+n+x])
i += n + x
}
kind := Kind(b[i])
i++
var data string
if datakind(kind) {
x, n = uvarint(b[i:])
data = btoa(b[i+n : i+n+x])
i += n + x
}
if !iter(bfield(fname, kind, data)) {
return
}
}
}
// Len return the number of fields in list.
func (fields List) Len() int {
var count int
b := ptob(fields.p)
var i int
for {
x, n := uvarint(b[i:])
if n == 0 {
break
}
if useSharedNames {
i += n
} else {
i += n + x
}
isdatakind := datakind(Kind(b[i]))
i++
if isdatakind {
x, n = uvarint(b[i:])
i += n + x
}
count++
}
return count
}
// Weight is the number of bytes of the list.
func (fields List) Weight() int {
if fields.p == nil {
return 0
}
x, n := uvarint(*(*[]byte)(unsafe.Pointer(&bytes{fields.p, 10, 10})))
return x + n
}
// MakeList returns a field list from an array of fields.
func MakeList(fields []Field) List {
// TODO: optimize to reduce allocations.
var list List
for _, f := range fields {
list = list.Set(f)
}
return list
}
func (fields List) String() string {
var dst []byte
dst = append(dst, '{')
var i int
fields.Scan(func(f Field) bool {
if i > 0 {
dst = append(dst, ',')
}
dst = gjson.AppendJSONString(dst, f.Name())
dst = append(dst, ':')
dst = append(dst, f.Value().JSON()...)
i++
return true
})
dst = append(dst, '}')
return string(pretty.UglyInPlace(dst))
}
================================================
FILE: internal/field/list_struct.go
================================================
//go:build exclude
package field
type List struct {
entries []Field
}
// bsearch searches array for value.
func (fields List) bsearch(name string) (index int, found bool) {
i, j := 0, len(fields.entries)
for i < j {
h := i + (j-i)/2
if name >= fields.entries[h].name {
i = h + 1
} else {
j = h
}
}
if i > 0 && fields.entries[i-1].name >= name {
return i - 1, true
}
return i, false
}
func (fields List) Set(field Field) List {
var updated List
index, found := fields.bsearch(field.name)
if found {
if field.value.IsZero() {
// delete
if len(fields.entries) > 1 {
updated.entries = make([]Field, len(fields.entries)-1)
copy(updated.entries, fields.entries[:index])
copy(updated.entries[index:], fields.entries[index+1:])
}
} else if !fields.entries[index].value.Equals(field.value) {
// update
updated.entries = make([]Field, len(fields.entries))
copy(updated.entries, fields.entries)
updated.entries[index].value = field.value
} else {
// nothing changes
updated = fields
}
return updated
}
if field.Value().IsZero() {
return fields
}
updated.entries = make([]Field, len(fields.entries)+1)
copy(updated.entries, fields.entries[:index])
copy(updated.entries[index+1:], fields.entries[index:])
updated.entries[index] = field
return updated
}
func (fields List) Get(name string) Field {
index, found := fields.bsearch(name)
if !found {
return ZeroField
}
return fields.entries[index]
}
func (fields List) Scan(iter func(field Field) bool) {
for _, f := range fields.entries {
if !iter(f) {
return
}
}
}
func (fields List) Len() int {
return len(fields.entries)
}
func (fields List) Weight() int {
var weight int
for _, f := range fields.entries {
weight += f.Weight()
}
return weight
}
================================================
FILE: internal/field/list_test.go
================================================
package field
import (
"fmt"
"math/rand"
"testing"
"time"
"github.com/tidwall/assert"
"github.com/tidwall/btree"
)
func TestList(t *testing.T) {
var fields List
fields = fields.Set(Make("hello", "123"))
assert.Assert(fields.Len() == 1)
// println(fields.Weight())
// assert.Assert(fields.Weight() == 16)
fields = fields.Set(Make("jello", "456"))
assert.Assert(fields.Len() == 2)
// assert.Assert(fields.Weight() == 32)
value := fields.Get("jello")
assert.Assert(value.Value().Data() == "456")
assert.Assert(value.Value().JSON() == "456")
assert.Assert(value.Value().Num() == 456)
value = fields.Get("nello")
assert.Assert(value.Name() == "")
assert.Assert(value.Value().IsZero())
fields = fields.Set(Make("jello", "789"))
assert.Assert(fields.Len() == 2)
// assert.Assert(fields.Weight() == 32)
fields = fields.Set(Make("nello", "0"))
assert.Assert(fields.Len() == 2)
// assert.Assert(fields.Weight() == 32)
fields = fields.Set(Make("jello", "789"))
assert.Assert(fields.Len() == 2)
// assert.Assert(fields.Weight() == 32)
fields = fields.Set(Make("jello", "0"))
assert.Assert(fields.Len() == 1)
// assert.Assert(fields.Weight() == 16)
fields = fields.Set(Make("nello", "012"))
fields = fields.Set(Make("hello", "456"))
fields = fields.Set(Make("fello", "123"))
fields = fields.Set(Make("jello", "789"))
var names string
var datas string
var nums float64
fields.Scan(func(f Field) bool {
names += f.Name()
datas += f.Value().Data()
nums += f.Value().Num()
return true
})
assert.Assert(names == "fellohellojellonello")
assert.Assert(datas == "123456789012")
assert.Assert(nums == 1368)
names = ""
datas = ""
nums = 0
fields.Scan(func(f Field) bool {
names += f.Name()
datas += f.Value().Data()
nums += f.Value().Num()
return false
})
assert.Assert(names == "fello")
assert.Assert(datas == "123")
assert.Assert(nums == 123)
}
func randStr(n int) string {
b := make([]byte, n)
rand.Read(b)
for i := 0; i < n; i++ {
b[i] = 'a' + b[i]%26
}
return string(b)
}
func randVal(n int) string {
switch rand.Intn(10) {
case 0:
return "null"
case 1:
return "true"
case 2:
return "false"
case 3:
return `{"a":"` + randStr(n) + `"}`
case 4:
return `["` + randStr(n) + `"]`
case 5:
return `"` + randStr(n) + `"`
case 6:
return randStr(n)
default:
return fmt.Sprintf("%f", rand.Float64()*360)
}
}
func TestRandom(t *testing.T) {
seed := time.Now().UnixNano()
// seed = 1663607868546669000
rand.Seed(seed)
start := time.Now()
var total int
for time.Since(start) < time.Second*2 {
N := rand.Intn(500)
var org []Field
var tr btree.Map[string, Field]
var fields List
for i := 0; i < N; i++ {
name := randStr(rand.Intn(10))
value := randVal(rand.Intn(10))
field := Make(name, value)
org = append(org, field)
fields = fields.Set(field)
v := fields.Get(name)
// println(name, v.Value().Data(), field.Value().Data())
if !v.Value().Equals(field.Value()) {
t.Fatalf("seed: %d, expected true", seed)
}
tr.Set(name, field)
if fields.Len() != tr.Len() {
t.Fatalf("seed: %d, expected %d, got %d",
seed, tr.Len(), fields.Len())
}
}
comp := func() {
var all []Field
fields.Scan(func(f Field) bool {
all = append(all, f)
return true
})
if len(all) != fields.Len() {
t.Fatalf("seed: %d, expected %d, got %d",
seed, fields.Len(), len(all))
}
if fields.Len() != tr.Len() {
t.Fatalf("seed: %d, expected %d, got %d",
seed, tr.Len(), fields.Len())
}
var i int
tr.Scan(func(name string, f Field) bool {
if name != f.Name() || all[i].Name() != f.Name() {
t.Fatalf("seed: %d, out of order", seed)
}
i++
return true
})
}
comp()
rand.Shuffle(len(org), func(i, j int) {
org[i], org[j] = org[j], org[i]
})
for _, f := range org {
comp()
tr.Delete(f.Name())
fields = fields.Set(Make(f.Name(), "0"))
if fields.Len() != tr.Len() {
t.Fatalf("seed: %d, expected %d, got %d",
seed, tr.Len(), fields.Len())
}
comp()
}
total++
}
}
func TestJSONGet(t *testing.T) {
var list List
list = list.Set(Make("hello", "world"))
list = list.Set(Make("hello", `"world"`))
list = list.Set(Make("jello", "planet"))
list = list.Set(Make("telly", `{"a":[1,2,3],"b":null,"c":true,"d":false}`))
list = list.Set(Make("belly", `{"a":{"b":{"c":"fancy"}}}`))
json := list.String()
exp := `{"belly":{"a":{"b":{"c":"fancy"}}},"hello":"world","jello":` +
`"planet","telly":{"a":[1,2,3],"b":null,"c":true,"d":false}}`
if json != exp {
t.Fatalf("expected '%s', got '%s'", exp, json)
}
data := list.Get("hello").Value().Data()
if data != "world" {
t.Fatalf("expected '%s', got '%s'", "world", data)
}
data = list.Get("telly").Value().Data()
if data != `{"a":[1,2,3],"b":null,"c":true,"d":false}` {
t.Fatalf("expected '%s', got '%s'",
`{"a":[1,2,3],"b":null,"c":true,"d":false}`, data)
}
data = list.Get("belly").Value().Data()
if data != `{"a":{"b":{"c":"fancy"}}}` {
t.Fatalf("expected '%s', got '%s'",
`{"a":{"b":{"c":"fancy"}}}`, data)
}
data = list.Get("belly.a").Value().Data()
if data != `{"b":{"c":"fancy"}}` {
t.Fatalf("expected '%s', got '%s'",
`{"b":{"c":"fancy"}}`, data)
}
data = list.Get("belly.a.b").Value().Data()
if data != `{"c":"fancy"}` {
t.Fatalf("expected '%s', got '%s'",
`{"c":"fancy"}`, data)
}
data = list.Get("belly.a.b.c").Value().Data()
if data != `fancy` {
t.Fatalf("expected '%s', got '%s'",
`fancy`, data)
}
// Tile38 defaults non-existent fields to zero.
data = list.Get("belly.a.b.c.d").Value().Data()
if data != `0` {
t.Fatalf("expected '%s', got '%s'",
`0`, data)
}
}
================================================
FILE: internal/glob/glob.go
================================================
package glob
import "strings"
// Glob structure for simple string matching
type Glob struct {
Pattern string
Desc bool
Limits []string
IsGlob bool
}
// Match returns true when string matches pattern. Returns an error when the
// pattern is invalid.
func Match(pattern, str string) (matched bool, err error) {
return wildcardMatch(pattern, str)
}
// IsGlob returns true when the pattern is a valid glob
func IsGlob(pattern string) bool {
for i := 0; i < len(pattern); i++ {
switch pattern[i] {
case '[', '*', '?':
_, err := Match(pattern, "whatever")
return err == nil
}
}
return false
}
// Parse returns a glob structure from the pattern.
func Parse(pattern string, desc bool) *Glob {
g := &Glob{Pattern: pattern, Desc: desc, Limits: []string{"", ""}}
if strings.HasPrefix(pattern, "*") {
g.IsGlob = true
return g
}
if pattern == "" {
g.IsGlob = false
return g
}
n := 0
isGlob := false
outer:
for i := 0; i < len(pattern); i++ {
switch pattern[i] {
case '[', '*', '?':
_, err := Match(pattern, "whatever")
if err == nil {
isGlob = true
}
break outer
}
n++
}
if n == 0 {
g.Limits = []string{pattern, pattern}
g.IsGlob = false
return g
}
var a, b string
if desc {
a = pattern[:n]
b = a
if b[n-1] == 0x00 {
for len(b) > 0 && b[len(b)-1] == 0x00 {
if len(b) > 1 {
if b[len(b)-2] == 0x00 {
b = b[:len(b)-1]
} else {
b = string(append([]byte(b[:len(b)-2]), b[len(b)-2]-1, 0xFF))
}
} else {
b = ""
}
}
} else {
b = string(append([]byte(b[:n-1]), b[n-1]-1))
}
if a[n-1] == 0xFF {
a = string(append([]byte(a), 0x00))
} else {
a = string(append([]byte(a[:n-1]), a[n-1]+1))
}
} else {
a = pattern[:n]
if a[n-1] == 0xFF {
b = string(append([]byte(a), 0x00))
} else {
b = string(append([]byte(a[:n-1]), a[n-1]+1))
}
}
g.Limits = []string{a, b}
g.IsGlob = isGlob
return g
}
================================================
FILE: internal/glob/glob_test.go
================================================
package glob
import (
"math/rand"
"testing"
"time"
)
func test(t *testing.T, pattern string, desc bool, limitsExpect []string, isGlobExpect bool) {
g := Parse(pattern, desc)
if g.IsGlob != isGlobExpect {
t.Fatalf("pattern[%v] desc[%v] (isGlob=%v, expected=%v)", pattern, desc, g.IsGlob, isGlobExpect)
}
if g.Limits[0] != limitsExpect[0] || g.Limits[1] != limitsExpect[1] {
t.Fatalf("pattern[%v] desc[%v] (limits=%v, expected=%v)", pattern, desc, g.Limits, limitsExpect)
}
if g.Pattern != pattern {
t.Fatalf("pattern[%v] desc[%v] (pattern=%v, expected=%v)", pattern, desc, g.Pattern, pattern)
}
if g.Desc != desc {
t.Fatalf("pattern[%v] desc[%v] (desc=%v, expected=%v)", pattern, desc, g.Desc, desc)
}
}
func TestGlob(t *testing.T) {
test(t, "*", false, []string{"", ""}, true)
test(t, "", false, []string{"", ""}, false)
test(t, "hello*", false, []string{"hello", "hellp"}, true)
test(t, "hello", false, []string{"hello", "hellp"}, false)
test(t, "\xff*", false, []string{"\xff", "\xff\x00"}, true)
test(t, "\x00*", false, []string{"\x00", "\x01"}, true)
test(t, "\xff", false, []string{"\xff", "\xff\x00"}, false)
test(t, "*", true, []string{"", ""}, true)
test(t, "", true, []string{"", ""}, false)
test(t, "hello*", true, []string{"hellp", "helln"}, true)
test(t, "hello", true, []string{"hellp", "helln"}, false)
test(t, "a\xff*", true, []string{"a\xff\x00", "a\xfe"}, true)
test(t, "\x00*", true, []string{"\x01", ""}, true)
test(t, "\x01*", true, []string{"\x02", "\x00"}, true)
test(t, "b\x00*", true, []string{"b\x01", "a\xff"}, true)
test(t, "\x00\x00*", true, []string{"\x00\x01", ""}, true)
test(t, "\x00\x01\x00*", true, []string{"\x00\x01\x01", "\x00\x00\xff"}, true)
}
func testMatch(s, pattern string) bool {
ok, _ := Match(pattern, s)
return ok
}
func TestMatch(t *testing.T) {
if !testMatch("hello world", "hello world") {
t.Fatal("fail")
}
if testMatch("hello world", "jello world") {
t.Fatal("fail")
}
if !testMatch("hello world", "hello*") {
t.Fatal("fail")
}
if testMatch("hello world", "jello*") {
t.Fatal("fail")
}
if !testMatch("hello world", "hello?world") {
t.Fatal("fail")
}
if testMatch("hello world", "jello?world") {
t.Fatal("fail")
}
if !testMatch("hello world", "he*o?world") {
t.Fatal("fail")
}
if !testMatch("hello world", "he*o?wor*") {
t.Fatal("fail")
}
if !testMatch("hello world", "he*o?*r*") {
t.Fatal("fail")
}
if !testMatch("的情况下解析一个", "*") {
t.Fatal("fail")
}
if !testMatch("的情况下解析一个", "*况下*") {
t.Fatal("fail")
}
if !testMatch("的情况下解析一个", "*况?*") {
t.Fatal("fail")
}
if !testMatch("的情况下解析一个", "的情况?解析一个") {
t.Fatal("fail")
}
}
// TestWildcardMatch - Tests validate the logic of wild card matching.
// `WildcardMatch` supports '*' and '?' wildcards.
// Sample usage: In resource matching for folder policy validation.
func TestWildcardMatch(t *testing.T) {
testCases := []struct {
pattern string
text string
matched bool
}{
// Test case - 1.
// Test case with pattern containing key name with a prefix. Should accept the same text without a "*".
{
pattern: "my-folder/oo*",
text: "my-folder/oo",
matched: true,
},
// Test case - 2.
// Test case with "*" at the end of the pattern.
{
pattern: "my-folder/In*",
text: "my-folder/India/Karnataka/",
matched: true,
},
// Test case - 3.
// Test case with prefixes shuffled.
// This should fail.
{
pattern: "my-folder/In*",
text: "my-folder/Karnataka/India/",
matched: false,
},
// Test case - 4.
// Test case with text expanded to the wildcards in the pattern.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Ban",
matched: true,
},
// Test case - 5.
// Test case with the keyname part is repeated as prefix several times.
// This is valid.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Ban/Ban/Ban/Ban/Ban",
matched: true,
},
// Test case - 6.
// Test case to validate that `*` can be expanded into multiple prefixes.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Area1/Area2/Area3/Ban",
matched: true,
},
// Test case - 7.
// Test case to validate that `*` can be expanded into multiple prefixes.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/State1/State2/Karnataka/Area1/Area2/Area3/Ban",
matched: true,
},
// Test case - 8.
// Test case where the keyname part of the pattern is expanded in the text.
{
pattern: "my-folder/In*/Ka*/Ban",
text: "my-folder/India/Karnataka/Bangalore",
matched: false,
},
// Test case - 9.
// Test case with prefixes and wildcard expanded for all "*".
{
pattern: "my-folder/In*/Ka*/Ban*",
text: "my-folder/India/Karnataka/Bangalore",
matched: true,
},
// Test case - 10.
// Test case with keyname part being a wildcard in the pattern.
{pattern: "my-folder/*",
text: "my-folder/India",
matched: true,
},
// Test case - 11.
{
pattern: "my-folder/oo*",
text: "my-folder/odo",
matched: false,
},
// Test case with pattern containing wildcard '?'.
// Test case - 12.
// "my-folder?/" matches "my-folder1/", "my-folder2/", "my-folder3" etc...
// doesn't match "myfolder/".
{
pattern: "my-folder?/abc*",
text: "myfolder/abc",
matched: false,
},
// Test case - 13.
{
pattern: "my-folder?/abc*",
text: "my-folder1/abc",
matched: true,
},
// Test case - 14.
{
pattern: "my-?-folder/abc*",
text: "my--folder/abc",
matched: false,
},
// Test case - 15.
{
pattern: "my-?-folder/abc*",
text: "my-1-folder/abc",
matched: true,
},
// Test case - 16.
{
pattern: "my-?-folder/abc*",
text: "my-k-folder/abc",
matched: true,
},
// Test case - 17.
{
pattern: "my??folder/abc*",
text: "myfolder/abc",
matched: false,
},
// Test case - 18.
{
pattern: "my??folder/abc*",
text: "my4afolder/abc",
matched: true,
},
// Test case - 19.
{
pattern: "my-folder?abc*",
text: "my-folder/abc",
matched: true,
},
// Test case 20-21.
// '?' matches '/' too. (works with s3).
// This is because the namespace is considered flat.
// "abc?efg" matches both "abcdefg" and "abc/efg".
{
pattern: "my-folder/abc?efg",
text: "my-folder/abcdefg",
matched: true,
},
{
pattern: "my-folder/abc?efg",
text: "my-folder/abc/efg",
matched: true,
},
// Test case - 22.
{
pattern: "my-folder/abc????",
text: "my-folder/abc",
matched: false,
},
// Test case - 23.
{
pattern: "my-folder/abc????",
text: "my-folder/abcde",
matched: false,
},
// Test case - 24.
{
pattern: "my-folder/abc????",
text: "my-folder/abcdefg",
matched: true,
},
// Test case 25-26.
// test case with no '*'.
{
pattern: "my-folder/abc?",
text: "my-folder/abc",
matched: false,
},
{
pattern: "my-folder/abc?",
text: "my-folder/abcd",
matched: true,
},
{
pattern: "my-folder/abc?",
text: "my-folder/abcde",
matched: false,
},
// Test case 27.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnop",
matched: false,
},
// Test case 28.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqr",
matched: true,
},
// Test case 29.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqrs",
matched: true,
},
// Test case 30.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnop",
matched: false,
},
// Test case 31.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopq",
matched: true,
},
// Test case 32.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqr",
matched: true,
},
// Test case 33.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 34.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopand",
matched: false,
},
// Test case 35.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 36.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mn",
matched: false,
},
// Test case 37.
{
pattern: "my-folder/mnop*?",
text: "my-folder/mnopqrst/mnopqrs",
matched: true,
},
// Test case 38.
{
pattern: "my-folder/mnop*??",
text: "my-folder/mnopqrst",
matched: true,
},
// Test case 39.
{
pattern: "my-folder/mnop*qrst",
text: "my-folder/mnopabcdegqrst",
matched: true,
},
// Test case 40.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqand",
matched: true,
},
// Test case 41.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopand",
matched: false,
},
// Test case 42.
{
pattern: "my-folder/mnop*?and?",
text: "my-folder/mnopqanda",
matched: true,
},
// Test case 43.
{
pattern: "my-folder/mnop*?and",
text: "my-folder/mnopqanda",
matched: false,
},
// Test case 44.
{
pattern: "my-?-folder/abc*",
text: "my-folder/mnopqanda",
matched: false,
},
}
// Iterating over the test cases, call the function under test and asert the output.
for i, testCase := range testCases {
actualResult := testMatch(testCase.text, testCase.pattern)
if testCase.matched != actualResult {
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult)
}
}
}
func TestRandomInput(t *testing.T) {
rand.Seed(time.Now().UnixNano())
b1 := make([]byte, 100)
b2 := make([]byte, 100)
for i := 0; i < 1000000; i++ {
if _, err := rand.Read(b1); err != nil {
t.Fatal(err)
}
if _, err := rand.Read(b2); err != nil {
t.Fatal(err)
}
testMatch(string(b1), string(b2))
}
}
func BenchmarkAscii(t *testing.B) {
for i := 0; i < t.N; i++ {
if !testMatch("hello", "hello") {
t.Fatal("fail")
}
}
}
func BenchmarkUnicode(t *testing.B) {
for i := 0; i < t.N; i++ {
if !testMatch("h情llo", "h情llo") {
t.Fatal("fail")
}
}
}
================================================
FILE: internal/glob/match.go
================================================
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package glob
import (
"errors"
"unicode/utf8"
)
// ErrBadPattern indicates a globbing pattern was malformed.
var ErrBadPattern = errors.New("syntax error in pattern")
// Match reports whether name matches the shell file name pattern.
// The pattern syntax is:
//
// pattern:
// { term }
// term:
// '*' matches any sequence of non-Separator characters
// '?' matches any single non-Separator character
// '[' [ '^' ] { character-range } ']'
// character class (must be non-empty)
// c matches character c (c != '*', '?', '\\', '[')
// '\\' c matches character c
//
// character-range:
// c matches character c (c != '\\', '-', ']')
// '\\' c matches character c
// lo '-' hi matches character c for lo <= c <= hi
//
// Match requires pattern to match all of name, not just a substring.
// The only possible returned error is ErrBadPattern, when pattern
// is malformed.
//
// On Windows, escaping is disabled. Instead, '\\' is treated as
// path separator.
func wildcardMatch(pattern, name string) (matched bool, err error) {
Pattern:
for len(pattern) > 0 {
var star bool
var chunk string
star, chunk, pattern = scanChunk(pattern)
if star && chunk == "" {
// Trailing * matches rest of string unless it has a /.
return true, nil
}
// Look for match at current position.
t, ok, err := matchChunk(chunk, name)
// if we're the last chunk, make sure we've exhausted the name
// otherwise we'll give a false result even if we could still match
// using the star
if ok && (len(t) == 0 || len(pattern) > 0) {
name = t
continue
}
if err != nil {
return false, err
}
if star {
// Look for match skipping i+1 bytes.
// Cannot skip /.
for i := 0; i < len(name); i++ {
t, ok, err := matchChunk(chunk, name[i+1:])
if ok {
// if we're the last chunk, make sure we exhausted the name
if len(pattern) == 0 && len(t) > 0 {
continue
}
name = t
continue Pattern
}
if err != nil {
return false, err
}
}
}
return false, nil
}
return len(name) == 0, nil
}
// scanChunk gets the next segment of pattern, which is a non-star string
// possibly preceded by a star.
func scanChunk(pattern string) (star bool, chunk, rest string) {
for len(pattern) > 0 && pattern[0] == '*' {
pattern = pattern[1:]
star = true
}
inrange := false
var i int
Scan:
for i = 0; i < len(pattern); i++ {
switch pattern[i] {
case '\\':
// error check handled in matchChunk: bad pattern.
if i+1 < len(pattern) {
i++
}
case '[':
inrange = true
case ']':
inrange = false
case '*':
if !inrange {
break Scan
}
}
}
return star, pattern[0:i], pattern[i:]
}
// matchChunk checks whether chunk matches the beginning of s.
// If so, it returns the remainder of s (after the match).
// Chunk is all single-character operators: literals, char classes, and ?.
func matchChunk(chunk, s string) (rest string, ok bool, err error) {
for len(chunk) > 0 {
if len(s) == 0 {
return
}
switch chunk[0] {
case '[':
// character class
r, n := utf8.DecodeRuneInString(s)
s = s[n:]
chunk = chunk[1:]
// We can't end right after '[', we're expecting at least
// a closing bracket and possibly a caret.
if len(chunk) == 0 {
err = ErrBadPattern
return
}
// possibly negated
negated := chunk[0] == '^'
if negated {
chunk = chunk[1:]
}
// parse all ranges
match := false
nrange := 0
for {
if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 {
chunk = chunk[1:]
break
}
var lo, hi rune
if lo, chunk, err = getEsc(chunk); err != nil {
return
}
hi = lo
if chunk[0] == '-' {
if hi, chunk, err = getEsc(chunk[1:]); err != nil {
return
}
}
if lo <= r && r <= hi {
match = true
}
nrange++
}
if match == negated {
return
}
case '?':
_, n := utf8.DecodeRuneInString(s)
s = s[n:]
chunk = chunk[1:]
case '\\':
chunk = chunk[1:]
if len(chunk) == 0 {
err = ErrBadPattern
return
}
fallthrough
default:
if chunk[0] != s[0] {
return
}
s = s[1:]
chunk = chunk[1:]
}
}
return s, true, nil
}
// getEsc gets a possibly-escaped character from chunk, for a character class.
func getEsc(chunk string) (r rune, nchunk string, err error) {
if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' {
err = ErrBadPattern
return
}
if chunk[0] == '\\' {
chunk = chunk[1:]
if len(chunk) == 0 {
err = ErrBadPattern
return
}
}
r, n := utf8.DecodeRuneInString(chunk)
if r == utf8.RuneError && n == 1 {
err = ErrBadPattern
}
nchunk = chunk[n:]
if len(nchunk) == 0 {
err = ErrBadPattern
}
return
}
================================================
FILE: internal/hservice/gen.sh
================================================
#!/bin/bash
cd $(dirname "${BASH_SOURCE[0]}")
protoc --go_out=plugins=grpc,import_path=hservice:. *.proto
================================================
FILE: internal/hservice/hservice.pb.go
================================================
// Code generated by protoc-gen-go.
// source: hservice.proto
// DO NOT EDIT!
/*
Package hservice is a generated protocol buffer package.
It is generated from these files:
hservice.proto
It has these top-level messages:
MessageRequest
MessageReply
*/
package hservice
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the message value
type MessageRequest struct {
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
}
func (m *MessageRequest) Reset() { *m = MessageRequest{} }
func (m *MessageRequest) String() string { return proto.CompactTextString(m) }
func (*MessageRequest) ProtoMessage() {}
func (*MessageRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
// The response message containing an ok (true or false)
type MessageReply struct {
Ok bool `protobuf:"varint,1,opt,name=ok" json:"ok,omitempty"`
}
func (m *MessageReply) Reset() { *m = MessageReply{} }
func (m *MessageReply) String() string { return proto.CompactTextString(m) }
func (*MessageReply) ProtoMessage() {}
func (*MessageReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func init() {
proto.RegisterType((*MessageRequest)(nil), "hservice.MessageRequest")
proto.RegisterType((*MessageReply)(nil), "hservice.MessageReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion3
// Client API for HookService service
type HookServiceClient interface {
// Sends a greeting
Send(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*MessageReply, error)
}
type hookServiceClient struct {
cc *grpc.ClientConn
}
func NewHookServiceClient(cc *grpc.ClientConn) HookServiceClient {
return &hookServiceClient{cc}
}
func (c *hookServiceClient) Send(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*MessageReply, error) {
out := new(MessageReply)
err := grpc.Invoke(ctx, "/hservice.HookService/Send", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for HookService service
type HookServiceServer interface {
// Sends a greeting
Send(context.Context, *MessageRequest) (*MessageReply, error)
}
func RegisterHookServiceServer(s *grpc.Server, srv HookServiceServer) {
s.RegisterService(&_HookService_serviceDesc, srv)
}
func _HookService_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MessageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HookServiceServer).Send(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/hservice.HookService/Send",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HookServiceServer).Send(ctx, req.(*MessageRequest))
}
return interceptor(ctx, in, info, handler)
}
var _HookService_serviceDesc = grpc.ServiceDesc{
ServiceName: "hservice.HookService",
HandlerType: (*HookServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Send",
Handler: _HookService_Send_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: fileDescriptor0,
}
func init() { proto.RegisterFile("hservice.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 168 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0xcb, 0x28, 0x4e, 0x2d,
0x2a, 0xcb, 0x4c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x80, 0xf1, 0x95, 0xd4,
0xb8, 0xf8, 0x7c, 0x53, 0x8b, 0x8b, 0x13, 0xd3, 0x53, 0x83, 0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b,
0x84, 0x44, 0xb8, 0x58, 0xcb, 0x12, 0x73, 0x4a, 0x53, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83,
0x20, 0x1c, 0x25, 0x39, 0x2e, 0x1e, 0xb8, 0xba, 0x82, 0x9c, 0x4a, 0x21, 0x3e, 0x2e, 0xa6, 0xfc,
0x6c, 0xb0, 0x12, 0x8e, 0x20, 0xa6, 0xfc, 0x6c, 0x23, 0x4f, 0x2e, 0x6e, 0x8f, 0xfc, 0xfc, 0xec,
0x60, 0x88, 0xb1, 0x42, 0x56, 0x5c, 0x2c, 0xc1, 0xa9, 0x79, 0x29, 0x42, 0x12, 0x7a, 0x70, 0x9b,
0x51, 0xad, 0x91, 0x12, 0xc3, 0x22, 0x53, 0x90, 0x53, 0xa9, 0xc4, 0xe0, 0xa4, 0xc9, 0x25, 0x9c,
0x9c, 0x9f, 0xab, 0x57, 0x92, 0x99, 0x93, 0x6a, 0x6c, 0x01, 0x57, 0xe5, 0x24, 0x80, 0x64, 0x7e,
0x00, 0xc8, 0x17, 0x01, 0x8c, 0x49, 0x6c, 0x60, 0xef, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff,
0x6d, 0xd0, 0x2b, 0x13, 0xe0, 0x00, 0x00, 0x00,
}
================================================
FILE: internal/hservice/hservice.proto
================================================
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.tile38.hservice";
option java_outer_classname = "HookServiceProto";
package hservice;
// The greeting service definition.
service HookService {
// Sends a greeting
rpc Send (MessageRequest) returns (MessageReply) {}
}
// The request message containing the message value
message MessageRequest {
string value = 1;
}
// The response message containing an ok (true or false)
message MessageReply {
bool ok = 1;
}
================================================
FILE: internal/log/log.go
================================================
package log
import (
"encoding/json"
"fmt"
"io"
"os"
"sync"
"sync/atomic"
"time"
"go.uber.org/zap"
"golang.org/x/term"
)
var wmu sync.Mutex
var wr io.Writer
var zmu sync.Mutex
var zlogger *zap.SugaredLogger
var tty atomic.Bool
var ljson atomic.Bool
var llevel atomic.Int32
func init() {
SetOutput(os.Stderr)
SetLevel(1)
}
// Level is the log level
// 0: silent - do not log
// 1: normal - show everything except debug and warn
// 2: verbose - show everything except debug
// 3: very verbose - show everything
func SetLevel(level int) {
if level < 0 {
level = 0
} else if level > 3 {
level = 3
}
llevel.Store(int32(level))
}
// Level returns the log level
func Level() int {
return int(llevel.Load())
}
func SetLogJSON(logJSON bool) {
ljson.Store(logJSON)
}
func LogJSON() bool {
return ljson.Load()
}
// SetOutput sets the output of the logger
func SetOutput(w io.Writer) {
f, ok := w.(*os.File)
tty.Store(ok && term.IsTerminal(int(f.Fd())))
wmu.Lock()
wr = w
wmu.Unlock()
}
// Build a zap logger from default or custom config
func Build(c string) error {
var zcfg zap.Config
if c == "" {
zcfg = zap.NewProductionConfig()
// to be able to filter with Tile38 levels
zcfg.Level.SetLevel(zap.DebugLevel)
// disable caller because caller is always log.go
zcfg.DisableCaller = true
} else {
err := json.Unmarshal([]byte(c), &zcfg)
if err != nil {
return err
}
// to be able to filter with Tile38 levels
zcfg.Level.SetLevel(zap.DebugLevel)
// disable caller because caller is always log.go
zcfg.DisableCaller = true
}
core, err := zcfg.Build()
if err != nil {
return err
}
defer core.Sync()
zmu.Lock()
zlogger = core.Sugar()
zmu.Unlock()
return nil
}
// Set a zap logger
func Set(sl *zap.SugaredLogger) {
zmu.Lock()
zlogger = sl
zmu.Unlock()
}
// Get a zap logger
func Get() *zap.SugaredLogger {
zmu.Lock()
sl := zlogger
zmu.Unlock()
return sl
}
// Output returns the output writer
func Output() io.Writer {
wmu.Lock()
defer wmu.Unlock()
return wr
}
func log(level int, tag, color string, formatted bool, format string, args ...interface{}) {
if llevel.Load() < int32(level) {
return
}
var msg string
if formatted {
msg = fmt.Sprintf(format, args...)
} else {
msg = fmt.Sprint(args...)
}
if ljson.Load() {
zmu.Lock()
defer zmu.Unlock()
switch tag {
case "ERRO":
zlogger.Error(msg)
case "FATA":
zlogger.Fatal(msg)
case "WARN":
zlogger.Warn(msg)
case "DEBU":
zlogger.Debug(msg)
default:
zlogger.Info(msg)
}
return
}
s := []byte(time.Now().Format("2006/01/02 15:04:05"))
s = append(s, ' ')
if tty.Load() {
s = append(s, color...)
}
s = append(s, '[')
s = append(s, tag...)
s = append(s, ']')
if tty.Load() {
s = append(s, "\x1b[0m"...)
}
s = append(s, ' ')
s = append(s, msg...)
if s[len(s)-1] != '\n' {
s = append(s, '\n')
}
wmu.Lock()
wr.Write(s)
wmu.Unlock()
}
var emptyFormat string
// Infof ...
func Infof(format string, args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "INFO", "\x1b[36m", true, format, args...)
}
}
// Info ...
func Info(args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "INFO", "\x1b[36m", false, emptyFormat, args...)
}
}
// HTTPf ...
func HTTPf(format string, args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "HTTP", "\x1b[1m\x1b[30m", true, format, args...)
}
}
// HTTP ...
func HTTP(args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "HTTP", "\x1b[1m\x1b[30m", false, emptyFormat, args...)
}
}
// Errorf ...
func Errorf(format string, args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "ERRO", "\x1b[1m\x1b[31m", true, format, args...)
}
}
// Error ..
func Error(args ...interface{}) {
if llevel.Load() >= 1 {
log(1, "ERRO", "\x1b[1m\x1b[31m", false, emptyFormat, args...)
}
}
// Warnf ...
func Warnf(format string, args ...interface{}) {
if llevel.Load() >= 1 {
log(2, "WARN", "\x1b[33m", true, format, args...)
}
}
// Warn ...
func Warn(args ...interface{}) {
if llevel.Load() >= 1 {
log(2, "WARN", "\x1b[33m", false, emptyFormat, args...)
}
}
// Debugf ...
func Debugf(format string, args ...interface{}) {
if llevel.Load() >= 3 {
log(3, "DEBU", "\x1b[35m", true, format, args...)
}
}
// Debug ...
func Debug(args ...interface{}) {
if llevel.Load() >= 3 {
log(3, "DEBU", "\x1b[35m", false, emptyFormat, args...)
}
}
// Printf ...
func Printf(format string, args ...interface{}) {
Infof(format, args...)
}
// Print ...
func Print(args ...interface{}) {
Info(args...)
}
// Fatalf ...
func Fatalf(format string, args ...interface{}) {
log(1, "FATA", "\x1b[31m", true, format, args...)
os.Exit(1)
}
// Fatal ...
func Fatal(args ...interface{}) {
log(1, "FATA", "\x1b[31m", false, emptyFormat, args...)
os.Exit(1)
}
================================================
FILE: internal/log/log_test.go
================================================
package log
import (
"bytes"
"io"
"strings"
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
func TestLog(t *testing.T) {
f := &bytes.Buffer{}
SetLogJSON(false)
SetOutput(f)
Printf("hello %v", "everyone")
if !strings.HasSuffix(f.String(), "hello everyone\n") {
t.Fatal("fail")
}
}
func TestLogJSON(t *testing.T) {
SetLogJSON(true)
Build("")
type tcase struct {
level int
format string
args string
ops func(...interface{})
fops func(string, ...interface{})
expMsg string
expLvl zapcore.Level
}
fn := func(tc tcase) func(*testing.T) {
return func(t *testing.T) {
observedZapCore, observedLogs := observer.New(zap.DebugLevel)
Set(zap.New(observedZapCore).Sugar())
SetLevel(tc.level)
if tc.format != "" {
tc.fops(tc.format, tc.args)
} else {
tc.ops(tc.args)
}
if observedLogs.Len() < 1 {
t.Fatal("fail")
}
allLogs := observedLogs.All()
if allLogs[0].Message != tc.expMsg {
t.Fatal("fail")
}
if allLogs[0].Level != tc.expLvl {
t.Fatal("fail")
}
}
}
tests := map[string]tcase{
"Print": {
level: 1,
args: "Print json logger",
ops: func(args ...interface{}) {
Print(args...)
},
expMsg: "Print json logger",
expLvl: zapcore.InfoLevel,
},
"Printf": {
level: 1,
format: "Printf json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
Printf(format, args...)
},
expMsg: "Printf json logger",
expLvl: zapcore.InfoLevel,
},
"Info": {
level: 1,
args: "Info json logger",
ops: func(args ...interface{}) {
Info(args...)
},
expMsg: "Info json logger",
expLvl: zapcore.InfoLevel,
},
"Infof": {
level: 1,
format: "Infof json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
Infof(format, args...)
},
expMsg: "Infof json logger",
expLvl: zapcore.InfoLevel,
},
"Debug": {
level: 3,
args: "Debug json logger",
ops: func(args ...interface{}) {
Debug(args...)
},
expMsg: "Debug json logger",
expLvl: zapcore.DebugLevel,
},
"Debugf": {
level: 3,
format: "Debugf json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
Debugf(format, args...)
},
expMsg: "Debugf json logger",
expLvl: zapcore.DebugLevel,
},
"Warn": {
level: 2,
args: "Warn json logger",
ops: func(args ...interface{}) {
Warn(args...)
},
expMsg: "Warn json logger",
expLvl: zapcore.WarnLevel,
},
"Warnf": {
level: 2,
format: "Warnf json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
Warnf(format, args...)
},
expMsg: "Warnf json logger",
expLvl: zapcore.WarnLevel,
},
"Error": {
level: 1,
args: "Error json logger",
ops: func(args ...interface{}) {
Error(args...)
},
expMsg: "Error json logger",
expLvl: zapcore.ErrorLevel,
},
"Errorf": {
level: 1,
format: "Errorf json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
Errorf(format, args...)
},
expMsg: "Errorf json logger",
expLvl: zapcore.ErrorLevel,
},
"Http": {
level: 1,
args: "Http json logger",
ops: func(args ...interface{}) {
HTTP(args...)
},
expMsg: "Http json logger",
expLvl: zapcore.InfoLevel,
},
"Httpf": {
level: 1,
format: "Httpf json %v",
args: "logger",
fops: func(format string, args ...interface{}) {
HTTPf(format, args...)
},
expMsg: "Httpf json logger",
expLvl: zapcore.InfoLevel,
},
}
for name, tc := range tests {
t.Run(name, fn(tc))
}
}
func BenchmarkLogPrintf(t *testing.B) {
SetLogJSON(false)
SetLevel(1)
SetOutput(io.Discard)
t.ResetTimer()
for i := 0; i < t.N; i++ {
Printf("X %s", "Y")
}
}
func BenchmarkLogJSONPrintf(t *testing.B) {
SetLogJSON(true)
SetLevel(1)
ec := zap.NewProductionEncoderConfig()
ec.EncodeDuration = zapcore.NanosDurationEncoder
ec.EncodeTime = zapcore.EpochNanosTimeEncoder
enc := zapcore.NewJSONEncoder(ec)
logger := zap.New(
zapcore.NewCore(
enc,
zapcore.AddSync(io.Discard),
zap.DebugLevel,
)).Sugar()
Set(logger)
t.ResetTimer()
for i := 0; i < t.N; i++ {
Printf("X %s", "Y")
}
}
================================================
FILE: internal/object/object_binary.go
================================================
package object
import (
"encoding/binary"
"unsafe"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/tile38/internal/field"
)
type pointObject struct {
base Object
pt geojson.SimplePoint
}
type geoObject struct {
base Object
geo geojson.Object
}
const opoint = 1
const ogeo = 2
type Object struct {
head string // tuple (kind,expires,id)
fields field.List
}
func (o *Object) geo() geojson.Object {
if o != nil {
switch o.head[0] {
case opoint:
return &(*pointObject)(unsafe.Pointer(o)).pt
case ogeo:
return (*geoObject)(unsafe.Pointer(o)).geo
}
}
return nil
}
// uvarint is a slightly modified version of binary.Uvarint, and it's a little
// faster. But it lacks overflow checks which are not needed for our use.
func uvarint(s string) (uint64, int) {
var x uint64
for i := 0; i < len(s); i++ {
b := s[i]
if b < 0x80 {
return x | uint64(b)<<(i*7), i + 1
}
x |= uint64(b&0x7f) << (i * 7)
}
return 0, 0
}
func varint(s string) (int64, int) {
ux, n := uvarint(s)
x := int64(ux >> 1)
if ux&1 != 0 {
x = ^x
}
return x, n
}
func (o *Object) ID() string {
if o.head[1] == 0 {
return o.head[2:]
}
_, n := varint(o.head[1:])
return o.head[1+n:]
}
func (o *Object) Fields() field.List {
return o.fields
}
func (o *Object) Expires() int64 {
ex, _ := varint(o.head[1:])
return ex
}
func (o *Object) Rect() geometry.Rect {
ogeo := o.geo()
if ogeo == nil {
return geometry.Rect{}
}
return ogeo.Rect()
}
func (o *Object) Geo() geojson.Object {
return o.geo()
}
func (o *Object) String() string {
ogeo := o.geo()
if ogeo == nil {
return ""
}
return ogeo.String()
}
func (o *Object) IsSpatial() bool {
_, ok := o.geo().(geojson.Spatial)
return ok
}
func (o *Object) Weight() int {
var weight int
weight += len(o.ID())
ogeo := o.geo()
if ogeo != nil {
if o.IsSpatial() {
weight += ogeo.NumPoints() * 16
} else {
weight += len(ogeo.String())
}
}
weight += o.Fields().Weight()
return weight
}
func makeHead(kind byte, id string, expires int64) string {
var exb [20]byte
exn := 1
if expires != 0 {
exn = binary.PutVarint(exb[:], expires)
}
n := 1 + exn + len(id)
head := make([]byte, n)
head[0] = kind
copy(head[1:], exb[:exn])
copy(head[1+exn:], id)
return *(*string)(unsafe.Pointer(&head))
}
func newPoint(id string, pt geometry.Point, expires int64, fields field.List,
) *Object {
return (*Object)(unsafe.Pointer(&pointObject{
Object{
head: makeHead(opoint, id, expires),
fields: fields,
},
geojson.SimplePoint{Point: pt},
}))
}
func newGeo(id string, geo geojson.Object, expires int64, fields field.List,
) *Object {
return (*Object)(unsafe.Pointer(&geoObject{
Object{
head: makeHead(ogeo, id, expires),
fields: fields,
},
geo,
}))
}
func New(id string, geo geojson.Object, expires int64, fields field.List,
) *Object {
switch p := geo.(type) {
case *geojson.SimplePoint:
return newPoint(id, p.Base(), expires, fields)
case *geojson.Point:
if p.IsSimple() {
return newPoint(id, p.Base(), expires, fields)
}
}
return newGeo(id, geo, expires, fields)
}
================================================
FILE: internal/object/object_struct.go
================================================
//go:build exclude
package object
import (
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/tile38/internal/field"
)
type Object struct {
id string
geo geojson.Object
expires int64 // unix nano expiration
fields field.List
}
func (o *Object) ID() string {
if o == nil {
return ""
}
return o.id
}
func (o *Object) Fields() field.List {
if o == nil {
return field.List{}
}
return o.fields
}
func (o *Object) Expires() int64 {
if o == nil {
return 0
}
return o.expires
}
func (o *Object) Rect() geometry.Rect {
if o == nil || o.geo == nil {
return geometry.Rect{}
}
return o.geo.Rect()
}
func (o *Object) Geo() geojson.Object {
if o == nil || o.geo == nil {
return nil
}
return o.geo
}
func (o *Object) String() string {
if o == nil || o.geo == nil {
return ""
}
return o.geo.String()
}
func (o *Object) IsSpatial() bool {
_, ok := o.geo.(geojson.Spatial)
return ok
}
func (o *Object) Weight() int {
if o == nil {
return 0
}
var weight int
weight += len(o.ID())
if o.IsSpatial() {
weight += o.Geo().NumPoints() * 16
} else {
weight += len(o.Geo().String())
}
weight += o.Fields().Weight()
return weight
}
func New(id string, geo geojson.Object, expires int64, fields field.List,
) *Object {
return &Object{
id: id,
geo: geo,
expires: expires,
fields: fields,
}
}
================================================
FILE: internal/object/object_test.go
================================================
package object
import (
"testing"
"github.com/tidwall/assert"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/tile38/internal/field"
)
func P(x, y float64) geojson.Object {
return geojson.NewSimplePoint(geometry.Point{X: 10, Y: 20})
}
func TestObject(t *testing.T) {
o := New("hello", P(10, 20), 99, field.List{})
assert.Assert(o.ID() == "hello")
}
================================================
FILE: internal/server/aof.go
================================================
package server
import (
"errors"
"fmt"
"io"
"math"
"net"
"os"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/buntdb"
"github.com/tidwall/gjson"
"github.com/tidwall/redcon"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
type errAOFHook struct {
err error
}
func (err errAOFHook) Error() string {
return fmt.Sprintf("hook: %v", err.err)
}
func (s *Server) loadAOF() (err error) {
fi, err := s.aof.Stat()
if err != nil {
return err
}
start := time.Now()
var count int
defer func() {
d := time.Since(start)
ps := float64(count) / (float64(d) / float64(time.Second))
suf := []string{"bytes/s", "KB/s", "MB/s", "GB/s", "TB/s"}
bps := float64(fi.Size()) / (float64(d) / float64(time.Second))
for i := 0; bps > 1024 && len(suf) > 1; i++ {
bps /= 1024
suf = suf[1:]
}
byteSpeed := fmt.Sprintf("%.0f %s", bps, suf[0])
log.Infof("AOF loaded %d commands: %.2fs, %.0f/s, %s",
count, float64(d)/float64(time.Second), ps, byteSpeed)
}()
var buf []byte
var args [][]byte
var packet [0xFFFF]byte
for {
n, err := s.aof.Read(packet[:])
if err != nil {
if err != io.EOF {
return err
}
if len(buf) > 0 {
// There was an incomplete command or other data at the end of
// the AOF file. Attempt to recover the file by truncating the
// file at the end position of the last complete command.
log.Warnf("Truncating %d bytes due to an incomplete command\n",
len(buf))
s.aofsz -= len(buf)
if err := s.aof.Truncate(int64(s.aofsz)); err != nil {
return err
}
if _, err := s.aof.Seek(int64(s.aofsz), 0); err != nil {
return err
}
}
return nil
}
s.aofsz += n
data := packet[:n]
if len(buf) > 0 {
data = append(buf, data...)
}
var complete bool
for {
if len(data) > 0 && data[0] == 0 {
// Zeros found in AOF file (issue #230).
// Just ignore it and move the next byte.
data = data[1:]
continue
}
complete, args, _, data, err = redcon.ReadNextCommand(data, args[:0])
if err != nil {
return err
}
if !complete {
break
}
if len(args) > 0 {
var msg Message
msg.Args = msg.Args[:0]
for _, arg := range args {
msg.Args = append(msg.Args, string(arg))
}
if _, _, err := s.command(&msg, nil); err != nil {
if commandErrIsFatal(err) {
return err
}
}
count++
}
}
if len(data) > 0 {
buf = append(buf[:0], data...)
} else if len(buf) > 0 {
buf = buf[:0]
}
}
}
func commandErrIsFatal(err error) bool {
// FSET (and other writable commands) may return errors that we need
// to ignore during the loading process. These errors may occur (though unlikely)
// due to the aof rewrite operation.
return !(err == errKeyNotFound || err == errIDNotFound)
}
// flushAOF flushes all aof buffer data to disk. Set sync to true to sync the
// fsync the file.
func (s *Server) flushAOF(sync bool) {
if len(s.aofbuf) > 0 {
_, err := s.aof.Write(s.aofbuf)
if err != nil {
panic(err)
}
// send a broadcast to all sleeping followers
s.fcond.Broadcast()
if sync {
if err := s.aof.Sync(); err != nil {
panic(err)
}
}
if cap(s.aofbuf) > 1024*1024*32 {
s.aofbuf = make([]byte, 0, 1024*1024*32)
} else {
s.aofbuf = s.aofbuf[:0]
}
}
}
func (s *Server) writeAOF(args []string, d *commandDetails) error {
if d != nil && !d.updated {
// just ignore writes if the command did not update
return nil
}
if s.shrinking {
nargs := make([]string, len(args))
copy(nargs, args)
s.shrinklog = append(s.shrinklog, nargs)
}
if s.aof != nil {
s.aofdirty.Store(true) // prewrite optimization flag
n := len(s.aofbuf)
s.aofbuf = redcon.AppendArray(s.aofbuf, len(args))
for _, arg := range args {
s.aofbuf = redcon.AppendBulkString(s.aofbuf, arg)
}
s.aofsz += len(s.aofbuf) - n
}
// process geofences
if d != nil {
// webhook geofences
if s.config.followHost() == "" {
// for leader only
if d.parent {
// queue children
for _, d := range d.children {
if err := s.queueHooks(d); err != nil {
return err
}
}
} else {
// queue parent
if err := s.queueHooks(d); err != nil {
return err
}
}
}
// live geofences
s.lcond.L.Lock()
if len(s.lives) > 0 {
if d.parent {
// queue children
s.lstack = append(s.lstack, d.children...)
} else {
// queue parent
s.lstack = append(s.lstack, d)
}
s.lcond.Broadcast()
}
s.lcond.L.Unlock()
}
return nil
}
func (s *Server) getQueueCandidates(d *commandDetails) []*Hook {
candidates := make(map[*Hook]bool)
// add the hooks with "outside" detection
s.hooksOut.Ascend(nil, func(v interface{}) bool {
hook := v.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
// look for candidates that might "cross" geofences
if d.old != nil && d.obj != nil && s.hookCross.Len() > 0 {
r1, r2 := d.old.Rect(), d.obj.Rect()
s.hookCross.Search(
[2]float64{
math.Min(r1.Min.X, r2.Min.X),
math.Min(r1.Min.Y, r2.Min.Y),
},
[2]float64{
math.Max(r1.Max.X, r2.Max.X),
math.Max(r1.Max.Y, r2.Max.Y),
},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
// look for candidates that overlap the old object
if d.old != nil {
r1 := d.old.Rect()
s.hookTree.Search(
[2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
// look for candidates that overlap the new object
if d.obj != nil {
r1 := d.obj.Rect()
s.hookTree.Search(
[2]float64{r1.Min.X, r1.Min.Y},
[2]float64{r1.Max.X, r1.Max.Y},
func(min, max [2]float64, value interface{}) bool {
hook := value.(*Hook)
if hook.Key == d.key {
candidates[hook] = true
}
return true
})
}
if len(candidates) == 0 {
return nil
}
// return the candidates as a slice
ret := make([]*Hook, 0, len(candidates))
for hook := range candidates {
ret = append(ret, hook)
}
return ret
}
func (s *Server) queueHooks(d *commandDetails) error {
// Create the slices that will store all messages and hooks
var cmsgs, wmsgs []string
var whooks []*Hook
// Compile a slice of potential hook recipients
candidates := s.getQueueCandidates(d)
for _, hook := range candidates {
// Calculate all matching fence messages for all candidates and append
// them to the appropriate message slice
msgs := FenceMatch(hook.Name, hook.ScanWriter, hook.Fence, hook.Metas, d)
if len(msgs) > 0 {
if hook.channel {
cmsgs = append(cmsgs, msgs...)
} else {
wmsgs = append(wmsgs, msgs...)
whooks = append(whooks, hook)
}
}
}
// Return nil if there are no messages to be sent
if len(cmsgs)+len(wmsgs) == 0 {
return nil
}
// Sort both message channel and webhook message slices
if len(cmsgs) > 1 {
sortMsgs(cmsgs)
}
if len(wmsgs) > 1 {
sortMsgs(wmsgs)
}
// Publish all channel messages if any exist
if len(cmsgs) > 0 {
for _, m := range cmsgs {
s.Publish(gjson.Get(m, "hook").String(), m)
}
}
// Queue the webhook messages in the buntdb database
err := s.qdb.Update(func(tx *buntdb.Tx) error {
for _, msg := range wmsgs {
s.qidx++ // increment the log id
key := hookLogPrefix + uint64ToString(s.qidx)
_, _, err := tx.Set(key, msg, hookLogSetDefaults)
if err != nil {
return err
}
log.Debugf("queued hook: %d", s.qidx)
}
_, _, err := tx.Set("hook:idx", uint64ToString(s.qidx), nil)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// all the messages have been queued.
// notify the hooks
for _, hook := range whooks {
hook.Signal()
}
return nil
}
// sortMsgs sorts passed notification messages by their detect and hook fields
func sortMsgs(msgs []string) {
sort.SliceStable(msgs, func(i, j int) bool {
detectI := msgDetectCode(gjson.Get(msgs[i], "detect").String())
detectJ := msgDetectCode(gjson.Get(msgs[j], "detect").String())
if detectI < detectJ {
return true
}
if detectI > detectJ {
return false
}
hookI := gjson.Get(msgs[i], "hook").String()
hookJ := gjson.Get(msgs[j], "hook").String()
return hookI < hookJ
})
}
// msgDetectCode returns a weight value for the passed detect value
func msgDetectCode(detect string) int {
switch detect {
case "exit":
return 1
case "outside":
return 2
case "enter":
return 3
case "inside":
return 4
default:
return 0
}
}
// Converts string to an integer
func stringToUint64(s string) uint64 {
n, _ := strconv.ParseUint(s, 10, 64)
return n
}
// Converts a uint to a string
func uint64ToString(u uint64) string {
s := strings.Repeat("0", 20) + strconv.FormatUint(u, 10)
return s[len(s)-20:]
}
type liveAOFSwitches struct {
pos int64
}
func (s liveAOFSwitches) Error() string {
return goingLive
}
// AOFMD5 pos size
func (s *Server) cmdAOFMD5(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
pos, err := strconv.ParseInt(args[1], 10, 64)
if err != nil || pos < 0 {
return retrerr(errInvalidArgument(args[1]))
}
size, err := strconv.ParseInt(args[2], 10, 64)
if err != nil || size < 0 {
return retrerr(errInvalidArgument(args[2]))
}
// >> Operation
sum, err := s.checksum(pos, size)
if err != nil {
return retrerr(err)
}
// >> Response
if msg.OutputType == JSON {
return resp.StringValue(fmt.Sprintf(
`{"ok":true,"md5":"%s","elapsed":"%s"}`,
sum, time.Since(start))), nil
}
return resp.SimpleStringValue(sum), nil
}
// AOF pos
func (s *Server) cmdAOF(msg *Message) (resp.Value, error) {
if s.aof == nil {
return retrerr(errors.New("aof disabled"))
}
// >> Args
args := msg.Args
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
pos, err := strconv.ParseInt(args[1], 10, 64)
if err != nil || pos < 0 {
return retrerr(errInvalidArgument(args[1]))
}
// >> Operation
f, err := os.Open(s.aof.Name())
if err != nil {
return retrerr(err)
}
defer f.Close()
n, err := f.Seek(0, 2)
if err != nil {
return retrerr(err)
}
if n < pos {
return retrerr(errors.New(
"pos is too big, must be less that the aof_size of leader"))
}
// >> Response
var ls liveAOFSwitches
ls.pos = pos
return NOMessage, ls
}
func (s *Server) liveAOF(pos int64, conn net.Conn, rd *PipelineReader, msg *Message) error {
s.mu.RLock()
f, err := os.Open(s.aof.Name())
s.mu.RUnlock()
if err != nil {
return err
}
s.mu.Lock()
s.aofconnM[conn] = f
s.mu.Unlock()
defer func() {
s.mu.Lock()
delete(s.aofconnM, conn)
s.mu.Unlock()
conn.Close()
f.Close()
}()
if _, err := conn.Write([]byte("+OK\r\n")); err != nil {
return err
}
if _, err := f.Seek(pos, 0); err != nil {
return err
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer func() {
f.Close()
conn.Close()
wg.Done()
}()
// Any incoming message should end the connection
rd.ReadMessages()
}()
_, err = io.Copy(conn, f)
if err != nil {
return err
}
b := make([]byte, 4096*2)
for {
n, err := f.Read(b)
if n > 0 {
if _, err := conn.Write(b[:n]); err != nil {
return err
}
}
if err == io.EOF {
s.fcond.L.Lock()
s.fcond.Wait()
s.fcond.L.Unlock()
} else if err != nil {
if errors.Is(err, os.ErrClosed) {
// The live aof file can be closed when a client (follower) has
// closed their connection or following an AOFSHRINK operation.
err = nil
}
return err
}
}
}
================================================
FILE: internal/server/aofmigrate.go
================================================
package server
import (
"bufio"
"encoding/binary"
"errors"
"io"
"os"
"path"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
var errCorruptedAOF = errors.New("corrupted aof file")
// LegacyAOFReader represents the older AOF file reader.
type LegacyAOFReader struct {
r io.Reader // reader
rerr error // read error
chunk []byte // chunk buffer
buf []byte // main buffer
l int // length of valid data in buffer
p int // pointer
}
// ReadCommand reads an old command.
func (rd *LegacyAOFReader) ReadCommand() ([]byte, error) {
if rd.l >= 4 {
sz1 := int(binary.LittleEndian.Uint32(rd.buf[rd.p:]))
if rd.l >= sz1+9 {
// we have enough data for a record
sz2 := int(binary.LittleEndian.Uint32(rd.buf[rd.p+4+sz1:]))
if sz2 != sz1 || rd.buf[rd.p+4+sz1+4] != 0 {
return nil, errCorruptedAOF
}
buf := rd.buf[rd.p+4 : rd.p+4+sz1]
rd.p += sz1 + 9
rd.l -= sz1 + 9
return buf, nil
}
}
// need more data
if rd.rerr != nil {
if rd.rerr == io.EOF {
rd.rerr = nil // we want to return EOF, but we want to be able to try again
if rd.l != 0 {
return nil, io.ErrUnexpectedEOF
}
return nil, io.EOF
}
return nil, rd.rerr
}
if rd.p != 0 {
// move p to the beginning
copy(rd.buf, rd.buf[rd.p:rd.p+rd.l])
rd.p = 0
}
var n int
n, rd.rerr = rd.r.Read(rd.chunk)
if n > 0 {
cbuf := rd.chunk[:n]
if len(rd.buf)-rd.l < n {
if len(rd.buf) == 0 {
rd.buf = make([]byte, len(cbuf))
copy(rd.buf, cbuf)
} else {
copy(rd.buf[rd.l:], cbuf[:len(rd.buf)-rd.l])
rd.buf = append(rd.buf, cbuf[len(rd.buf)-rd.l:]...)
}
} else {
copy(rd.buf[rd.l:], cbuf)
}
rd.l += n
}
return rd.ReadCommand()
}
// NewLegacyAOFReader creates a new LegacyAOFReader.
func NewLegacyAOFReader(r io.Reader) *LegacyAOFReader {
rd := &LegacyAOFReader{r: r, chunk: make([]byte, 0xFFFF)}
return rd
}
func (s *Server) migrateAOF() error {
_, err := os.Stat(path.Join(s.dir, "appendonly.aof"))
if err == nil {
return nil
}
if !os.IsNotExist(err) {
return err
}
_, err = os.Stat(path.Join(s.dir, "aof"))
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
log.Warn("Migrating aof to new format")
newf, err := os.Create(path.Join(s.dir, "migrate.aof"))
if err != nil {
return err
}
defer newf.Close()
oldf, err := os.Open(path.Join(s.dir, "aof"))
if err != nil {
return err
}
defer oldf.Close()
start := time.Now()
count := 0
wr := bufio.NewWriter(newf)
rd := NewLegacyAOFReader(oldf)
for {
cmdb, err := rd.ReadCommand()
if err != nil {
if err == io.EOF {
break
}
return err
}
line := string(cmdb)
var tok string
values := make([]resp.Value, 0, 64)
for line != "" {
line, tok = token(line)
if len(tok) > 0 && tok[0] == '{' {
if line != "" {
tok = tok + " " + line
line = ""
}
}
values = append(values, resp.StringValue(tok))
}
data, err := resp.ArrayValue(values).MarshalRESP()
if err != nil {
return err
}
if _, err := wr.Write(data); err != nil {
return err
}
if wr.Buffered() > 1024*1024 {
if err := wr.Flush(); err != nil {
return err
}
}
count++
}
if err := wr.Flush(); err != nil {
return err
}
oldf.Close()
newf.Close()
log.Debugf("%d items: %.0f/sec", count, float64(count)/(float64(time.Since(start))/float64(time.Second)))
return os.Rename(path.Join(s.dir, "migrate.aof"), path.Join(s.dir, "appendonly.aof"))
}
================================================
FILE: internal/server/aofshrink.go
================================================
package server
import (
"math"
"os"
"strconv"
"strings"
"time"
"github.com/tidwall/btree"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/object"
)
const maxkeys = 8
const maxids = 32
const maxchunk = 4 * 1024 * 1024
func (s *Server) aofshrink() {
start := time.Now()
s.mu.Lock()
if s.aof == nil || s.shrinking {
s.mu.Unlock()
return
}
s.shrinking = true
s.shrinklog = nil
s.mu.Unlock()
defer func() {
s.mu.Lock()
s.shrinking = false
s.shrinklog = nil
s.mu.Unlock()
log.Infof("aof shrink ended %v", time.Since(start))
}()
err := func() error {
f, err := os.Create(s.opts.AppendFileName + "-shrink")
if err != nil {
return err
}
defer f.Close()
var aofbuf []byte
var values []string
var keys []string
var nextkey string
var keysdone bool
for {
if len(keys) == 0 {
// load more keys
if keysdone {
break
}
keysdone = true
func() {
s.mu.Lock()
defer s.mu.Unlock()
s.cols.Ascend(nextkey,
func(key string, col *collection.Collection) bool {
if len(keys) == maxkeys {
keysdone = false
nextkey = key
return false
}
keys = append(keys, key)
return true
},
)
}()
continue
}
var idsdone bool
var nextid string
for {
if idsdone {
keys = keys[1:]
break
}
// load more objects
func() {
idsdone = true
s.mu.Lock()
defer s.mu.Unlock()
col, ok := s.cols.Get(keys[0])
if !ok {
return
}
var now = time.Now().UnixNano() // used for expiration
var count = 0 // the object count
col.ScanGreaterOrEqual(nextid, false, nil, nil,
func(o *object.Object) bool {
if count == maxids {
// we reached the max number of ids for one batch
nextid = o.ID()
idsdone = false
return false
}
// here we fill the values array with a new command
values = values[:0]
values = append(values, "set")
values = append(values, keys[0])
values = append(values, o.ID())
o.Fields().Scan(func(f field.Field) bool {
if !f.Value().IsZero() {
values = append(values, "field")
values = append(values, f.Name())
values = append(values, f.Value().JSON())
}
return true
})
if o.Expires() != 0 {
ttl := math.Floor(float64(o.Expires()-now)/float64(time.Second)*10) / 10
if ttl < 0.1 {
// always leave a little bit of ttl.
ttl = 0.1
}
values = append(values, "ex")
values = append(values, strconv.FormatFloat(ttl, 'f', -1, 64))
}
if objIsSpatial(o.Geo()) {
values = append(values, "object")
values = append(values, string(o.Geo().AppendJSON(nil)))
} else {
values = append(values, "string")
values = append(values, o.Geo().String())
}
// append the values to the aof buffer
aofbuf = append(aofbuf, '*')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
for _, value := range values {
aofbuf = append(aofbuf, '$')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(value)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
aofbuf = append(aofbuf, value...)
aofbuf = append(aofbuf, '\r', '\n')
}
// increment the object count
count++
return true
},
)
}()
if len(aofbuf) > maxchunk {
if _, err := f.Write(aofbuf); err != nil {
return err
}
aofbuf = aofbuf[:0]
}
}
}
// load hooks
// first load the names of the hooks
var hnames []string
func() {
s.mu.Lock()
defer s.mu.Unlock()
hnames = make([]string, 0, s.hooks.Len())
s.hooks.Walk(func(v []interface{}) {
for _, v := range v {
hnames = append(hnames, v.(*Hook).Name)
}
})
}()
var hookHint btree.PathHint
for _, name := range hnames {
func() {
s.mu.Lock()
defer s.mu.Unlock()
hook, _ := s.hooks.GetHint(&Hook{Name: name}, &hookHint).(*Hook)
if hook == nil {
return
}
hook.cond.L.Lock()
defer hook.cond.L.Unlock()
var values []string
if hook.channel {
values = append(values, "setchan", name)
} else {
values = append(values, "sethook", name,
strings.Join(hook.Endpoints, ","))
}
for _, meta := range hook.Metas {
values = append(values, "meta", meta.Name, meta.Value)
}
if !hook.expires.IsZero() {
ex := float64(time.Until(hook.expires)) / float64(time.Second)
values = append(values, "ex",
strconv.FormatFloat(ex, 'f', 1, 64))
}
values = append(values, hook.Message.Args...)
// append the values to the aof buffer
aofbuf = append(aofbuf, '*')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
for _, value := range values {
aofbuf = append(aofbuf, '$')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(value)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
aofbuf = append(aofbuf, value...)
aofbuf = append(aofbuf, '\r', '\n')
}
}()
}
if len(aofbuf) > 0 {
if _, err := f.Write(aofbuf); err != nil {
return err
}
aofbuf = aofbuf[:0]
}
if err := f.Sync(); err != nil {
return err
}
// finally grab any new data that may have been written since
// the aofshrink has started and swap out the files.
return func() error {
s.mu.Lock()
defer s.mu.Unlock()
// kill all followers connections and close their files. This
// ensures that there is only one opened AOF at a time which is
// what Windows requires in order to perform the Rename function
// below.
for conn, f := range s.aofconnM {
conn.Close()
f.Close()
}
// send a broadcast to all sleeping followers
s.fcond.Broadcast()
// flush the aof buffer
s.flushAOF(false)
aofbuf = aofbuf[:0]
for _, values := range s.shrinklog {
// append the values to the aof buffer
aofbuf = append(aofbuf, '*')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(values)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
for _, value := range values {
aofbuf = append(aofbuf, '$')
aofbuf = append(aofbuf, strconv.FormatInt(int64(len(value)), 10)...)
aofbuf = append(aofbuf, '\r', '\n')
aofbuf = append(aofbuf, value...)
aofbuf = append(aofbuf, '\r', '\n')
}
}
if _, err := f.Write(aofbuf); err != nil {
return err
}
if err := f.Sync(); err != nil {
return err
}
// we now have a shrunken aof file that is fully in-sync with
// the current dataset. let's swap out the on disk files and
// point to the new file.
// anything below this point is unrecoverable. just log and exit process
// back up the live aof, just in case of fatal error
if err := s.aof.Close(); err != nil {
log.Fatalf("shrink live aof close fatal operation: %v", err)
}
if err := f.Close(); err != nil {
log.Fatalf("shrink new aof close fatal operation: %v", err)
}
if err := os.Rename(s.opts.AppendFileName, s.opts.AppendFileName+"-bak"); err != nil {
log.Fatalf("shrink backup fatal operation: %v", err)
}
if err := os.Rename(s.opts.AppendFileName+"-shrink", s.opts.AppendFileName); err != nil {
log.Fatalf("shrink rename fatal operation: %v", err)
}
s.aof, err = os.OpenFile(s.opts.AppendFileName, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
log.Fatalf("shrink openfile fatal operation: %v", err)
}
var n int64
n, err = s.aof.Seek(0, 2)
if err != nil {
log.Fatalf("shrink seek end fatal operation: %v", err)
}
s.aofsz = int(n)
os.Remove(s.opts.AppendFileName + "-bak") // ignore error
return nil
}()
}()
if err != nil {
log.Errorf("aof shrink failed: %v", err)
return
}
}
================================================
FILE: internal/server/bson.go
================================================
package server
import (
"crypto/md5"
"crypto/rand"
"encoding/binary"
"encoding/hex"
"os"
"sync/atomic"
"time"
)
func bsonID() string {
b := make([]byte, 12)
binary.BigEndian.PutUint32(b, uint32(time.Now().Unix()))
copy(b[4:], bsonMachine)
binary.BigEndian.PutUint32(b[8:], atomic.AddUint32(&bsonCounter, 1))
binary.BigEndian.PutUint16(b[7:], bsonProcess)
return hex.EncodeToString(b)
}
var (
bsonProcess = uint16(os.Getpid())
bsonMachine = func() []byte {
host, _ := os.Hostname()
b := make([]byte, 3)
Must(rand.Read(b))
host = Default(host, string(b))
hw := md5.New()
hw.Write([]byte(host))
return hw.Sum(nil)[:3]
}()
bsonCounter = func() uint32 {
b := make([]byte, 4)
Must(rand.Read(b))
return binary.BigEndian.Uint32(b)
}()
)
================================================
FILE: internal/server/bson_test.go
================================================
package server
import "testing"
func TestBSON(t *testing.T) {
id := bsonID()
if len(id) != 24 {
t.Fail()
}
}
================================================
FILE: internal/server/checksum.go
================================================
package server
import (
"crypto/md5"
"errors"
"fmt"
"io"
"os"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
// checksum performs a simple md5 checksum on the aof file
func (s *Server) checksum(pos, size int64) (sum string, err error) {
if pos+size > int64(s.aofsz) {
return "", io.EOF
}
var f *os.File
f, err = os.Open(s.aof.Name())
if err != nil {
return
}
defer f.Close()
sumr := md5.New()
err = func() error {
if size == 0 {
n, err := f.Seek(int64(s.aofsz), 0)
if err != nil {
return err
}
if pos >= n {
return io.EOF
}
return nil
}
_, err = f.Seek(pos, 0)
if err != nil {
return err
}
_, err = io.CopyN(sumr, f, size)
if err != nil {
return err
}
return nil
}()
if err != nil {
if err == io.ErrUnexpectedEOF {
err = io.EOF
}
return "", err
}
return fmt.Sprintf("%x", sumr.Sum(nil)), nil
}
func connAOFMD5(conn *RESPConn, pos, size int64) (sum string, err error) {
v, err := conn.Do("aofmd5", pos, size)
if err != nil {
return "", err
}
if v.Error() != nil {
errmsg := v.Error().Error()
if errmsg == "ERR EOF" || errmsg == "EOF" {
return "", io.EOF
}
return "", v.Error()
}
sum = v.String()
if len(sum) != 32 {
return "", errors.New("checksum not ok")
}
return sum, nil
}
func (s *Server) matchChecksums(conn *RESPConn, pos, size int64) (match bool, err error) {
sum, err := s.checksum(pos, size)
if err != nil {
if err == io.EOF {
return false, nil
}
return false, err
}
csum, err := connAOFMD5(conn, pos, size)
if err != nil {
if err == io.EOF {
return false, nil
}
return false, err
}
return csum == sum, nil
}
// getEndOfLastValuePositionInFile is a very slow operation because it reads the file
// backwards on byte at a time. Eek. It seek+read, seek+read, etc.
func getEndOfLastValuePositionInFile(fname string, startPos int64) (int64, error) {
pos := startPos
f, err := os.Open(fname)
if err != nil {
return 0, err
}
defer f.Close()
readByte := func() (byte, error) {
if pos <= 0 {
return 0, io.EOF
}
pos--
if _, err := f.Seek(pos, 0); err != nil {
return 0, err
}
b := make([]byte, 1)
if n, err := f.Read(b); err != nil {
return 0, err
} else if n != 1 {
return 0, errors.New("invalid read")
}
return b[0], nil
}
for {
c, err := readByte()
if err != nil {
return 0, err
}
if c == '*' {
if _, err := f.Seek(pos, 0); err != nil {
return 0, err
}
rd := resp.NewReader(f)
_, telnet, n, err := rd.ReadMultiBulk()
if err != nil || telnet {
continue // keep reading backwards
}
return pos + int64(n), nil
}
}
}
// followCheckSome is not a full checksum. It just "checks some" data.
// We will do some various checksums on the leader until we find the correct position to start at.
func (s *Server) followCheckSome(addr string, followc int, auth string,
) (pos int64, err error) {
if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":check some")
}
s.mu.Lock()
defer s.mu.Unlock()
if int(s.followc.Load()) != followc {
return 0, errNoLongerFollowing
}
if s.aofsz < checksumsz {
return 0, nil
}
conn, err := DialTimeout(addr, time.Second*2)
if err != nil {
return 0, err
}
defer conn.Close()
if auth != "" {
if err := s.followDoLeaderAuth(conn, auth); err != nil {
return 0, err
}
}
min := int64(0)
max := int64(s.aofsz) - checksumsz
limit := int64(s.aofsz)
match, err := s.matchChecksums(conn, min, checksumsz)
if err != nil {
return 0, err
}
if match {
min += checksumsz // bump up the min
for {
if max < min || max+checksumsz > limit {
pos = min
break
} else {
match, err = s.matchChecksums(conn, max, checksumsz)
if err != nil {
return 0, err
}
if match {
min = max + checksumsz
} else {
limit = max
}
max = (limit-min)/2 - checksumsz/2 + min // multiply
}
}
}
fullpos := pos
fname := s.aof.Name()
if pos == 0 {
s.aof.Close()
s.aof, err = os.Create(fname)
if err != nil {
log.Fatalf("could not recreate aof, possible data loss. %s", err.Error())
return 0, err
}
return 0, nil
}
// we want to truncate at a command location
// search for nearest command
pos, err = getEndOfLastValuePositionInFile(s.aof.Name(), fullpos)
if err != nil {
return 0, err
}
if pos == fullpos {
if s.opts.ShowDebugMessages {
log.Debug("follow: aof fully intact")
}
return pos, nil
}
log.Warnf("truncating aof to %d", pos)
// any error below are fatal.
s.aof.Close()
if err := os.Truncate(fname, pos); err != nil {
log.Fatalf("could not truncate aof, possible data loss. %s", err.Error())
return 0, err
}
s.aof, err = os.OpenFile(fname, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
log.Fatalf("could not create aof, possible data loss. %s", err.Error())
return 0, err
}
// reset the entire system.
log.Infof("reloading aof commands")
s.reset()
if err := s.loadAOF(); err != nil {
log.Fatalf("could not reload aof, possible data loss. %s", err.Error())
return 0, err
}
if int64(s.aofsz) != pos {
log.Fatalf("aof size mismatch during reload, possible data loss.")
return 0, errors.New("?")
}
return pos, nil
}
================================================
FILE: internal/server/client.go
================================================
package server
import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"sync"
"time"
"github.com/tidwall/resp"
)
// Client is an remote connection into to Tile38
type Client struct {
id int // unique id
replPort int // the known replication port for follower connections
replAddr string // the known replication addr for follower connections
authd bool // client has been authenticated
outputType Type // Null, JSON, or RESP
strictRESP bool // client is in strict RESP mode
remoteAddr string // original remote address
in InputStream // input stream
pr PipelineReader // command reader
out []byte // output write buffer
goLiveErr error // error type used for going line
goLiveMsg *Message // last message for go live
mu sync.Mutex // guard
conn io.ReadWriteCloser // out-of-loop connection.
name string // optional defined name
opened time.Time // when the client was created/opened, unix nano
last time.Time // last client request/response, unix nano
closer io.Closer // used to close the connection
}
// Write ...
func (client *Client) Write(b []byte) (n int, err error) {
client.out = append(client.out, b...)
return len(b), nil
}
// CLIENT (LIST | KILL | GETNAME | SETNAME)
func (s *Server) cmdCLIENT(msg *Message, client *Client) (resp.Value, error) {
start := time.Now()
args := msg.Args
if len(args) == 1 {
return retrerr(errInvalidNumberOfArguments)
}
switch strings.ToLower(args[1]) {
case "list":
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
var list []*Client
s.connsmu.RLock()
for _, cc := range s.conns {
list = append(list, cc)
}
s.connsmu.RUnlock()
sort.Slice(list, func(i, j int) bool {
return list[i].id < list[j].id
})
now := time.Now()
var buf []byte
for _, client := range list {
client.mu.Lock()
buf = append(buf,
fmt.Sprintf("id=%d addr=%s name=%s age=%d idle=%d\n",
client.id,
client.remoteAddr,
client.name,
now.Sub(client.opened)/time.Second,
now.Sub(client.last)/time.Second,
)...,
)
client.mu.Unlock()
}
if msg.OutputType == JSON {
// Create a map of all key/value info fields
var cmap []map[string]interface{}
clients := strings.Split(string(buf), "\n")
for _, client := range clients {
client = strings.TrimSpace(client)
m := make(map[string]interface{})
var hasFields bool
for _, kv := range strings.Split(client, " ") {
kv = strings.TrimSpace(kv)
if split := strings.SplitN(kv, "=", 2); len(split) == 2 {
hasFields = true
m[split[0]] = tryParseType(split[1])
}
}
if hasFields {
cmap = append(cmap, m)
}
}
data, _ := json.Marshal(cmap)
return resp.StringValue(`{"ok":true,"list":` + string(data) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
return resp.BytesValue(buf), nil
case "getname":
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
client.mu.Lock()
name := client.name
client.mu.Unlock()
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"name":` + jsonString(name) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
return resp.StringValue(name), nil
case "setname":
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
name := msg.Args[2]
for i := 0; i < len(name); i++ {
if name[i] < '!' || name[i] > '~' {
return retrerr(clientErrorf(
"Client names cannot contain spaces, newlines or special characters.",
))
}
}
client.mu.Lock()
client.name = name
client.mu.Unlock()
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.SimpleStringValue("OK"), nil
case "kill":
if len(args) < 3 {
return retrerr(errInvalidNumberOfArguments)
}
var useAddr bool
var addr string
var useID bool
var id string
for i := 2; i < len(args); i++ {
if useAddr || useID {
return retrerr(errInvalidNumberOfArguments)
}
arg := args[i]
if strings.Contains(arg, ":") {
addr = arg
useAddr = true
} else {
switch strings.ToLower(arg) {
case "addr":
i++
if i == len(args) {
return retrerr(errInvalidNumberOfArguments)
}
addr = args[i]
useAddr = true
case "id":
i++
if i == len(args) {
return retrerr(errInvalidNumberOfArguments)
}
id = args[i]
useID = true
default:
return retrerr(clientErrorf("No such client"))
}
}
}
var closing []io.Closer
s.connsmu.RLock()
for _, cc := range s.conns {
if useID && fmt.Sprintf("%d", cc.id) == id {
if cc.closer != nil {
closing = append(closing, cc.closer)
}
} else if useAddr {
if cc.remoteAddr == addr {
if cc.closer != nil {
closing = append(closing, cc.closer)
}
}
}
}
s.connsmu.RUnlock()
if len(closing) == 0 {
return retrerr(clientErrorf("No such client"))
}
// go func() {
// close the connections behind the scene
for _, closer := range closing {
closer.Close()
}
// }()
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.SimpleStringValue("OK"), nil
default:
return retrerr(clientErrorf(
"Syntax error, try CLIENT (LIST | KILL | GETNAME | SETNAME)",
))
}
}
================================================
FILE: internal/server/config.go
================================================
package server
import (
"encoding/json"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/glob"
)
const (
defaultKeepAlive = 300 // seconds
defaultProtectedMode = "yes"
)
// Config keys
const (
FollowHost = "follow_host"
FollowPort = "follow_port"
FollowID = "follow_id"
FollowPos = "follow_pos"
ReplicaPriority = "replica-priority"
ServerID = "server_id"
ReadOnly = "read_only"
RequirePass = "requirepass"
LeaderAuth = "leaderauth"
ProtectedMode = "protected-mode"
MaxMemory = "maxmemory"
AutoGC = "autogc"
KeepAlive = "keepalive"
LogConfig = "logconfig"
AnnounceIP = "replica_announce_ip"
AnnouncePort = "replica_announce_port"
)
var validProperties = []string{RequirePass, LeaderAuth, ProtectedMode, MaxMemory, AutoGC, KeepAlive, LogConfig, ReplicaPriority, AnnouncePort, AnnounceIP}
// Config is a tile38 config
type Config struct {
path string
mu sync.RWMutex
_followHost string
_followPort int64
_followID string
_followPos int64
_replicaPriority int64
_serverID string
_readOnly bool
_requirePassP string
_requirePass string
_leaderAuthP string
_leaderAuth string
_protectedModeP string
_protectedMode string
_maxMemoryP string
_maxMemory int64
_autoGCP string
_autoGC uint64
_keepAliveP string
_keepAlive int64
_logConfigP interface{}
_logConfig string
_announceIPP string
_announceIP string
_announcePortP string
_announcePort int64
}
func loadConfig(path string) (*Config, error) {
var json string
data, err := os.ReadFile(path)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
json = string(data)
}
config := &Config{
path: path,
_followHost: gjson.Get(json, FollowHost).String(),
_followPort: gjson.Get(json, FollowPort).Int(),
_followID: gjson.Get(json, FollowID).String(),
_followPos: gjson.Get(json, FollowPos).Int(),
_serverID: gjson.Get(json, ServerID).String(),
_readOnly: gjson.Get(json, ReadOnly).Bool(),
_requirePassP: gjson.Get(json, RequirePass).String(),
_leaderAuthP: gjson.Get(json, LeaderAuth).String(),
_protectedModeP: gjson.Get(json, ProtectedMode).String(),
_maxMemoryP: gjson.Get(json, MaxMemory).String(),
_autoGCP: gjson.Get(json, AutoGC).String(),
_keepAliveP: gjson.Get(json, KeepAlive).String(),
_logConfig: gjson.Get(json, LogConfig).String(),
_announceIPP: gjson.Get(json, AnnounceIP).String(),
_announcePortP: gjson.Get(json, AnnouncePort).String(),
}
if config._serverID == "" {
config._serverID = randomKey(16)
}
// Need to be sure we look for existence vs not zero because zero is an intentional setting
// anything less than zero will be considered default and will result in no slave_priority
// being output when INFO is called.
if gjson.Get(json, ReplicaPriority).Exists() {
config._replicaPriority = gjson.Get(json, ReplicaPriority).Int()
} else {
config._replicaPriority = -1
}
// load properties
if err := config.setProperty(RequirePass, config._requirePassP, true); err != nil {
return nil, err
}
if err := config.setProperty(LeaderAuth, config._leaderAuthP, true); err != nil {
return nil, err
}
if err := config.setProperty(ProtectedMode, config._protectedModeP, true); err != nil {
return nil, err
}
if err := config.setProperty(MaxMemory, config._maxMemoryP, true); err != nil {
return nil, err
}
if err := config.setProperty(AutoGC, config._autoGCP, true); err != nil {
return nil, err
}
if err := config.setProperty(KeepAlive, config._keepAliveP, true); err != nil {
return nil, err
}
if err := config.setProperty(LogConfig, config._logConfig, true); err != nil {
return nil, err
}
if err := config.setProperty(AnnounceIP, config._announceIPP, true); err != nil {
return nil, err
}
if err := config.setProperty(AnnouncePort, config._announcePortP, true); err != nil {
return nil, err
}
config.write(false)
return config, nil
}
func (config *Config) write(writeProperties bool) {
config.mu.Lock()
defer config.mu.Unlock()
if writeProperties {
// save properties
config._requirePassP = config._requirePass
config._leaderAuthP = config._leaderAuth
if config._protectedMode == defaultProtectedMode {
config._protectedModeP = ""
} else {
config._protectedModeP = config._protectedMode
}
config._maxMemoryP = formatMemSize(config._maxMemory)
if config._autoGC == 0 {
config._autoGCP = ""
} else {
config._autoGCP = strconv.FormatUint(config._autoGC, 10)
}
if config._keepAlive == defaultKeepAlive {
config._keepAliveP = ""
} else {
config._keepAliveP = strconv.FormatUint(uint64(config._keepAlive), 10)
}
if config._logConfig != "" {
config._logConfigP = config._logConfig
}
if config._announceIP != "" {
config._announceIPP = config._announceIP
}
if config._announcePort == 0 {
config._announcePortP = ""
} else {
config._announcePortP = strconv.FormatUint(uint64(config._announcePort), 10)
}
}
m := make(map[string]interface{})
if config._followHost != "" {
m[FollowHost] = config._followHost
}
if config._followPort != 0 {
m[FollowPort] = config._followPort
}
if config._followID != "" {
m[FollowID] = config._followID
}
if config._followPos != 0 {
m[FollowPos] = config._followPos
}
if config._replicaPriority >= 0 {
m[ReplicaPriority] = config._replicaPriority
}
if config._serverID != "" {
m[ServerID] = config._serverID
}
if config._readOnly {
m[ReadOnly] = config._readOnly
}
if config._requirePassP != "" {
m[RequirePass] = config._requirePassP
}
if config._leaderAuthP != "" {
m[LeaderAuth] = config._leaderAuthP
}
if config._protectedModeP != "" {
m[ProtectedMode] = config._protectedModeP
}
if config._maxMemoryP != "" {
m[MaxMemory] = config._maxMemoryP
}
if config._autoGCP != "" {
m[AutoGC] = config._autoGCP
}
if config._keepAliveP != "" {
m[KeepAlive] = config._keepAliveP
}
if config._logConfigP != "" {
var lcfg map[string]interface{}
json.Unmarshal([]byte(config._logConfig), &lcfg)
if len(lcfg) > 0 {
m[LogConfig] = lcfg
}
}
if config._announceIPP != "" {
m[AnnounceIP] = config._announceIPP
}
if config._announcePortP != "" {
m[AnnouncePort] = config._announcePortP
}
data, err := json.MarshalIndent(m, "", "\t")
if err != nil {
panic(err)
}
data = append(data, '\n')
err = os.WriteFile(config.path, data, 0600)
if err != nil {
panic(err)
}
}
func parseMemSize(s string) (bytes int64, ok bool) {
if s == "" {
return 0, true
}
s = strings.ToLower(s)
var n uint64
var sz int64
var err error
if strings.HasSuffix(s, "gb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int64(n * 1024 * 1024 * 1024)
} else if strings.HasSuffix(s, "mb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int64(n * 1024 * 1024)
} else if strings.HasSuffix(s, "kb") {
n, err = strconv.ParseUint(s[:len(s)-2], 10, 64)
sz = int64(n * 1024)
} else {
n, err = strconv.ParseUint(s, 10, 64)
sz = int64(n)
}
if err != nil {
return 0, false
}
return sz, true
}
func formatMemSize(sz int64) string {
if sz <= 0 {
return ""
}
if sz < 1024 {
return strconv.FormatInt(sz, 10)
}
sz /= 1024
if sz < 1024 {
return strconv.FormatInt(sz, 10) + "kb"
}
sz /= 1024
if sz < 1024 {
return strconv.FormatInt(sz, 10) + "mb"
}
sz /= 1024
return strconv.FormatInt(sz, 10) + "gb"
}
func (config *Config) setProperty(name, value string, fromLoad bool) error {
config.mu.Lock()
defer config.mu.Unlock()
var invalid bool
switch name {
default:
return clientErrorf("Unsupported CONFIG parameter: %s", name)
case RequirePass:
config._requirePass = value
case LeaderAuth:
config._leaderAuth = value
case AutoGC:
if value == "" {
config._autoGC = 0
} else {
gc, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return err
}
config._autoGC = gc
}
case MaxMemory:
sz, ok := parseMemSize(value)
if !ok {
return clientErrorf("Invalid argument '%s' for CONFIG SET '%s'", value, name)
}
config._maxMemory = sz
case ProtectedMode:
switch strings.ToLower(value) {
case "":
if fromLoad {
config._protectedMode = defaultProtectedMode
} else {
invalid = true
}
case "yes", "no":
config._protectedMode = strings.ToLower(value)
default:
invalid = true
}
case KeepAlive:
if value == "" {
config._keepAlive = defaultKeepAlive
} else {
keepalive, err := strconv.ParseUint(value, 10, 64)
if err != nil {
invalid = true
} else {
config._keepAlive = int64(keepalive)
}
}
case LogConfig:
if value == "" {
config._logConfig = ""
} else {
config._logConfig = value
}
case ReplicaPriority:
replicaPriority, err := strconv.ParseInt(value, 10, 64)
if err != nil || replicaPriority < 0 {
invalid = true
} else {
config._replicaPriority = replicaPriority
}
case AnnounceIP:
config._announceIP = value
case AnnouncePort:
if value == "" {
config._announcePort = 0
} else {
announcePort, err := strconv.ParseUint(value, 10, 64)
if err != nil {
invalid = true
} else {
config._announcePort = int64(announcePort)
}
}
}
if invalid {
return clientErrorf("Invalid argument '%s' for CONFIG SET '%s'", value, name)
}
return nil
}
func (config *Config) getProperties(pattern string) map[string]interface{} {
m := make(map[string]interface{})
for _, name := range validProperties {
matched, _ := glob.Match(pattern, name)
if matched {
m[name] = config.getProperty(name)
}
}
return m
}
func (config *Config) getProperty(name string) string {
config.mu.RLock()
defer config.mu.RUnlock()
switch name {
default:
return ""
case AutoGC:
return strconv.FormatUint(config._autoGC, 10)
case RequirePass:
return config._requirePass
case LeaderAuth:
return config._leaderAuth
case ProtectedMode:
return config._protectedMode
case MaxMemory:
return formatMemSize(config._maxMemory)
case KeepAlive:
return strconv.FormatUint(uint64(config._keepAlive), 10)
case LogConfig:
return config._logConfig
case ReplicaPriority:
if config._replicaPriority < 0 {
return ""
} else {
return strconv.FormatUint(uint64(config._replicaPriority), 10)
}
case AnnounceIP:
return config._announceIP
case AnnouncePort:
return strconv.FormatUint(uint64(config._announcePort), 10)
}
}
func (s *Server) cmdConfigGet(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var name string
if vs, name, ok = tokenval(vs); !ok {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
m := s.config.getProperties(name)
switch msg.OutputType {
case JSON:
data, err := json.Marshal(m)
if err != nil {
return NOMessage, err
}
res = resp.StringValue(`{"ok":true,"properties":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
vals := respValuesSimpleMap(m)
res = resp.ArrayValue(vals)
}
return
}
func (s *Server) cmdConfigSet(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var name string
if vs, name, ok = tokenval(vs); !ok {
return NOMessage, errInvalidNumberOfArguments
}
var value string
if vs, value, ok = tokenval(vs); !ok {
if strings.ToLower(name) != RequirePass {
return NOMessage, errInvalidNumberOfArguments
}
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
if err := s.config.setProperty(name, value, false); err != nil {
return NOMessage, err
}
if name == MaxMemory {
s.checkOutOfMemory()
}
return OKMessage(msg, start), nil
}
func (s *Server) cmdConfigRewrite(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
s.config.write(true)
return OKMessage(msg, start), nil
}
func (config *Config) followHost() string {
config.mu.RLock()
v := config._followHost
config.mu.RUnlock()
return v
}
func (config *Config) followPort() int {
config.mu.RLock()
v := config._followPort
config.mu.RUnlock()
return int(v)
}
func (config *Config) replicaPriority() int {
config.mu.RLock()
v := config._replicaPriority
config.mu.RUnlock()
return int(v)
}
func (config *Config) serverID() string {
config.mu.RLock()
v := config._serverID
config.mu.RUnlock()
return v
}
func (config *Config) readOnly() bool {
config.mu.RLock()
v := config._readOnly
config.mu.RUnlock()
return v
}
func (config *Config) requirePass() string {
config.mu.RLock()
v := config._requirePass
config.mu.RUnlock()
return v
}
func (config *Config) leaderAuth() string {
config.mu.RLock()
v := config._leaderAuth
config.mu.RUnlock()
return v
}
func (config *Config) protectedMode() string {
config.mu.RLock()
v := config._protectedMode
config.mu.RUnlock()
return v
}
func (config *Config) maxMemory() int {
config.mu.RLock()
v := config._maxMemory
config.mu.RUnlock()
return int(v)
}
func (config *Config) autoGC() uint64 {
config.mu.RLock()
v := config._autoGC
config.mu.RUnlock()
return v
}
func (config *Config) keepAlive() int64 {
config.mu.RLock()
v := config._keepAlive
config.mu.RUnlock()
return v
}
func (config *Config) announceIP() string {
config.mu.RLock()
v := config._announceIP
config.mu.RUnlock()
return v
}
func (config *Config) announcePort() int {
config.mu.RLock()
v := config._announcePort
config.mu.RUnlock()
return int(v)
}
func (config *Config) setFollowHost(v string) {
config.mu.Lock()
config._followHost = v
config.mu.Unlock()
}
func (config *Config) setFollowPort(v int) {
config.mu.Lock()
config._followPort = int64(v)
config.mu.Unlock()
}
func (config *Config) setReadOnly(v bool) {
config.mu.Lock()
config._readOnly = v
config.mu.Unlock()
}
================================================
FILE: internal/server/crud.go
================================================
package server
import (
"bytes"
"math"
"strconv"
"strings"
"time"
"github.com/mmcloughlin/geohash"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/object"
)
// BOUNDS key
func (s *Server) cmdBOUNDS(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
key := args[1]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return retrerr(errKeyNotFound)
}
// >> Response
vals := make([]resp.Value, 0, 2)
var buf bytes.Buffer
if msg.OutputType == JSON {
buf.WriteString(`{"ok":true`)
}
minX, minY, maxX, maxY := col.Bounds()
bbox := geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minX, Y: minY},
Max: geometry.Point{X: maxX, Y: maxY},
})
if msg.OutputType == JSON {
buf.WriteString(`,"bounds":`)
buf.WriteString(string(bbox.AppendJSON(nil)))
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
}
// RESP
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(minX),
resp.FloatValue(minY),
}),
resp.ArrayValue([]resp.Value{
resp.FloatValue(maxX),
resp.FloatValue(maxY),
}),
}))
return vals[0], nil
}
// TYPE key
// undocumented return "none" or "hash"
func (s *Server) cmdTYPE(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
key := args[1]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.SimpleStringValue("none"), nil
}
return retrerr(errKeyNotFound)
}
// >> Response
typ := "hash"
if msg.OutputType == JSON {
return resp.StringValue(`{"ok":true,"type":` + jsonString(typ) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
return resp.SimpleStringValue(typ), nil
}
// GET key id [WITHFIELDS] [OBJECT|POINT|BOUNDS|(HASH geohash)]
func (s *Server) cmdGET(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) < 3 {
return retrerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
withfields := false
kind := "object"
var precision int64
for i := 3; i < len(args); i++ {
switch strings.ToLower(args[i]) {
case "withfields":
withfields = true
case "object":
kind = "object"
case "point":
kind = "point"
case "bounds":
kind = "bounds"
case "hash":
kind = "hash"
i++
if i == len(args) {
return retrerr(errInvalidNumberOfArguments)
}
var err error
precision, err = strconv.ParseInt(args[i], 10, 64)
if err != nil || precision < 1 || precision > 12 {
return retrerr(errInvalidArgument(args[i]))
}
default:
return retrerr(errInvalidNumberOfArguments)
}
}
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return retrerr(errKeyNotFound)
}
o := col.Get(id)
if o == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return retrerr(errIDNotFound)
}
// >> Response
oval := buildObjectResponse(msg, o, start, kind, precision, withfields, msg.OutputType == JSON)
return oval, nil
}
func buildObjectResponse(msg *Message, o *object.Object, start time.Time, kind string, precision int64, withfields, json bool) resp.Value {
vals := make([]resp.Value, 0, 2)
var buf bytes.Buffer
if msg.OutputType == JSON {
buf.WriteString(`{"ok":true`)
}
switch kind {
case "object":
if msg.OutputType == JSON {
buf.WriteString(`,"object":`)
buf.WriteString(string(o.Geo().AppendJSON(nil)))
} else {
vals = append(vals, resp.StringValue(o.Geo().String()))
}
case "point":
if msg.OutputType == JSON {
buf.WriteString(`,"point":`)
buf.Write(appendJSONSimplePoint(nil, o.Geo()))
} else {
point := o.Geo().Center()
z := extractZCoordinate(o.Geo())
if z != 0 {
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)),
resp.StringValue(strconv.FormatFloat(point.X, 'f', -1, 64)),
resp.StringValue(strconv.FormatFloat(z, 'f', -1, 64)),
}))
} else {
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)),
resp.StringValue(strconv.FormatFloat(point.X, 'f', -1, 64)),
}))
}
}
case "hash":
if msg.OutputType == JSON {
buf.WriteString(`,"hash":`)
}
center := o.Geo().Center()
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision))
if msg.OutputType == JSON {
buf.WriteString(`"` + p + `"`)
} else {
vals = append(vals, resp.StringValue(p))
}
case "bounds":
if msg.OutputType == JSON {
buf.WriteString(`,"bounds":`)
buf.Write(appendJSONSimpleBounds(nil, o.Geo()))
} else {
bbox := o.Rect()
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(bbox.Min.Y),
resp.FloatValue(bbox.Min.X),
}),
resp.ArrayValue([]resp.Value{
resp.FloatValue(bbox.Max.Y),
resp.FloatValue(bbox.Max.X),
}),
}))
}
}
if withfields {
nfields := o.Fields().Len()
if nfields > 0 {
fvals := make([]resp.Value, 0, nfields*2)
if msg.OutputType == JSON {
buf.WriteString(`,"fields":{`)
}
var i int
o.Fields().Scan(func(f field.Field) bool {
if json {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(f.Name()) + ":" +
f.Value().JSON())
} else {
fvals = append(fvals, resp.StringValue(f.Name()),
resp.StringValue(f.Value().Data()))
}
i++
return true
})
if json {
buf.WriteString(`}`)
} else {
vals = append(vals, resp.ArrayValue(fvals))
}
}
}
if json {
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String())
}
var oval resp.Value
if withfields {
oval = resp.ArrayValue(vals)
} else {
oval = vals[0]
}
return oval
}
// DEL key id [ERRON404]
func (s *Server) cmdDEL(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) < 3 {
return retwerr(errInvalidNumberOfArguments)
}
key := args[1]
id := args[2]
erron404 := false
for i := 3; i < len(args); i++ {
switch strings.ToLower(args[i]) {
case "erron404":
erron404 = true
default:
return retwerr(errInvalidArgument(args[i]))
}
}
// >> Operation
updated := false
var old *object.Object
col, _ := s.cols.Get(key)
if col != nil {
old = col.Delete(id)
if old != nil {
if col.Count() == 0 {
s.cols.Delete(key)
}
updated = true
} else if erron404 {
return retwerr(errIDNotFound)
}
} else if erron404 {
return retwerr(errKeyNotFound)
}
s.groupDisconnectObject(key, id)
// >> Response
var d commandDetails
d.command = "del"
d.key = key
d.obj = old
d.updated = updated
d.timestamp = time.Now()
var res resp.Value
switch msg.OutputType {
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
if d.updated {
res = resp.IntegerValue(1)
} else {
res = resp.IntegerValue(0)
}
}
return res, d, nil
}
// PDEL key pattern
func (s *Server) cmdPDEL(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
}
key := args[1]
pattern := args[2]
// >> Operation
now := time.Now()
var children []*commandDetails
col, _ := s.cols.Get(key)
if col != nil {
g := glob.Parse(pattern, false)
var ids []string
iter := func(o *object.Object) bool {
if match, _ := glob.Match(pattern, o.ID()); match {
ids = append(ids, o.ID())
}
return true
}
if g.Limits[0] == "" && g.Limits[1] == "" {
col.Scan(false, nil, msg.Deadline, iter)
} else {
col.ScanRange(g.Limits[0], g.Limits[1],
false, nil, msg.Deadline, iter)
}
for _, id := range ids {
obj := col.Delete(id)
children = append(children, &commandDetails{
command: "del",
updated: true,
timestamp: now,
key: key,
obj: obj,
})
s.groupDisconnectObject(key, id)
}
if col.Count() == 0 {
s.cols.Delete(key)
}
}
// >> Response
var d commandDetails
var res resp.Value
d.command = "pdel"
d.children = children
d.key = key
d.pattern = pattern
d.updated = len(d.children) > 0
d.timestamp = now
d.parent = true
switch msg.OutputType {
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
res = resp.IntegerValue(len(d.children))
}
return res, d, nil
}
func (s *Server) cmdDROPop(key string) *collection.Collection {
col, _ := s.cols.Get(key)
if col != nil {
s.cols.Delete(key)
}
s.groupDisconnectCollection(key)
return col
}
// DROP key
func (s *Server) cmdDROP(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 2 {
return retwerr(errInvalidNumberOfArguments)
}
key := args[1]
// >> Operation
col := s.cmdDROPop(key)
// >> Response
var res resp.Value
var d commandDetails
d.key = key
d.updated = col != nil
d.command = "drop"
d.timestamp = time.Now()
switch msg.OutputType {
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
if d.updated {
res = resp.IntegerValue(1)
} else {
res = resp.IntegerValue(0)
}
}
return res, d, nil
}
// RENAME key newkey
// RENAMENX key newkey
func (s *Server) cmdRENAME(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
}
nx := strings.ToLower(args[0]) == "renamenx"
key := args[1]
newKey := args[2]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
return retwerr(errKeyNotFound)
}
var hasHook, hasChannel bool
s.hooks.Ascend(nil, func(v interface{}) bool {
h := v.(*Hook)
if h.Key == key || h.Key == newKey {
if h.channel {
hasChannel = true
} else {
hasHook = true
}
}
return true
})
if hasHook {
return retwerr(errKeyHasHooksSet)
}
if hasChannel {
return retwerr(errKeyHasChannelsSet)
}
var updated bool
newCol, _ := s.cols.Get(newKey)
if newCol == nil {
updated = true
} else if !nx {
s.cols.Delete(newKey)
updated = true
}
if updated {
s.cols.Delete(key)
s.cols.Set(newKey, col)
}
// >> Response
var d commandDetails
var res resp.Value
d.command = "rename"
d.key = key
d.newKey = newKey
d.updated = updated
d.timestamp = time.Now()
switch msg.OutputType {
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
if !nx {
res = resp.SimpleStringValue("OK")
} else if d.updated {
res = resp.IntegerValue(1)
} else {
res = resp.IntegerValue(0)
}
}
return res, d, nil
}
// FLUSHDB
func (s *Server) cmdFLUSHDB(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 1 {
return retwerr(errInvalidNumberOfArguments)
}
// >> Operation
// clear the entire database
// drop each collection
keys := s.cols.Keys()
for _, key := range keys {
s.cmdDROPop(key)
}
// delete all channels
var names []string
s.hooks.Ascend(nil, func(item any) bool {
hook := item.(*Hook)
if hook.channel {
names = append(names, hook.Name)
}
return true
})
for _, name := range names {
s.cmdDELHOOKop(name, true)
}
// delete all hooks
names = names[:0]
s.hooks.Ascend(nil, func(item any) bool {
hook := item.(*Hook)
if !hook.channel {
names = append(names, hook.Name)
}
return true
})
for _, name := range names {
s.cmdDELHOOKop(name, false)
}
s.cols.Clear()
s.groupHooks.Clear()
s.groupObjects.Clear()
s.hookExpires.Clear()
s.hooks.Clear()
s.hooksOut.Clear()
s.hookTree.Clear()
s.hookCross.Clear()
// >> Response
var d commandDetails
d.command = "flushdb"
d.updated = true
d.timestamp = time.Now()
var res resp.Value
if msg.OutputType == JSON {
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
} else {
res = resp.SimpleStringValue("OK")
}
return res, d, nil
}
// SET key id [FIELD name value ...] [EX seconds] [NX|XX]
// (OBJECT geojson)|(POINT lat lon z)|(BOUNDS minlat minlon maxlat maxlon)|
// (HASH geohash)|(STRING value)
func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
if s.config.maxMemory() > 0 && s.outOfMemory.Load() {
return retwerr(errOOM)
}
// >> Args
var key string
var id string
var fields []field.Field
var ex int64
var xx bool
var nx bool
var ret bool
var withfields bool
kind := "object"
var precision int64
var oobj geojson.Object
args := msg.Args
if len(args) < 3 {
return retwerr(errInvalidNumberOfArguments)
}
key, id = args[1], args[2]
for i := 3; i < len(args); i++ {
switch strings.ToLower(args[i]) {
case "field":
if i+2 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
fkey := args[i+1]
fval := args[i+2]
i += 2
if isReservedFieldName(fkey) {
return retwerr(errInvalidArgument(fkey))
}
fields = append(fields, field.Make(fkey, fval))
case "ex":
if i+1 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
exval := args[i+1]
i += 1
x, err := strconv.ParseFloat(exval, 64)
if err != nil {
return retwerr(errInvalidArgument(exval))
}
ex = time.Now().UnixNano() + int64(float64(time.Second)*x)
case "nx":
if xx {
return retwerr(errInvalidArgument(args[i]))
}
nx = true
case "xx":
if nx {
return retwerr(errInvalidArgument(args[i]))
}
xx = true
case "return":
if ret {
return retwerr(errInvalidArgument(args[i]))
}
ret = true
for j := i; j < i+3; j++ {
if j >= len(args) {
break
}
switch strings.ToLower(args[j]) {
case "withfields":
withfields = true
i += 1
case "object":
kind = "object"
i += 1
case "point":
kind = "point"
i += 1
case "bounds":
kind = "bounds"
i += 1
case "hash":
kind = "hash"
j++
if j == len(args) {
return retwerr(errInvalidNumberOfArguments)
}
var err error
precision, err = strconv.ParseInt(args[j], 10, 64)
if err != nil || precision < 1 || precision > 12 {
return retwerr(errInvalidArgument(args[j]))
}
i += 2
}
}
case "string":
if i+1 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
str := args[i+1]
i += 1
oobj = collection.String(str)
case "point":
if i+2 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
slat := args[i+1]
slon := args[i+2]
i += 2
var z float64
var hasZ bool
if i+1 < len(args) {
// probe for possible z coordinate
var err error
z, err = strconv.ParseFloat(args[i+1], 64)
if err == nil {
hasZ = true
i++
}
}
y, err := strconv.ParseFloat(slat, 64)
if err != nil {
return retwerr(errInvalidArgument(slat))
}
x, err := strconv.ParseFloat(slon, 64)
if err != nil {
return retwerr(errInvalidArgument(slon))
}
if !hasZ {
oobj = geojson.NewPoint(geometry.Point{X: x, Y: y})
} else {
oobj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z)
}
case "bounds":
if i+4 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
var vals [4]float64
for j := 0; j < 4; j++ {
var err error
vals[j], err = strconv.ParseFloat(args[i+1+j], 64)
if err != nil {
return retwerr(errInvalidArgument(args[i+1+j]))
}
}
i += 4
oobj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: vals[1], Y: vals[0]},
Max: geometry.Point{X: vals[3], Y: vals[2]},
})
case "hash":
if i+1 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
shash := args[i+1]
i += 1
lat, lon := geohash.Decode(shash)
oobj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
case "object":
if i+1 >= len(args) {
return retwerr(errInvalidNumberOfArguments)
}
json := args[i+1]
i += 1
var err error
oobj, err = geojson.Parse(json, &s.geomParseOpts)
if err != nil {
return retwerr(err)
}
default:
return retwerr(errInvalidArgument(args[i]))
}
}
if oobj == nil {
return retwerr(errInvalidNumberOfArguments)
}
// >> Operation
nada := func() (resp.Value, commandDetails, error) {
// exclude operation due to 'xx' or 'nx' match
if msg.OutputType == JSON {
if nx {
return retwerr(errIDAlreadyExists)
} else {
return retwerr(errIDNotFound)
}
}
return resp.NullValue(), commandDetails{}, nil
}
col, ok := s.cols.Get(key)
if !ok {
if xx {
return nada()
}
col = collection.New()
s.cols.Set(key, col)
}
if xx || nx {
if col.Get(id) == nil {
if xx {
return nada()
}
} else {
if nx {
return nada()
}
}
}
var flist field.List
if old := col.Get(id); old != nil {
flist = old.Fields()
}
for _, f := range fields {
flist = flist.Set(f)
}
obj := object.New(id, oobj, ex, flist)
old := col.Set(obj)
// >> Response
var d commandDetails
d.command = "set"
d.key = key
d.obj = obj
d.old = old
d.updated = true // perhaps we should do a diff on the previous object?
d.timestamp = time.Now()
if ret {
res := buildObjectResponse(msg, obj, start, kind, precision, withfields, msg.OutputType == JSON)
return res, d, nil
}
var res resp.Value
switch msg.OutputType {
default:
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
res = resp.SimpleStringValue("OK")
}
return res, d, nil
}
func retwerr(err error) (resp.Value, commandDetails, error) {
return resp.Value{}, commandDetails{}, err
}
func retrerr(err error) (resp.Value, error) {
return resp.Value{}, err
}
// FSET key id [XX] field value [field value...]
func (s *Server) cmdFSET(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
if s.config.maxMemory() > 0 && s.outOfMemory.Load() {
return retwerr(errOOM)
}
// >> Args
var id string
var key string
var xx bool
var ret bool
var withfields bool
kind := "object"
var precision int64
var fields []field.Field // raw fields
args := msg.Args
if len(args) < 5 {
return retwerr(errInvalidNumberOfArguments)
}
key, id = args[1], args[2]
for i := 3; i < len(args); i++ {
arg := args[i]
switch strings.ToLower(arg) {
case "xx":
xx = true
case "return":
if ret {
return retwerr(errInvalidArgument(args[i]))
}
ret = true
for j := i; j < i+3; j++ {
if j >= len(args) {
break
}
switch strings.ToLower(args[j]) {
case "withfields":
withfields = true
i += 1
case "object":
kind = "object"
i += 1
case "point":
kind = "point"
i += 1
case "bounds":
kind = "bounds"
i += 1
case "hash":
kind = "hash"
j++
if j == len(args) {
return retwerr(errInvalidNumberOfArguments)
}
var err error
precision, err = strconv.ParseInt(args[j], 10, 64)
if err != nil || precision < 1 || precision > 12 {
return retwerr(errInvalidArgument(args[j]))
}
i += 2
}
}
default:
fkey := arg
i++
if i == len(args) {
return retwerr(errInvalidNumberOfArguments)
}
if isReservedFieldName(fkey) {
return retwerr(errInvalidArgument(fkey))
}
fval := args[i]
fields = append(fields, field.Make(fkey, fval))
}
}
// >> Operation
var d commandDetails
var updateCount int
col, ok := s.cols.Get(key)
if !ok {
return retwerr(errKeyNotFound)
}
o := col.Get(id)
ok = o != nil
if !(ok || xx) {
return retwerr(errIDNotFound)
}
if ok {
ofields := o.Fields()
for _, f := range fields {
prev := ofields.Get(f.Name())
if !prev.Value().Equals(f.Value()) {
ofields = ofields.Set(f)
updateCount++
}
}
obj := object.New(id, o.Geo(), o.Expires(), ofields)
col.Set(obj)
d.command = "fset"
d.key = key
d.obj = obj
d.timestamp = time.Now()
d.updated = updateCount > 0
}
// >> Response
var res resp.Value
if ret {
res := buildObjectResponse(msg, d.obj, start, kind, precision, withfields, msg.OutputType == JSON)
return res, d, nil
}
switch msg.OutputType {
case JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
res = resp.IntegerValue(updateCount)
}
return res, d, nil
}
// FGET key id field
func (s *Server) cmdFGET(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) < 4 {
return retrerr(errInvalidNumberOfArguments)
}
key, id, field := args[1], args[2], args[3]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
return retrerr(errKeyNotFound)
}
o := col.Get(id)
if o == nil {
return retrerr(errIDNotFound)
}
f := o.Fields().Get(field)
// >> Response
var buf bytes.Buffer
switch msg.OutputType {
case JSON:
buf.WriteString(`{"ok":true`)
buf.WriteString(`,"value":` + f.Value().JSON())
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
case RESP:
return resp.StringValue(f.Value().Data()), nil
}
return NOMessage, nil
}
// EXPIRE key id seconds
func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 4 {
return retwerr(errInvalidNumberOfArguments)
}
key, id, svalue := args[1], args[2], args[3]
value, err := strconv.ParseFloat(svalue, 64)
if err != nil {
return retwerr(errInvalidArgument(svalue))
}
// >> Operation
var ok bool
var obj *object.Object
col, _ := s.cols.Get(key)
if col != nil {
// replace the expiration by getting the old object
ex := time.Now().Add(
time.Duration(float64(time.Second) * value)).UnixNano()
o := col.Get(id)
ok = o != nil
if ok {
obj = object.New(id, o.Geo(), ex, o.Fields())
col.Set(obj)
}
}
// >> Response
var d commandDetails
if ok {
d.key = key
d.obj = obj
d.command = "expire"
d.updated = true
d.timestamp = time.Now()
}
var res resp.Value
switch msg.OutputType {
case JSON:
if ok {
res = resp.StringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
} else if col == nil {
return retwerr(errKeyNotFound)
} else {
return retwerr(errIDNotFound)
}
case RESP:
if ok {
res = resp.IntegerValue(1)
} else {
res = resp.IntegerValue(0)
}
}
return res, d, nil
}
// PERSIST key id
func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.IntegerValue(0), commandDetails{}, nil
}
return retwerr(errKeyNotFound)
}
o := col.Get(id)
if o == nil {
if msg.OutputType == RESP {
return resp.IntegerValue(0), commandDetails{}, nil
}
return retwerr(errIDNotFound)
}
var obj *object.Object
var cleared bool
if o.Expires() != 0 {
obj = object.New(id, o.Geo(), 0, o.Fields())
col.Set(obj)
cleared = true
}
// >> Response
var res resp.Value
var d commandDetails
d.command = "persist"
d.key = key
d.obj = obj
d.updated = cleared
d.timestamp = time.Now()
switch msg.OutputType {
case JSON:
res = resp.SimpleStringValue(`{"ok":true,"elapsed":"` +
time.Since(start).String() + "\"}")
case RESP:
if cleared {
res = resp.IntegerValue(1)
} else {
res = resp.IntegerValue(0)
}
}
return res, d, nil
}
// TTL key id
func (s *Server) cmdTTL(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == JSON {
return retrerr(errKeyNotFound)
}
return resp.IntegerValue(-2), nil
}
o := col.Get(id)
if o == nil {
if msg.OutputType == JSON {
return retrerr(errIDNotFound)
}
return resp.IntegerValue(-2), nil
}
var ttl float64
if o.Expires() == 0 {
ttl = -1
} else {
now := start.UnixNano()
ttl = math.Max(float64(o.Expires()-now)/float64(time.Second), 0)
}
// >> Response
if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"ttl":` + strconv.Itoa(int(ttl)) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.IntegerValue(int(ttl)), nil
}
// EXISTS key id
func (s *Server) cmdEXISTS(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
return retrerr(errKeyNotFound)
}
o := col.Get(id)
exists := o != nil
// >> Response
if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"exists":` + strconv.FormatBool(exists) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.BoolValue(exists), nil
}
// FEXISTS key id field
func (s *Server) cmdFEXISTS(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 4 {
return retrerr(errInvalidNumberOfArguments)
}
key, id, field := args[1], args[2], args[3]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
return retrerr(errKeyNotFound)
}
o := col.Get(id)
if o == nil {
return retrerr(errIDNotFound)
}
f := o.Fields().Get(field)
exists := f.Name() != ""
// >> Response
if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"exists":` + strconv.FormatBool(exists) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.BoolValue(exists), nil
}
================================================
FILE: internal/server/dev.go
================================================
package server
import (
"errors"
"fmt"
"math/rand"
"strconv"
"sync/atomic"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
// MASSINSERT num_keys num_points [minx miny maxx maxy]
func randMassInsertPosition(minLat, minLon, maxLat, maxLon float64) (float64, float64) {
lat, lon := (rand.Float64()*(maxLat-minLat))+minLat, (rand.Float64()*(maxLon-minLon))+minLon
return lat, lon
}
func (s *Server) cmdMassInsert(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
minLat, minLon, maxLat, maxLon := -90.0, -180.0, 90.0, 180.0 //37.10776, -122.67145, 38.19502, -121.62775
var snumCols, snumPoints string
var cols, objs int
var ok bool
if vs, snumCols, ok = tokenval(vs); !ok || snumCols == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, snumPoints, ok = tokenval(vs); !ok || snumPoints == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
var sminLat, sminLon, smaxLat, smaxLon string
if vs, sminLat, ok = tokenval(vs); !ok || sminLat == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, sminLon, ok = tokenval(vs); !ok || sminLon == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, smaxLat, ok = tokenval(vs); !ok || smaxLat == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, smaxLon, ok = tokenval(vs); !ok || smaxLon == "" {
return NOMessage, errInvalidNumberOfArguments
}
var err error
if minLat, err = strconv.ParseFloat(sminLat, 64); err != nil {
return NOMessage, err
}
if minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
return NOMessage, err
}
if maxLat, err = strconv.ParseFloat(smaxLat, 64); err != nil {
return NOMessage, err
}
if maxLon, err = strconv.ParseFloat(smaxLon, 64); err != nil {
return NOMessage, err
}
if len(vs) != 0 {
return NOMessage, errors.New("invalid number of arguments")
}
}
n, err := strconv.ParseUint(snumCols, 10, 64)
if err != nil {
return NOMessage, errInvalidArgument(snumCols)
}
cols = int(n)
n, err = strconv.ParseUint(snumPoints, 10, 64)
if err != nil {
return NOMessage, errInvalidArgument(snumPoints)
}
docmd := func(args []string) error {
s.mu.Lock()
defer s.mu.Unlock()
nmsg := *msg
nmsg._command = ""
nmsg.Args = args
_, d, err := s.command(&nmsg, nil)
if err != nil {
return err
}
return s.writeAOF(nmsg.Args, &d)
}
rand.Seed(time.Now().UnixNano())
objs = int(n)
var k atomic.Uint64
for i := 0; i < cols; i++ {
key := "mi:" + strconv.FormatInt(int64(i), 10)
func(key string) {
// lock cycle
for j := 0; j < objs; j++ {
id := strconv.FormatInt(int64(j), 10)
var values []string
values = append(values, "set", key, id)
fvals := []float64{
1, // one
0, // zero
-1, // negOne
14, // nibble
20.5, // tinyDiv10
120, // int8
-120, // int8
20000, // int16
-20000, // int16
214748300, // int32
-214748300, // int32
2014748300, // float64
123.12312301, // float64
}
for i, fval := range fvals {
values = append(values, "FIELD",
fmt.Sprintf("fname:%d", i),
strconv.FormatFloat(fval, 'f', -1, 64))
}
if rand.Int()%2 == 0 {
values = append(values, "EX", fmt.Sprint(rand.Intn(25)+5))
}
if j%8 == 0 {
values = append(values, "STRING", fmt.Sprintf("str%v", j))
} else {
lat, lon := randMassInsertPosition(minLat, minLon, maxLat, maxLon)
values = append(values, "POINT",
strconv.FormatFloat(lat, 'f', -1, 64),
strconv.FormatFloat(lon, 'f', -1, 64),
)
}
err := docmd(values)
if err != nil {
log.Fatal(err)
return
}
k.Add(1)
if j%1000 == 1000-1 {
log.Debugf("massinsert: %s %d/%d", key, k.Load(), cols*objs)
}
}
}(key)
}
log.Infof("massinsert: done %d objects", k.Load())
return OKMessage(msg, start), nil
}
func (s *Server) cmdSleep(msg *Message) (res resp.Value, err error) {
start := time.Now()
if len(msg.Args) != 2 {
return NOMessage, errInvalidNumberOfArguments
}
d, _ := strconv.ParseFloat(msg.Args[1], 64)
time.Sleep(time.Duration(float64(time.Second) * d))
return OKMessage(msg, start), nil
}
================================================
FILE: internal/server/expire.go
================================================
package server
import (
"sync"
"time"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/object"
)
const bgExpireDelay = time.Second / 10
// backgroundExpiring deletes expired items from the database.
// It's executes every 1/10 of a second.
func (s *Server) backgroundExpiring(wg *sync.WaitGroup) {
defer wg.Done()
s.loopUntilServerStops(bgExpireDelay, func() {
s.mu.LockLowPriority()
defer s.mu.Unlock()
now := time.Now()
s.backgroundExpireObjects(now)
s.backgroundExpireHooks(now)
})
}
func (s *Server) backgroundExpireObjects(now time.Time) {
nano := now.UnixNano()
var msgs []*Message
s.cols.Scan(func(key string, col *collection.Collection) bool {
col.ScanExpires(func(o *object.Object) bool {
if nano < o.Expires() {
return false
}
s.statsExpired.Add(1)
msgs = append(msgs, &Message{Args: []string{"del", key, o.ID()}})
return true
})
return true
})
for _, msg := range msgs {
_, d, err := s.cmdDEL(msg)
if err != nil {
log.Fatal(err)
}
if err := s.writeAOF(msg.Args, &d); err != nil {
log.Fatal(err)
}
}
if len(msgs) > 0 {
log.Debugf("Expired %d objects\n", len(msgs))
}
}
func (s *Server) backgroundExpireHooks(now time.Time) {
var msgs []*Message
s.hookExpires.Ascend(nil, func(v interface{}) bool {
h := v.(*Hook)
if h.expires.After(now) {
return false
}
msg := &Message{}
if h.channel {
msg.Args = []string{"delchan", h.Name}
} else {
msg.Args = []string{"delhook", h.Name}
}
msgs = append(msgs, msg)
return true
})
for _, msg := range msgs {
_, d, err := s.cmdDelHook(msg)
if err != nil {
log.Fatal(err)
}
if err := s.writeAOF(msg.Args, &d); err != nil {
log.Fatal(err)
}
}
if len(msgs) > 0 {
log.Debugf("Expired %d hooks\n", len(msgs))
}
}
================================================
FILE: internal/server/expr.go
================================================
package server
import (
"fmt"
"regexp"
"sync"
"github.com/tidwall/expr"
"github.com/tidwall/geojson"
"github.com/tidwall/gjson"
"github.com/tidwall/match"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/object"
"github.com/tidwall/tinylru"
)
type exprPool struct {
pool *sync.Pool
regexCache tinylru.LRUG[string, *regexp.Regexp]
}
func typeForObject(o *object.Object) expr.Value {
switch o.Geo().(type) {
case *geojson.Point, *geojson.SimplePoint:
return expr.String("Point")
case *geojson.LineString:
return expr.String("LineString")
case *geojson.Polygon, *geojson.Circle, *geojson.Rect:
return expr.String("Polygon")
case *geojson.MultiPoint:
return expr.String("MultiPoint")
case *geojson.MultiLineString:
return expr.String("MultiLineString")
case *geojson.MultiPolygon:
return expr.String("MultiPolygon")
case *geojson.GeometryCollection:
return expr.String("GeometryCollection")
case *geojson.Feature:
return expr.String("Feature")
case *geojson.FeatureCollection:
return expr.String("FeatureCollection")
default:
return expr.Undefined
}
}
func resultToValue(r gjson.Result) expr.Value {
if !r.Exists() {
return expr.Undefined
}
switch r.Type {
case gjson.String:
return expr.String(r.String())
case gjson.False:
return expr.Bool(false)
case gjson.True:
return expr.Bool(true)
case gjson.Number:
return expr.Number(r.Float())
case gjson.JSON:
return expr.Object(r)
default:
return expr.Null
}
}
func objExpr(o *object.Object, info expr.RefInfo) (expr.Value, error) {
if r := gjson.Get(o.Geo().Members(), info.Ident); r.Exists() {
return resultToValue(r), nil
}
switch info.Ident {
case "id":
return expr.String(o.ID()), nil
case "type":
return typeForObject(o), nil
default:
var rf field.Field
var ok bool
o.Fields().Scan(func(f field.Field) bool {
if f.Name() == info.Ident {
rf = f
ok = true
return false
}
return true
})
if ok {
r := gjson.Parse(rf.Value().JSON())
return resultToValue(r), nil
}
}
return expr.Number(0), nil
}
func newExprPool(s *Server) *exprPool {
pool := &exprPool{}
ext := expr.NewExtender(
// ref
func(info expr.RefInfo, ctx *expr.Context) (expr.Value, error) {
o := ctx.UserData.(*object.Object)
if !info.Chain {
// root
if info.Ident == "this" {
return expr.Object(o), nil
}
return objExpr(o, info)
} else {
switch v := info.Value.Value().(type) {
case *object.Object:
return objExpr(o, info)
case gjson.Result:
return resultToValue(v.Get(info.Ident)), nil
default:
// object methods
switch info.Ident {
case "match":
return expr.Function("match"), nil
}
}
return expr.Undefined, nil
}
},
// call
func(info expr.CallInfo, ctx *expr.Context) (expr.Value, error) {
if info.Chain {
switch info.Ident {
case "match":
if info.Args.Len() < 0 {
return expr.Undefined, nil
}
t := match.MatchNoCase(info.Value.String(),
info.Args.At(0).String())
return expr.Bool(t), nil
}
}
return expr.Undefined, nil
},
// op
func(info expr.OpInfo, ctx *expr.Context) (expr.Value, error) {
switch info.Op {
case expr.OpRegex:
field := info.Left.String()
pattern := info.Right.String()
re, ok := pool.regexCache.Get(pattern)
if !ok {
var err error
re, err = regexp.Compile(pattern)
if err != nil {
return expr.Undefined,
fmt.Errorf("invalid regex pattern: %v", err)
}
pool.regexCache.Set(pattern, re)
}
return expr.Bool(re.MatchString(field)), nil
}
return expr.Undefined, nil
},
)
pool.pool = &sync.Pool{
New: func() any {
ctx := &expr.Context{
Extender: ext,
}
return ctx
},
}
return pool
}
func (p *exprPool) Get(o *object.Object) *expr.Context {
ctx := p.pool.Get().(*expr.Context)
ctx.UserData = o
ctx.NoCase = true
return ctx
}
func (p *exprPool) Put(ctx *expr.Context) {
p.pool.Put(ctx)
}
func (where whereT) matchExpr(s *Server, o *object.Object) bool {
ctx := s.epool.Get(o)
res, err := expr.Eval(where.name, ctx)
if err != nil {
log.Debugf("%v", err)
}
s.epool.Put(ctx)
return res.Bool()
}
================================================
FILE: internal/server/expression.go
================================================
package server
import (
"strings"
"github.com/tidwall/geojson"
)
// BinaryOp represents various operators for expressions
type BinaryOp byte
// expression operator enum
const (
NOOP BinaryOp = iota
AND
OR
tokenAND = "and"
tokenOR = "or"
tokenNOT = "not"
tokenLParen = "("
tokenRParen = ")"
)
// areaExpression is (maybe negated) either an spatial object or operator +
// children (other expressions).
type areaExpression struct {
negate bool
obj geojson.Object
op BinaryOp
children children
}
type children []*areaExpression
// String representation, helpful in logging.
func (e *areaExpression) String() (res string) {
if e.obj != nil {
res = e.obj.String()
} else {
var chStrings []string
for _, c := range e.children {
chStrings = append(chStrings, c.String())
}
switch e.op {
case NOOP:
res = "empty operator"
case AND:
res = "(" + strings.Join(chStrings, " "+tokenAND+" ") + ")"
case OR:
res = "(" + strings.Join(chStrings, " "+tokenOR+" ") + ")"
default:
res = "unknown operator"
}
}
if e.negate {
res = tokenNOT + " " + res
}
return
}
// Return boolean value modulo negate field of the expression.
func (e *areaExpression) maybeNegate(val bool) bool {
if e.negate {
return !val
}
return val
}
// Methods for testing an areaExpression against the spatial object.
func (e *areaExpression) testObject(
o geojson.Object,
objObjTest func(o1, o2 geojson.Object) bool,
exprObjTest func(ae *areaExpression, ob geojson.Object) bool,
) bool {
if e.obj != nil {
return objObjTest(e.obj, o)
}
switch e.op {
case AND:
for _, c := range e.children {
if !exprObjTest(c, o) {
return false
}
}
return true
case OR:
for _, c := range e.children {
if exprObjTest(c, o) {
return true
}
}
return false
}
return false
}
func (e *areaExpression) rawIntersects(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Intersects, (*areaExpression).Intersects)
}
func (e *areaExpression) rawContains(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Contains, (*areaExpression).Contains)
}
func (e *areaExpression) rawWithin(o geojson.Object) bool {
return e.testObject(o, geojson.Object.Within, (*areaExpression).Within)
}
func (e *areaExpression) Intersects(o geojson.Object) bool {
return e.maybeNegate(e.rawIntersects(o))
}
func (e *areaExpression) Contains(o geojson.Object) bool {
return e.maybeNegate(e.rawContains(o))
}
func (e *areaExpression) Within(o geojson.Object) bool {
return e.maybeNegate(e.rawWithin(o))
}
// Methods for testing an areaExpression against another areaExpression.
func (e *areaExpression) testExpression(
other *areaExpression,
exprObjTest func(ae *areaExpression, ob geojson.Object) bool,
rawExprExprTest func(ae1, ae2 *areaExpression) bool,
exprExprTest func(ae1, ae2 *areaExpression) bool,
) bool {
if other.negate {
oppositeExp := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children}
nonNegateOther := &areaExpression{obj: other.obj, op: other.op, children: other.children}
return exprExprTest(oppositeExp, nonNegateOther)
}
if other.obj != nil {
return exprObjTest(e, other.obj)
}
switch other.op {
case AND:
for _, c := range other.children {
if !rawExprExprTest(e, c) {
return false
}
}
return true
case OR:
for _, c := range other.children {
if rawExprExprTest(e, c) {
return true
}
}
return false
}
return false
}
func (e *areaExpression) rawIntersectsExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawIntersects,
(*areaExpression).rawIntersectsExpr,
(*areaExpression).IntersectsExpr)
}
func (e *areaExpression) rawWithinExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawWithin,
(*areaExpression).rawWithinExpr,
(*areaExpression).WithinExpr)
}
func (e *areaExpression) rawContainsExpr(other *areaExpression) bool {
return e.testExpression(
other,
(*areaExpression).rawContains,
(*areaExpression).rawContainsExpr,
(*areaExpression).ContainsExpr)
}
func (e *areaExpression) IntersectsExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawIntersectsExpr(other))
}
func (e *areaExpression) WithinExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawWithinExpr(other))
}
func (e *areaExpression) ContainsExpr(other *areaExpression) bool {
return e.maybeNegate(e.rawContainsExpr(other))
}
================================================
FILE: internal/server/fence.go
================================================
package server
import (
"math"
"sort"
"strconv"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geo"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/object"
)
// FenceMatch executes a fence match returns back json messages for fence detection.
func FenceMatch(hookName string, sw *scanWriter, fence *liveFenceSwitches, metas []FenceMeta, details *commandDetails) []string {
msgs := fenceMatch(hookName, sw, fence, metas, details)
if len(fence.accept) == 0 {
return msgs
}
nmsgs := make([]string, 0, len(msgs))
for _, msg := range msgs {
if fence.accept[gjson.Get(msg, "command").String()] {
nmsgs = append(nmsgs, msg)
}
}
return nmsgs
}
func appendHookDetails(b []byte, hookName string, metas []FenceMeta) []byte {
if len(hookName) > 0 {
b = append(b, `,"hook":`...)
b = appendJSONString(b, hookName)
}
if len(metas) > 0 {
b = append(b, `,"meta":{`...)
for i, meta := range metas {
if i > 0 {
b = append(b, ',')
}
b = appendJSONString(b, meta.Name)
b = append(b, ':')
b = appendJSONString(b, meta.Value)
}
b = append(b, '}')
}
return b
}
func objIsSpatial(obj geojson.Object) bool {
_, ok := obj.(geojson.Spatial)
return ok
}
func hookJSONString(hookName string, metas []FenceMeta) string {
return string(appendHookDetails(nil, hookName, metas))
}
func multiGlobMatch(globs []string, s string) bool {
if len(globs) == 0 || (len(globs) == 1 && globs[0] == "*") {
return true
}
for _, pattern := range globs {
match, _ := glob.Match(pattern, s)
if match {
return true
}
}
return false
}
func fenceMatch(
hookName string, sw *scanWriter, fence *liveFenceSwitches,
metas []FenceMeta, details *commandDetails,
) []string {
if details.command == "drop" {
return []string{
`{"command":"drop"` + hookJSONString(hookName, metas) +
`,"key":` + jsonString(details.key) +
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
}
}
if details.obj == nil {
return nil
}
if !multiGlobMatch(fence.globs, details.obj.ID()) {
return nil
}
if !objIsSpatial(details.obj.Geo()) {
return nil
}
if details.command == "fset" {
nofields := sw.nofields
if nofields {
return nil
}
}
if details.command == "del" {
return []string{
`{"command":"del"` + hookJSONString(hookName, metas) +
`,"key":` + jsonString(details.key) +
`,"id":` + jsonString(details.obj.ID()) +
`,"time":` + jsonTimeFormat(details.timestamp) + `}`,
}
}
var roamNearbys, roamFaraways []roamMatch
var detect = "outside"
if fence != nil {
if fence.roam.on {
if details.command == "set" {
roamNearbys, roamFaraways =
fenceMatchRoam(sw.s, fence, details.obj, details.old)
if len(roamNearbys) == 0 && len(roamFaraways) == 0 {
return nil
}
}
detect = "roam"
} else {
var nocross bool
// not using roaming
match1 := fenceMatchObject(fence, details.old)
if match1 {
match1, _, _ = sw.testObject(details.old)
nocross = !match1
}
match2 := fenceMatchObject(fence, details.obj)
if match2 {
match2, _, _ = sw.testObject(details.obj)
nocross = !match2
}
if match1 && match2 {
detect = "inside"
} else if match1 && !match2 {
detect = "exit"
} else if !match1 && match2 {
detect = "enter"
if details.command == "fset" {
detect = "inside"
}
} else {
if details.command != "fset" {
// For cross detection, the object is outside the fence spatially,
// so testObject wasn't called above. We need to check WHERE clause
// before proceeding with cross detection.
if match, _ := sw.fieldMatch(details.obj); !match {
return nil
}
// Maybe the old object and new object create a line that crosses the fence.
// Must detect for that possibility.
if !nocross && details.old != nil {
ls := geojson.NewLineString(geometry.NewLine(
[]geometry.Point{
details.old.Geo().Center(),
details.obj.Geo().Center(),
}, nil))
temp := false
if fence.cmd == "within" {
// because we are testing if the line croses the area we need to use
// "intersects" instead of "within".
fence.cmd = "intersects"
temp = true
}
lso := object.New("", ls, 0, field.List{})
if fenceMatchObject(fence, lso) {
detect = "cross"
}
if temp {
fence.cmd = "within"
}
}
}
}
}
}
// TODO: fields
// if details.fmap == nil {
// return nil
// }
for {
if fence.detect != nil && !fence.detect[detect] {
if detect == "enter" {
detect = "inside"
continue
}
if detect == "exit" || detect == "cross" {
detect = "outside"
continue
}
return nil
}
break
}
var distance float64
if fence.distance && fence.obj != nil {
distance = details.obj.Geo().Distance(fence.obj)
}
sw.fullFields = true
sw.msg.OutputType = JSON
sw.writeObject(ScanWriterParams{
obj: details.obj,
noTest: true,
dist: distance,
distOutput: fence.distance,
})
if sw.wr.Len() == 0 {
return nil
}
res := sw.wr.String()
sw.wr.Reset()
if len(res) > 0 && res[0] == ',' {
res = res[1:]
}
if sw.output == outputIDs {
res = `{"id":` + string(res) + `}`
}
var group string
if detect == "enter" {
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
} else if detect == "cross" {
sw.s.groupDisconnect(hookName, details.key, details.obj.ID())
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
} else {
group = sw.s.groupGet(hookName, details.key, details.obj.ID())
if group == "" {
group = sw.s.groupConnect(hookName, details.key, details.obj.ID())
}
}
var msgs []string
if fence.detect == nil || fence.detect[detect] {
if len(res) > 0 && res[0] == '{' {
msgs = append(msgs, makemsg(details.command, group, detect,
hookName, metas, details.key, details.timestamp, res[1:]))
} else {
msgs = append(msgs, string(res))
}
}
switch detect {
case "enter":
if fence.detect == nil || fence.detect["inside"] {
msgs = append(msgs, makemsg(details.command, group, "inside", hookName, metas, details.key, details.timestamp, res[1:]))
}
case "exit", "cross":
if fence.detect == nil || fence.detect["outside"] {
msgs = append(msgs, makemsg(details.command, group, "outside", hookName, metas, details.key, details.timestamp, res[1:]))
}
case "roam":
if len(msgs) > 0 {
var nmsgs []string
for _, msg := range msgs {
cmd := gjson.Get(msg, "command")
if cmd.Exists() && cmd.String() != "set" {
nmsgs = append(nmsgs, msg)
}
}
for i := range roamNearbys {
nmsg := extendRoamMessage(sw, fence,
"nearby", msgs[0], roamNearbys[i])
nmsgs = append(nmsgs, string(nmsg))
}
for i := range roamFaraways {
nmsg := extendRoamMessage(sw, fence,
"faraway", msgs[0], roamFaraways[i])
nmsgs = append(nmsgs, string(nmsg))
}
msgs = nmsgs
}
}
return msgs
}
func extendRoamMessage(
sw *scanWriter, fence *liveFenceSwitches,
kind string, baseMsg string, match roamMatch,
) string {
// hack off the last '}'
nmsg := []byte(baseMsg[:len(baseMsg)-1])
nmsg = append(nmsg, `,"`+kind+`":{"key":`...)
nmsg = appendJSONString(nmsg, fence.roam.key)
nmsg = append(nmsg, `,"id":`...)
nmsg = appendJSONString(nmsg, match.id)
nmsg = append(nmsg, `,"object":`...)
nmsg = match.obj.AppendJSON(nmsg)
nmsg = append(nmsg, `,"meters":`...)
nmsg = strconv.AppendFloat(nmsg,
math.Floor(match.meters*1000)/1000, 'f', -1, 64)
if fence.roam.scan != "" {
nmsg = append(nmsg, `,"scan":[`...)
col, _ := sw.s.cols.Get(fence.roam.key)
if col != nil {
o := col.Get(match.id)
if o != nil {
nmsg = append(nmsg, `{"id":`...)
nmsg = appendJSONString(nmsg, match.id)
nmsg = append(nmsg, `,"self":true,"object":`...)
nmsg = o.Geo().AppendJSON(nmsg)
nmsg = append(nmsg, '}')
}
pattern := match.id + fence.roam.scan
iterator := func(o *object.Object) bool {
if o.ID() == match.id {
return true
}
if matched, _ := glob.Match(pattern, o.ID()); matched {
nmsg = append(nmsg, `,{"id":`...)
nmsg = appendJSONString(nmsg, o.ID())
nmsg = append(nmsg, `,"object":`...)
nmsg = o.Geo().AppendJSON(nmsg)
nmsg = append(nmsg, '}')
}
return true
}
g := glob.Parse(pattern, false)
if g.Limits[0] == "" && g.Limits[1] == "" {
col.Scan(false, nil, nil, iterator)
} else {
col.ScanRange(g.Limits[0], g.Limits[1],
false, nil, nil, iterator)
}
}
nmsg = append(nmsg, ']')
}
nmsg = append(nmsg, '}')
// re-add the last '}'
nmsg = append(nmsg, '}')
return string(nmsg)
}
func makemsg(
command, group, detect, hookName string,
metas []FenceMeta, key string, t time.Time, tail string,
) string {
var buf []byte
buf = append(append(buf, `{"command":"`...), command...)
buf = append(append(buf, `","group":"`...), group...)
buf = append(append(buf, `","detect":"`...), detect...)
buf = append(buf, '"')
buf = appendHookDetails(buf, hookName, metas)
buf = appendJSONString(append(buf, `,"key":`...), key)
buf = appendJSONTimeFormat(append(buf, `,"time":`...), t)
buf = append(append(buf, ','), tail...)
return string(buf)
}
func fenceMatchObject(fence *liveFenceSwitches, o *object.Object) bool {
if o == nil {
return false
}
if fence.roam.on {
// we need to check this object against
return false
}
switch fence.cmd {
case "nearby":
// nearby is an INTERSECT on a Circle
return o.Geo().Intersects(fence.obj)
case "within":
return o.Geo().Within(fence.obj)
case "intersects":
return o.Geo().Intersects(fence.obj)
}
return false
}
func fenceMatchNearbys(
s *Server, fence *liveFenceSwitches,
obj *object.Object,
) (nearbys []roamMatch) {
if obj == nil {
return nil
}
col, _ := s.cols.Get(fence.roam.key)
if col == nil {
return nil
}
center := obj.Geo().Center()
minLat, minLon, maxLat, maxLon :=
geo.RectFromCenter(center.Y, center.X, fence.roam.meters)
rect := geometry.Rect{
Min: geometry.Point{X: minLon, Y: minLat},
Max: geometry.Point{X: maxLon, Y: maxLat},
}
col.Intersects(geojson.NewRect(rect), 0, nil, nil,
func(o *object.Object) bool {
var idMatch bool
if o.ID() == obj.ID() {
return true // skip self
}
meters := o.Geo().Distance(o.Geo())
if meters > fence.roam.meters {
return true // skip outside radius
}
if fence.roam.pattern {
idMatch, _ = glob.Match(fence.roam.id, o.ID())
} else {
idMatch = fence.roam.id == o.ID()
}
if !idMatch {
return true // skip non-id match
}
match := roamMatch{
id: o.ID(),
obj: o.Geo(),
meters: obj.Geo().Distance(o.Geo()),
}
nearbys = append(nearbys, match)
return true
},
)
return nearbys
}
func fenceMatchRoam(
s *Server, fence *liveFenceSwitches,
obj, old *object.Object,
) (nearbys, faraways []roamMatch) {
oldNearbys := fenceMatchNearbys(s, fence, old)
newNearbys := fenceMatchNearbys(s, fence, obj)
// Go through all matching objects in new-nearbys and old-nearbys.
for i := 0; i < len(oldNearbys); i++ {
var match bool
var j int
for ; j < len(newNearbys); j++ {
if newNearbys[j].id == oldNearbys[i].id {
match = true
break
}
}
if match {
// dwelling, more from old-nearbys
oldNearbys[i] = oldNearbys[len(oldNearbys)-1]
oldNearbys = oldNearbys[:len(oldNearbys)-1]
i--
if fence.nodwell {
// no dwelling allowed, remove from both lists
newNearbys[j] = newNearbys[len(newNearbys)-1]
newNearbys = newNearbys[:len(newNearbys)-1]
}
}
}
faraways, nearbys = oldNearbys, newNearbys
// ensure the faraways distances are to the new object
for i := 0; i < len(faraways); i++ {
faraways[i].meters = faraways[i].obj.Distance(obj.Geo())
}
sortRoamMatches(faraways)
sortRoamMatches(nearbys)
return nearbys, faraways
}
// sortRoamMatches stable sorts roam matches
func sortRoamMatches(matches []roamMatch) {
sort.Slice(matches, func(i, j int) bool {
if matches[i].meters < matches[j].meters {
return true
}
if matches[i].meters > matches[j].meters {
return false
}
return matches[i].id < matches[j].id
})
}
================================================
FILE: internal/server/follow.go
================================================
package server
import (
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/log"
)
var errNoLongerFollowing = errors.New("no longer following")
const checksumsz = 512 * 1024
func (s *Server) cmdFollow(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var host, sport string
if vs, host, ok = tokenval(vs); !ok || host == "" {
return NOMessage, errInvalidNumberOfArguments
}
if vs, sport, ok = tokenval(vs); !ok || sport == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
host = strings.ToLower(host)
sport = strings.ToLower(sport)
var update bool
if host == "no" && sport == "one" {
update = s.config.followHost() != "" || s.config.followPort() != 0
s.config.setFollowHost("")
s.config.setFollowPort(0)
} else {
n, err := strconv.ParseUint(sport, 10, 64)
if err != nil {
return NOMessage, errInvalidArgument(sport)
}
port := int(n)
update = s.config.followHost() != host || s.config.followPort() != port
auth := s.config.leaderAuth()
if update {
s.mu.Unlock()
conn, err := DialTimeout(fmt.Sprintf("%s:%d", host, port), time.Second*2)
if err != nil {
s.mu.Lock()
return NOMessage, fmt.Errorf("cannot follow: %v", err)
}
defer conn.Close()
if auth != "" {
if err := s.followDoLeaderAuth(conn, auth); err != nil {
return NOMessage, fmt.Errorf("cannot follow: %v", err)
}
}
m, err := doServer(conn)
if err != nil {
s.mu.Lock()
return NOMessage, fmt.Errorf("cannot follow: %v", err)
}
if m["id"] == "" {
s.mu.Lock()
return NOMessage, fmt.Errorf("cannot follow: invalid id")
}
if m["id"] == s.config.serverID() {
s.mu.Lock()
return NOMessage, fmt.Errorf("cannot follow self")
}
if m["following"] != "" {
s.mu.Lock()
return NOMessage, fmt.Errorf("cannot follow a follower")
}
s.mu.Lock()
}
s.config.setFollowHost(host)
s.config.setFollowPort(port)
}
s.config.write(false)
if update {
s.followc.Add(1)
if s.config.followHost() != "" {
log.Infof("following new host '%s' '%s'.", host, sport)
go s.follow(s.config.followHost(), s.config.followPort(),
int(s.followc.Load()))
} else {
log.Infof("following no one")
}
}
return OKMessage(msg, start), nil
}
// cmdReplConf is a command handler that sets replication configuration info
func (s *Server) cmdReplConf(msg *Message, client *Client) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var cmd, val string
// Parse the message
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return NOMessage, errInvalidNumberOfArguments
}
if _, val, ok = tokenval(vs); !ok || val == "" {
return NOMessage, errInvalidNumberOfArguments
}
// Switch on the command received
switch cmd {
case "listening-port":
// Parse the port as an integer
port, err := strconv.Atoi(val)
if err != nil {
return NOMessage, errInvalidArgument(val)
}
// Apply the replication port to the client and return
s.connsmu.RLock()
defer s.connsmu.RUnlock()
for _, c := range s.conns {
if c.remoteAddr == client.remoteAddr {
c.mu.Lock()
c.replPort = port
c.mu.Unlock()
return OKMessage(msg, start), nil
}
}
case "ip-address":
// Apply the replication ip to the client and return
s.connsmu.RLock()
defer s.connsmu.RUnlock()
for _, c := range s.conns {
if c.remoteAddr == client.remoteAddr {
c.mu.Lock()
c.replAddr = val
c.mu.Unlock()
return OKMessage(msg, start), nil
}
}
}
return NOMessage, fmt.Errorf("cannot find follower")
}
func doServer(conn *RESPConn) (map[string]string, error) {
v, err := conn.Do("server")
if err != nil {
return nil, err
}
if v.Error() != nil {
return nil, v.Error()
}
arr := v.Array()
m := make(map[string]string)
for i := 0; i < len(arr)/2; i++ {
m[arr[i*2+0].String()] = arr[i*2+1].String()
}
return m, err
}
func (s *Server) followHandleCommand(args []string, followc int, w io.Writer) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if int(s.followc.Load()) != followc {
return s.aofsz, errNoLongerFollowing
}
msg := &Message{Args: args}
_, d, err := s.command(msg, nil)
if err != nil {
if commandErrIsFatal(err) {
return s.aofsz, err
}
}
switch msg.Command() {
case "publish":
// Avoid writing these commands to the AOF
default:
if err := s.writeAOF(args, &d); err != nil {
return s.aofsz, err
}
}
if len(s.aofbuf) > 10240 {
s.flushAOF(false)
}
return s.aofsz, nil
}
func (s *Server) followDoLeaderAuth(conn *RESPConn, auth string) error {
v, err := conn.Do("auth", auth)
if err != nil {
return err
}
if v.Error() != nil {
return v.Error()
}
if v.String() != "OK" {
return errors.New("cannot follow: auth no ok")
}
return nil
}
// bit flags for the fcupflags server field
const (
bitCaughtUpOnce int32 = 1 // follower caught up at least once in the past
bitCaughtUp int32 = 2 // follower is fully caught up to leader
)
func (s *Server) setCaughtUp(caughtUp bool) {
var flags int32
if caughtUp {
flags = bitCaughtUp | bitCaughtUpOnce
} else {
flags = s.fcupflags.Load() & bitCaughtUpOnce
}
s.fcupflags.Store(flags)
}
func (s *Server) caughtUp() bool {
return (s.fcupflags.Load() & bitCaughtUp) == bitCaughtUp
}
func (s *Server) caughtUpOnce() bool {
return (s.fcupflags.Load() & bitCaughtUpOnce) == bitCaughtUpOnce
}
func (s *Server) followStep(host string, port int, followc int) error {
if int(s.followc.Load()) != followc {
return errNoLongerFollowing
}
s.mu.Lock()
s.faofsz = 0
s.setCaughtUp(false)
auth := s.config.leaderAuth()
s.mu.Unlock()
addr := fmt.Sprintf("%s:%d", host, port)
// check if we are following self
conn, err := DialTimeout(addr, time.Second*2)
if err != nil {
return fmt.Errorf("cannot follow: %v", err)
}
defer conn.Close()
if auth != "" {
if err := s.followDoLeaderAuth(conn, auth); err != nil {
return fmt.Errorf("cannot follow: %v", err)
}
}
m, err := doServer(conn)
if err != nil {
return fmt.Errorf("cannot follow: %v", err)
}
if m["id"] == "" {
return fmt.Errorf("cannot follow: invalid id")
}
if m["id"] == s.config.serverID() {
return fmt.Errorf("cannot follow self")
}
if m["following"] != "" {
return fmt.Errorf("cannot follow a follower")
}
// verify checksum
pos, err := s.followCheckSome(addr, followc, auth)
if err != nil {
return err
}
// Send the replication port to the leader
p := s.config.announcePort()
if p == 0 {
p = s.port
}
v, err := conn.Do("replconf", "listening-port", p)
if err != nil {
return err
}
if v.Error() != nil {
return v.Error()
}
if v.String() != "OK" {
return errors.New("invalid response to replconf request")
}
// Send the replication ip to the leader
ip := s.config.announceIP()
if ip != "" {
v, err := conn.Do("replconf", "ip-address", ip)
if err != nil {
return err
}
if v.Error() != nil {
return v.Error()
}
if v.String() != "OK" {
return errors.New("invalid response to replconf request")
}
}
if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":replconf")
}
v, err = conn.Do("aof", pos)
if err != nil {
return err
}
if v.Error() != nil {
return v.Error()
}
if v.String() != "OK" {
return errors.New("invalid response to aof live request")
}
if s.opts.ShowDebugMessages {
log.Debug("follow:", addr, ":read aof")
}
aofSize, err := strconv.ParseInt(m["aof_size"], 10, 64)
if err != nil {
return err
}
s.mu.Lock()
s.faofsz = int(aofSize)
s.mu.Unlock()
caughtUp := pos >= aofSize
if caughtUp {
s.setCaughtUp(true)
log.Info("caught up")
}
nullw := io.Discard
for {
v, telnet, _, err := conn.rd.ReadMultiBulk()
if err != nil {
return err
}
vals := v.Array()
if telnet || v.Type() != resp.Array {
return errors.New("invalid multibulk")
}
svals := make([]string, len(vals))
for i := 0; i < len(vals); i++ {
svals[i] = vals[i].String()
}
aofsz, err := s.followHandleCommand(svals, followc, nullw)
if err != nil {
return err
}
s.mu.Lock()
s.faofsz = aofsz
s.mu.Unlock()
if !caughtUp {
if aofsz >= int(aofSize) {
caughtUp = true
s.mu.Lock()
s.flushAOF(false)
s.setCaughtUp(true)
s.mu.Unlock()
log.Info("caught up")
}
}
}
}
func (s *Server) follow(host string, port int, followc int) {
for {
err := s.followStep(host, port, followc)
if err == errNoLongerFollowing {
return
}
if err != nil && err != io.EOF {
log.Error("follow: " + err.Error())
}
time.Sleep(time.Second)
}
}
================================================
FILE: internal/server/group.go
================================================
package server
import (
"github.com/tidwall/btree"
)
func byGroupHook(va, vb interface{}) bool {
a, b := va.(*groupItem), vb.(*groupItem)
if a.hookName < b.hookName {
return true
}
if a.hookName > b.hookName {
return false
}
if a.colKey < b.colKey {
return true
}
if a.colKey > b.colKey {
return false
}
return a.objID < b.objID
}
func byGroupObject(va, vb interface{}) bool {
a, b := va.(*groupItem), vb.(*groupItem)
if a.colKey < b.colKey {
return true
}
if a.colKey > b.colKey {
return false
}
if a.objID < b.objID {
return true
}
if a.objID > b.objID {
return false
}
return a.hookName < b.hookName
}
type groupItem struct {
hookName string
colKey string
objID string
groupID string
}
func newGroupItem(hookName, colKey, objID string) *groupItem {
groupID := bsonID()
g := &groupItem{}
// create a single string allocation
ustr := hookName + colKey + objID + groupID
var pos int
g.hookName = ustr[pos : pos+len(hookName)]
pos += len(hookName)
g.colKey = ustr[pos : pos+len(colKey)]
pos += len(colKey)
g.objID = ustr[pos : pos+len(objID)]
pos += len(objID)
g.groupID = ustr[pos : pos+len(groupID)]
pos += len(groupID)
return g
}
func (s *Server) groupConnect(hookName, colKey, objID string) (groupID string) {
g := newGroupItem(hookName, colKey, objID)
s.groupHooks.Set(g)
s.groupObjects.Set(g)
return g.groupID
}
func (s *Server) groupDisconnect(hookName, colKey, objID string) {
g := &groupItem{
hookName: hookName,
colKey: colKey,
objID: objID,
}
s.groupHooks.Delete(g)
s.groupObjects.Delete(g)
}
func (s *Server) groupGet(hookName, colKey, objID string) (groupID string) {
v := s.groupHooks.Get(&groupItem{
hookName: hookName,
colKey: colKey,
objID: objID,
})
if v != nil {
return v.(*groupItem).groupID
}
return ""
}
func deleteGroups(s *Server, groups []*groupItem) {
var hhint btree.PathHint
var ohint btree.PathHint
for _, g := range groups {
s.groupHooks.DeleteHint(g, &hhint)
s.groupObjects.DeleteHint(g, &ohint)
}
}
// groupDisconnectObject disconnects all hooks from provide object
func (s *Server) groupDisconnectObject(colKey, objID string) {
var groups []*groupItem
s.groupObjects.Ascend(&groupItem{colKey: colKey, objID: objID},
func(v interface{}) bool {
g := v.(*groupItem)
if g.colKey != colKey || g.objID != objID {
return false
}
groups = append(groups, g)
return true
},
)
deleteGroups(s, groups)
}
// groupDisconnectCollection disconnects all hooks from objects in provided
// collection.
func (s *Server) groupDisconnectCollection(colKey string) {
var groups []*groupItem
s.groupObjects.Ascend(&groupItem{colKey: colKey},
func(v interface{}) bool {
g := v.(*groupItem)
if g.colKey != colKey {
return false
}
groups = append(groups, g)
return true
},
)
deleteGroups(s, groups)
}
// groupDisconnectHook disconnects all objects from provided hook.
func (s *Server) groupDisconnectHook(hookName string) {
var groups []*groupItem
s.groupHooks.Ascend(&groupItem{hookName: hookName},
func(v interface{}) bool {
g := v.(*groupItem)
if g.hookName != hookName {
return false
}
groups = append(groups, g)
return true
},
)
deleteGroups(s, groups)
}
================================================
FILE: internal/server/hooks.go
================================================
package server
import (
"bytes"
"errors"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tidwall/buntdb"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/endpoint"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/log"
)
var hookLogSetDefaults = &buntdb.SetOptions{
Expires: true, // automatically delete after 30 seconds
TTL: time.Second * 30,
}
func byHookName(a, b interface{}) bool {
return a.(*Hook).Name < b.(*Hook).Name
}
func (s *Server) cmdSetHook(msg *Message) (
res resp.Value, d commandDetails, err error,
) {
channel := msg.Command() == "setchan"
start := time.Now()
vs := msg.Args[1:]
var name, urls, cmd string
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
var endpoints []string
if channel {
endpoints = []string{"local://" + name}
} else {
if vs, urls, ok = tokenval(vs); !ok || urls == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
for _, url := range strings.Split(urls, ",") {
url = strings.TrimSpace(url)
err := s.epc.Validate(url)
if err != nil {
log.Errorf("sethook: %v", err)
return resp.SimpleStringValue(""), d, errInvalidArgument(url)
}
endpoints = append(endpoints, url)
}
}
var commandvs []string
var cmdlc string
var types map[string]bool
var expires float64
var expiresSet bool
metaMap := make(map[string]string)
for {
commandvs = vs
if vs, cmd, ok = tokenval(vs); !ok || cmd == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
cmdlc = strings.ToLower(cmd)
switch cmdlc {
default:
return NOMessage, d, errInvalidArgument(cmd)
case "meta":
var metakey string
var metaval string
if vs, metakey, ok = tokenval(vs); !ok || metakey == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
if vs, metaval, ok = tokenval(vs); !ok || metaval == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
metaMap[metakey] = metaval
continue
case "ex":
var s string
if vs, s, ok = tokenval(vs); !ok || s == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return NOMessage, d, errInvalidArgument(s)
}
expires = v
expiresSet = true
continue
case "nearby":
types = nearbyTypes
case "within", "intersects":
types = withinOrIntersectsTypes
}
break
}
args, err := s.cmdSearchArgs(true, cmdlc, vs, types)
if args.usingLua() {
defer args.Close()
}
if err != nil {
return NOMessage, d, err
}
if !args.fence {
return NOMessage, d, errors.New("missing FENCE argument")
}
args.cmd = cmdlc
cmsg := &Message{}
*cmsg = *msg
cmsg.Args = make([]string, len(commandvs))
copy(cmsg.Args, commandvs)
metas := make([]FenceMeta, 0, len(metaMap))
for key, val := range metaMap {
metas = append(metas, FenceMeta{key, val})
}
sort.Sort(hookMetaByName(metas))
hook := &Hook{
Key: args.key,
Name: name,
Endpoints: endpoints,
Fence: &args,
Message: cmsg,
epm: s.epc,
Metas: metas,
channel: channel,
cond: sync.NewCond(&sync.Mutex{}),
counter: &s.statsTotalMsgsSent,
}
if expiresSet {
hook.expires =
time.Now().Add(time.Duration(expires * float64(time.Second)))
}
if !channel {
hook.db = s.qdb
}
var wr bytes.Buffer
hook.ScanWriter, err = s.newScanWriter(
&wr, cmsg, args.key, args.output, args.precision, args.globs, false,
args.cursor, args.limit, args.wheres, args.whereins, args.whereevals,
args.nofields, args.mvt, args.tileX, args.tileY, args.tileZ)
if err != nil {
return NOMessage, d, err
}
prevHook, _ := s.hooks.Get(&Hook{Name: name}).(*Hook)
if prevHook != nil {
if prevHook.channel != channel {
return NOMessage, d,
errors.New("hooks and channels cannot share the same name")
}
if prevHook.Equals(hook) {
// it was a match so we do nothing. But let's signal just
// for good measure.
prevHook.Signal()
if !hook.expires.IsZero() {
s.hookExpires.Set(hook)
}
switch msg.OutputType {
case JSON:
return OKMessage(msg, start), d, nil
case RESP:
return resp.IntegerValue(0), d, nil
}
}
prevHook.Close()
s.hooks.Delete(prevHook)
s.hooksOut.Delete(prevHook)
if !prevHook.expires.IsZero() {
s.hookExpires.Delete(prevHook)
}
s.groupDisconnectHook(name)
}
d.updated = true
d.timestamp = time.Now()
s.hooks.Set(hook)
if hook.Fence.detect == nil || hook.Fence.detect["outside"] {
s.hooksOut.Set(hook)
}
// remove previous hook from spatial index
if prevHook != nil && prevHook.Fence != nil && prevHook.Fence.obj != nil {
rect := prevHook.Fence.obj.Rect()
s.hookTree.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
prevHook)
if prevHook.Fence.detect["cross"] {
s.hookCross.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
prevHook)
}
}
// add hook to spatial index
if hook != nil && hook.Fence != nil && hook.Fence.obj != nil {
rect := hook.Fence.obj.Rect()
s.hookTree.Insert(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
if hook.Fence.detect["cross"] {
s.hookCross.Insert(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
}
}
hook.Open() // Opens a goroutine to notify the hook
if !hook.expires.IsZero() {
s.hookExpires.Set(hook)
}
switch msg.OutputType {
case JSON:
return OKMessage(msg, start), d, nil
case RESP:
return resp.IntegerValue(1), d, nil
}
return NOMessage, d, nil
}
func byHookExpires(a, b interface{}) bool {
ha := a.(*Hook)
hb := b.(*Hook)
if ha.expires.Before(hb.expires) {
return true
}
if ha.expires.After(hb.expires) {
return false
}
return ha.Name < hb.Name
}
func (s *Server) cmdDELHOOKop(name string, channel bool) (updated bool) {
hook, _ := s.hooks.Get(&Hook{Name: name}).(*Hook)
if hook == nil || hook.channel != channel {
return false
}
hook.Close()
// remove hook from maps
s.hooks.Delete(hook)
s.hooksOut.Delete(hook)
if !hook.expires.IsZero() {
s.hookExpires.Delete(hook)
}
// remove any hook / object connections
s.groupDisconnectHook(hook.Name)
// remove hook from spatial index
if hook.Fence != nil && hook.Fence.obj != nil {
rect := hook.Fence.obj.Rect()
s.hookTree.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
if hook.Fence.detect["cross"] {
s.hookCross.Delete(
[2]float64{rect.Min.X, rect.Min.Y},
[2]float64{rect.Max.X, rect.Max.Y},
hook)
}
}
return true
}
func (s *Server) cmdDelHook(msg *Message) (
res resp.Value, d commandDetails, err error,
) {
channel := msg.Command() == "delchan"
start := time.Now()
vs := msg.Args[1:]
var name string
var ok bool
if vs, name, ok = tokenval(vs); !ok || name == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, d, errInvalidNumberOfArguments
}
d.updated = s.cmdDELHOOKop(name, channel)
d.timestamp = time.Now()
switch msg.OutputType {
case JSON:
return OKMessage(msg, start), d, nil
case RESP:
if d.updated {
return resp.IntegerValue(1), d, nil
}
return resp.IntegerValue(0), d, nil
}
return
}
func (s *Server) cmdPDelHook(msg *Message) (
res resp.Value, d commandDetails, err error,
) {
channel := msg.Command() == "pdelchan"
start := time.Now()
vs := msg.Args[1:]
var pattern string
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return NOMessage, d, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, d, errInvalidNumberOfArguments
}
count := 0
var hooks []*Hook
s.forEachHookByPattern(pattern, channel, func(hook *Hook) bool {
hooks = append(hooks, hook)
return true
})
for _, hook := range hooks {
if hook.channel != channel {
continue
}
s.cmdDELHOOKop(hook.Name, channel)
d.updated = true
count++
}
d.timestamp = time.Now()
switch msg.OutputType {
case JSON:
return OKMessage(msg, start), d, nil
case RESP:
return resp.IntegerValue(count), d, nil
}
return
}
func (s *Server) forEachHookByPattern(
pattern string, channel bool, iter func(hook *Hook) bool,
) {
g := glob.Parse(pattern, false)
hasUpperLimit := g.Limits[1] != ""
s.hooks.Ascend(&Hook{Name: g.Limits[0]}, func(v interface{}) bool {
hook := v.(*Hook)
if hasUpperLimit && hook.Name > g.Limits[1] {
return false
}
if hook.channel == channel {
match, _ := glob.Match(pattern, hook.Name)
if match {
return iter(hook)
}
}
return true
})
}
func (s *Server) cmdHooks(msg *Message) (
res resp.Value, err error,
) {
channel := msg.Command() == "chans"
start := time.Now()
vs := msg.Args[1:]
var pattern string
var ok bool
if vs, pattern, ok = tokenval(vs); !ok || pattern == "" {
return NOMessage, errInvalidNumberOfArguments
}
if len(vs) != 0 {
return NOMessage, errInvalidNumberOfArguments
}
switch msg.OutputType {
case JSON:
buf := &bytes.Buffer{}
buf.WriteString(`{"ok":true,`)
if channel {
buf.WriteString(`"chans":[`)
} else {
buf.WriteString(`"hooks":[`)
}
var i int
s.forEachHookByPattern(pattern, channel, func(hook *Hook) bool {
var ttl = -1
if !hook.expires.IsZero() {
ttl = int(hook.expires.Sub(start).Seconds())
if ttl < 0 {
ttl = 0
}
}
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(`{`)
buf.WriteString(`"name":` + jsonString(hook.Name))
buf.WriteString(`,"key":` + jsonString(hook.Key))
buf.WriteString(`,"ttl":` + strconv.Itoa(ttl))
if !channel {
buf.WriteString(`,"endpoints":[`)
for i, endpoint := range hook.Endpoints {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(jsonString(endpoint))
}
buf.WriteString(`]`)
}
buf.WriteString(`,"command":[`)
for i, v := range hook.Message.Args {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(v))
}
buf.WriteString(`],"meta":{`)
for i, meta := range hook.Metas {
if i > 0 {
buf.WriteString(`,`)
}
buf.WriteString(jsonString(meta.Name))
buf.WriteString(`:`)
buf.WriteString(jsonString(meta.Value))
}
buf.WriteString(`}}`)
i++
return true
})
buf.WriteString(`],"elapsed":"` +
time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
case RESP:
var vals []resp.Value
s.forEachHookByPattern(pattern, channel, func(hook *Hook) bool {
var hvals []resp.Value
hvals = append(hvals, resp.StringValue(hook.Name))
hvals = append(hvals, resp.StringValue(hook.Key))
var evals []resp.Value
for _, endpoint := range hook.Endpoints {
evals = append(evals, resp.StringValue(endpoint))
}
hvals = append(hvals, resp.ArrayValue(evals))
avals := make([]resp.Value, len(hook.Message.Args))
for i := 0; i < len(hook.Message.Args); i++ {
avals[i] = resp.StringValue(hook.Message.Args[i])
}
hvals = append(hvals, resp.ArrayValue(avals))
var metas []resp.Value
for _, meta := range hook.Metas {
metas = append(metas, resp.StringValue(meta.Name))
metas = append(metas, resp.StringValue(meta.Value))
}
hvals = append(hvals, resp.ArrayValue(metas))
vals = append(vals, resp.ArrayValue(hvals))
return true
})
return resp.ArrayValue(vals), nil
}
return resp.SimpleStringValue(""), nil
}
// Hook represents a hook.
type Hook struct {
cond *sync.Cond
Key string
Name string
Endpoints []string
Message *Message
Fence *liveFenceSwitches
ScanWriter *scanWriter
Metas []FenceMeta
db *buntdb.DB
channel bool
closed bool
opened bool
query string
epm *endpoint.Manager
expires time.Time
counter *atomic.Int64 // counter that grows when a message was sent
sig int
}
// Expires returns when the hook expires. Required by the expire.Item interface.
func (h *Hook) Expires() time.Time {
return h.expires
}
// Equals returns true if two hooks are equal
func (h *Hook) Equals(hook *Hook) bool {
if h.Key != hook.Key ||
h.Name != hook.Name ||
len(h.Endpoints) != len(hook.Endpoints) ||
len(h.Metas) != len(hook.Metas) {
return false
}
if !h.expires.Equal(hook.expires) {
return false
}
for i, endpoint := range h.Endpoints {
if endpoint != hook.Endpoints[i] {
return false
}
}
for i, meta := range h.Metas {
if meta.Name != hook.Metas[i].Name ||
meta.Value != hook.Metas[i].Value {
return false
}
}
if len(h.Message.Args) != len(hook.Message.Args) {
return false
}
for i := 0; i < len(h.Message.Args); i++ {
if h.Message.Args[i] != hook.Message.Args[i] {
return false
}
}
return true
}
// FenceMeta is a meta key/value pair for fences
type FenceMeta struct {
Name, Value string
}
type hookMetaByName []FenceMeta
func (arr hookMetaByName) Len() int {
return len(arr)
}
func (arr hookMetaByName) Less(a, b int) bool {
return arr[a].Name < arr[b].Name
}
func (arr hookMetaByName) Swap(a, b int) {
arr[a], arr[b] = arr[b], arr[a]
}
// Open is called when a hook is first created. It calls the manager
// function in a goroutine
func (h *Hook) Open() {
if h.channel {
// nothing to open for channels
return
}
h.cond.L.Lock()
defer h.cond.L.Unlock()
if h.opened {
return
}
h.opened = true
h.query = `{"hook":` + jsonString(h.Name) + `}`
go h.manager()
}
// Close closed the hook and stop the manager function
func (h *Hook) Close() {
if h.channel {
// nothing to close for channels
return
}
h.cond.L.Lock()
defer h.cond.L.Unlock()
if h.closed {
return
}
h.closed = true
h.cond.Broadcast()
}
// Signal can be called at any point to wake up the hook and
// notify the manager that there may be something new in the queue.
func (h *Hook) Signal() {
if h.channel {
// nothing to signal for channels
return
}
h.cond.L.Lock()
h.sig++
h.cond.Broadcast()
h.cond.L.Unlock()
}
// the manager is a forever loop that calls proc whenever there's a signal.
// it ends when the "closed" flag is set.
func (h *Hook) manager() {
// lock the hook to waiting on signals
h.cond.L.Lock()
defer h.cond.L.Unlock()
var sig int
for {
if h.closed {
// the hook has closed, end manager
return
}
sig = h.sig
// unlock/logk the hook and send outgoing messages
if !func() bool {
h.cond.L.Unlock()
defer h.cond.L.Lock()
return h.proc()
}() {
// a send failed, try again in a moment
time.Sleep(time.Second / 2)
continue
}
if sig != h.sig {
// there was another incoming signal
continue
}
// wait on signal
h.cond.Wait()
}
}
// proc processes queued hook logs.
// returning true will indicate that all log entries have been
// successfully handled.
func (h *Hook) proc() (ok bool) {
var keys, vals []string
var ttls []time.Duration
start := time.Now()
err := h.db.Update(func(tx *buntdb.Tx) error {
// get keys and vals
err := tx.AscendGreaterOrEqual("hooks",
h.query, func(key, val string) bool {
if strings.HasPrefix(key, hookLogPrefix) {
// Verify this hooks name matches the one in the notif
if h.Name == gjson.Get(val, "hook").String() {
keys = append(keys, key)
vals = append(vals, val)
}
}
return true
},
)
if err != nil {
return err
}
// delete the keys
for _, key := range keys {
ttl, err := tx.TTL(key)
if err != nil {
if err != buntdb.ErrNotFound {
return err
}
}
ttls = append(ttls, ttl)
_, err = tx.Delete(key)
if err != nil {
if err != buntdb.ErrNotFound {
return err
}
}
}
return nil
})
if err != nil {
log.Error(err)
return false
}
// send each val. on failure reinsert that one and all of the following
for i, key := range keys {
val := vals[i]
idx := stringToUint64(key[len(hookLogPrefix):])
var sent bool
for _, endpoint := range h.Endpoints {
err := h.epm.Send(endpoint, val)
if err != nil {
log.Debugf("Endpoint connect/send error: %v: %v: %v",
idx, endpoint, err)
continue
}
log.Debugf("Endpoint send ok: %v: %v: %v", idx, endpoint, err)
sent = true
h.counter.Add(1)
break
}
if !sent {
// failed to send. try to reinsert the remaining.
// if this fails we lose log entries.
keys = keys[i:]
vals = vals[i:]
ttls = ttls[i:]
h.db.Update(func(tx *buntdb.Tx) error {
for i, key := range keys {
val := vals[i]
ttl := ttls[i] - time.Since(start)
if ttl > 0 {
opts := &buntdb.SetOptions{
Expires: true,
TTL: ttl,
}
_, _, err := tx.Set(key, val, opts)
if err != nil {
return err
}
}
}
return nil
})
return false
}
}
return true
}
================================================
FILE: internal/server/json.go
================================================
package server
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/sjson"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/field"
"github.com/tidwall/tile38/internal/object"
)
func appendJSONString(b []byte, s string) []byte {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return append(b, string(d)...)
}
}
b = append(b, '"')
b = append(b, s...)
b = append(b, '"')
return b
}
func jsonString(s string) string {
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return string(d)
}
}
b := make([]byte, len(s)+2)
b[0] = '"'
copy(b[1:], s)
b[len(b)-1] = '"'
return string(b)
}
func isJSONNumber(data string) bool {
// Returns true if the given string can be encoded as a JSON number value.
// See:
// https://json.org
// http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
if data == "" {
return false
}
i := 0
// sign
if data[i] == '-' {
i++
}
if i == len(data) {
return false
}
// int
if data[i] == '0' {
i++
} else {
for ; i < len(data); i++ {
if data[i] >= '0' && data[i] <= '9' {
continue
}
break
}
}
// frac
if i == len(data) {
return true
}
if data[i] == '.' {
i++
if i == len(data) {
return false
}
if data[i] < '0' || data[i] > '9' {
return false
}
i++
for ; i < len(data); i++ {
if data[i] >= '0' && data[i] <= '9' {
continue
}
break
}
}
// exp
if i == len(data) {
return true
}
if data[i] == 'e' || data[i] == 'E' {
i++
if i == len(data) {
return false
}
if data[i] == '+' || data[i] == '-' {
i++
}
if i == len(data) {
return false
}
if data[i] < '0' || data[i] > '9' {
return false
}
i++
for ; i < len(data); i++ {
if data[i] >= '0' && data[i] <= '9' {
continue
}
break
}
}
return i == len(data)
}
func appendJSONSimpleBounds(dst []byte, o geojson.Object) []byte {
bbox := o.Rect()
dst = append(dst, `{"sw":{"lat":`...)
dst = strconv.AppendFloat(dst, bbox.Min.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, bbox.Min.X, 'f', -1, 64)
dst = append(dst, `},"ne":{"lat":`...)
dst = strconv.AppendFloat(dst, bbox.Max.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, bbox.Max.X, 'f', -1, 64)
dst = append(dst, `}}`...)
return dst
}
func appendJSONSimplePoint(dst []byte, o geojson.Object) []byte {
point := o.Center()
z := extractZCoordinate(o)
dst = append(dst, `{"lat":`...)
dst = strconv.AppendFloat(dst, point.Y, 'f', -1, 64)
dst = append(dst, `,"lon":`...)
dst = strconv.AppendFloat(dst, point.X, 'f', -1, 64)
if z != 0 {
dst = append(dst, `,"z":`...)
dst = strconv.AppendFloat(dst, z, 'f', -1, 64)
}
dst = append(dst, '}')
return dst
}
func appendJSONTimeFormat(b []byte, t time.Time) []byte {
b = append(b, '"')
b = t.AppendFormat(b, "2006-01-02T15:04:05.999999999Z07:00")
b = append(b, '"')
return b
}
func jsonTimeFormat(t time.Time) string {
var b []byte
b = appendJSONTimeFormat(b, t)
return string(b)
}
func (s *Server) cmdJget(msg *Message) (resp.Value, error) {
start := time.Now()
if len(msg.Args) < 3 {
return NOMessage, errInvalidNumberOfArguments
}
if len(msg.Args) > 5 {
return NOMessage, errInvalidNumberOfArguments
}
key := msg.Args[1]
id := msg.Args[2]
var doget bool
var path string
var raw bool
if len(msg.Args) > 3 {
doget = true
path = msg.Args[3]
if len(msg.Args) == 5 {
if strings.ToLower(msg.Args[4]) == "raw" {
raw = true
} else {
return NOMessage, errInvalidArgument(msg.Args[4])
}
}
}
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return NOMessage, errKeyNotFound
}
o := col.Get(id)
if o == nil {
if msg.OutputType == RESP {
return resp.NullValue(), nil
}
return NOMessage, errIDNotFound
}
var res gjson.Result
if doget {
res = gjson.Get(o.Geo().String(), path)
} else {
res = gjson.Parse(o.Geo().String())
}
var val string
if raw {
val = res.Raw
} else {
val = res.String()
}
var buf bytes.Buffer
if msg.OutputType == JSON {
buf.WriteString(`{"ok":true`)
}
switch msg.OutputType {
case JSON:
if res.Exists() {
buf.WriteString(`,"value":` + jsonString(val))
}
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
case RESP:
if !res.Exists() {
return resp.NullValue(), nil
}
return resp.StringValue(val), nil
}
return NOMessage, nil
}
func (s *Server) cmdJset(msg *Message) (res resp.Value, d commandDetails, err error) {
// JSET key path value [RAW]
start := time.Now()
var raw, str bool
switch len(msg.Args) {
default:
return NOMessage, d, errInvalidNumberOfArguments
case 5:
case 6:
switch strings.ToLower(msg.Args[5]) {
default:
return NOMessage, d, errInvalidArgument(msg.Args[5])
case "raw":
raw = true
case "str":
str = true
}
}
key := msg.Args[1]
id := msg.Args[2]
path := msg.Args[3]
val := msg.Args[4]
if !str && !raw {
switch val {
default:
raw = isJSONNumber(val)
case "true", "false", "null":
raw = true
}
}
col, _ := s.cols.Get(key)
var createcol bool
if col == nil {
col = collection.New()
createcol = true
}
var json string
var geoobj bool
var fields field.List
o := col.Get(id)
if o != nil {
geoobj = objIsSpatial(o.Geo())
json = o.Geo().String()
fields = o.Fields()
}
if raw {
// set as raw block
json, err = sjson.SetRaw(json, path, val)
} else {
// set as a string
json, err = sjson.Set(json, path, val)
}
if err != nil {
return NOMessage, d, err
}
if geoobj {
nmsg := *msg
nmsg.Args = []string{"SET", key, id, "OBJECT", json}
// SET key id OBJECT json
return s.cmdSET(&nmsg)
}
if createcol {
s.cols.Set(key, col)
}
var oobj geojson.Object = collection.String(json)
obj := object.New(id, oobj, 0, fields)
col.Set(obj)
d.key = key
d.obj = obj
d.timestamp = time.Now()
d.updated = true
switch msg.OutputType {
case JSON:
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), d, nil
case RESP:
return resp.SimpleStringValue("OK"), d, nil
}
return NOMessage, d, nil
}
func (s *Server) cmdJdel(msg *Message) (res resp.Value, d commandDetails, err error) {
start := time.Now()
if len(msg.Args) != 4 {
return NOMessage, d, errInvalidNumberOfArguments
}
key := msg.Args[1]
id := msg.Args[2]
path := msg.Args[3]
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
return resp.IntegerValue(0), d, nil
}
return NOMessage, d, errKeyNotFound
}
var json string
var geoobj bool
var fields field.List
o := col.Get(id)
if o != nil {
geoobj = objIsSpatial(o.Geo())
json = o.Geo().String()
fields = o.Fields()
}
njson, err := sjson.Delete(json, path)
if err != nil {
return NOMessage, d, err
}
if njson == json {
switch msg.OutputType {
case JSON:
return NOMessage, d, errPathNotFound
case RESP:
return resp.IntegerValue(0), d, nil
}
return NOMessage, d, nil
}
json = njson
if geoobj {
nmsg := *msg
nmsg.Args = []string{"SET", key, id, "OBJECT", json}
// SET key id OBJECT json
return s.cmdSET(&nmsg)
}
var oobj geojson.Object = collection.String(json)
obj := object.New(id, oobj, 0, fields)
col.Set(obj)
d.key = key
d.obj = obj
d.timestamp = time.Now()
d.updated = true
switch msg.OutputType {
case JSON:
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), d, nil
case RESP:
return resp.IntegerValue(1), d, nil
}
return NOMessage, d, nil
}
================================================
FILE: internal/server/json_test.go
================================================
package server
import (
"encoding/json"
"testing"
)
func BenchmarkJSONString(t *testing.B) {
var s = "the need for mead"
for i := 0; i < t.N; i++ {
jsonString(s)
}
}
func BenchmarkJSONMarshal(t *testing.B) {
var s = "the need for mead"
for i := 0; i < t.N; i++ {
json.Marshal(s)
}
}
func TestIsJsonNumber(t *testing.T) {
test := func(expected bool, val string) {
actual := isJSONNumber(val)
if expected != actual {
t.Fatalf("Expected %t == isJsonNumber(\"%s\") but was %t", expected, val, actual)
}
}
test(false, "")
test(false, "-")
test(false, "foo")
test(false, "0123")
test(false, "1.")
test(false, "1.0e")
test(false, "1.0e-")
test(false, "1.0E10NaN")
test(false, "1.0ENaN")
test(true, "-1")
test(true, "0")
test(true, "0.0")
test(true, "42")
test(true, "1.0E10")
test(true, "1.0e10")
test(true, "1E+5")
test(true, "1E-10")
}
================================================
FILE: internal/server/keys.go
================================================
package server
import (
"encoding/json"
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/glob"
)
// KEYS pattern
func (s *Server) cmdKEYS(msg *Message) (resp.Value, error) {
var start = time.Now()
// >> Args
args := msg.Args
if len(args) != 2 {
return retrerr(errInvalidNumberOfArguments)
}
pattern := args[1]
// >> Operation
keys := []string{}
g := glob.Parse(pattern, false)
everything := g.Limits[0] == "" && g.Limits[1] == ""
if everything {
s.cols.Scan(
func(key string, _ *collection.Collection) bool {
match, _ := glob.Match(pattern, key)
if match {
keys = append(keys, key)
}
return true
},
)
} else {
s.cols.Ascend(g.Limits[0],
func(key string, _ *collection.Collection) bool {
if key > g.Limits[1] {
return false
}
match, _ := glob.Match(pattern, key)
if match {
keys = append(keys, key)
}
return true
},
)
}
// >> Response
if msg.OutputType == JSON {
data, _ := json.Marshal(keys)
return resp.StringValue(`{"ok":true,"keys":` + string(data) +
`,"elapsed":"` + time.Since(start).String() + `"}`), nil
}
var vals []resp.Value
for _, key := range keys {
vals = append(vals, resp.StringValue(key))
}
return resp.ArrayValue(vals), nil
}
================================================
FILE: internal/server/live.go
================================================
package server
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/tidwall/redcon"
"github.com/tidwall/tile38/internal/log"
"go.uber.org/atomic"
)
type liveBuffer struct {
key string
globs []string
fence *liveFenceSwitches
details []*commandDetails
cond *sync.Cond
}
func (s *Server) processLives(wg *sync.WaitGroup) {
defer wg.Done()
var done atomic.Bool
wg.Add(1)
go func() {
defer wg.Done()
for {
if done.Load() {
break
}
s.lcond.Broadcast()
time.Sleep(time.Second / 4)
}
}()
s.lcond.L.Lock()
defer s.lcond.L.Unlock()
for {
if s.stopServer.Load() {
done.Store(true)
return
}
for len(s.lstack) > 0 {
item := s.lstack[0]
s.lstack = s.lstack[1:]
if len(s.lstack) == 0 {
s.lstack = nil
}
for lb := range s.lives {
lb.cond.L.Lock()
if lb.key != "" && lb.key == item.key {
lb.details = append(lb.details, item)
lb.cond.Broadcast()
}
lb.cond.L.Unlock()
}
}
s.lcond.Wait()
}
}
func writeLiveMessage(
conn net.Conn,
message []byte,
wrapRESP bool,
connType Type, websocket bool,
) error {
if len(message) == 0 {
return nil
}
if websocket {
return WriteWebSocketMessage(conn, message)
}
var err error
switch connType {
case RESP:
if wrapRESP {
_, err = fmt.Fprintf(conn, "$%d\r\n%s\r\n", len(message), string(message))
} else {
_, err = conn.Write(message)
}
case Native:
_, err = fmt.Fprintf(conn, "$%d %s\r\n", len(message), string(message))
}
return err
}
func (s *Server) goLive(
inerr error, conn net.Conn, rd *PipelineReader, msg *Message, websocket bool,
) error {
addr := conn.RemoteAddr().String()
log.Info("live " + addr)
defer func() {
log.Info("not live " + addr)
}()
switch lfs := inerr.(type) {
default:
return errors.New("invalid live type switches")
case liveAOFSwitches:
return s.liveAOF(lfs.pos, conn, rd, msg)
case liveSubscriptionSwitches:
return s.liveSubscription(conn, rd, msg, websocket)
case liveMonitorSwitches:
return s.liveMonitor(conn, rd, msg)
case liveFenceSwitches:
// fallthrough
}
// everything below is for live geofences
lb := &liveBuffer{
cond: sync.NewCond(&sync.Mutex{}),
}
var err error
var sw *scanWriter
var wr bytes.Buffer
lfs := inerr.(liveFenceSwitches)
lb.globs = lfs.globs
lb.key = lfs.key
lb.fence = &lfs
s.mu.RLock()
sw, err = s.newScanWriter(
&wr, msg, lfs.key, lfs.output, lfs.precision, lfs.globs, false,
lfs.cursor, lfs.limit, lfs.wheres, lfs.whereins, lfs.whereevals,
lfs.nofields, lfs.mvt, lfs.tileX, lfs.tileY, lfs.tileZ)
s.mu.RUnlock()
// everything below if for live SCAN, NEARBY, WITHIN, INTERSECTS
if err != nil {
return err
}
s.lcond.L.Lock()
s.lives[lb] = true
s.lcond.L.Unlock()
defer func() {
s.lcond.L.Lock()
delete(s.lives, lb)
s.lcond.L.Unlock()
conn.Close()
}()
var mustQuit bool
go func() {
defer func() {
lb.cond.L.Lock()
mustQuit = true
lb.cond.Broadcast()
lb.cond.L.Unlock()
conn.Close()
}()
for {
vs, err := rd.ReadMessages()
if err != nil {
if err != io.EOF && !(websocket && err == io.ErrUnexpectedEOF) {
log.Error(err)
}
return
}
for _, v := range vs {
if v == nil {
continue
}
switch v.Command() {
default:
log.Error("received a live command that was not QUIT")
return
case "quit", "":
return
}
}
}
}()
outputType := msg.OutputType
connType := msg.ConnType
if websocket {
outputType = JSON
}
var livemsg []byte
switch outputType {
case JSON:
livemsg = redcon.AppendBulkString(nil, `{"ok":true,"live":true}`)
case RESP:
livemsg = redcon.AppendOK(nil)
}
if err := writeLiveMessage(conn, livemsg, false, connType, websocket); err != nil {
return nil // nil return is fine here
}
for {
lb.cond.L.Lock()
if mustQuit {
lb.cond.L.Unlock()
return nil
}
for len(lb.details) > 0 {
details := lb.details[0]
lb.details = lb.details[1:]
if len(lb.details) == 0 {
lb.details = nil
}
fence := lb.fence
lb.cond.L.Unlock()
var msgs []string
func() {
// safely lock the fence because we are outside the main loop
s.mu.RLock()
defer s.mu.RUnlock()
msgs = FenceMatch("", sw, fence, nil, details)
}()
for _, msg := range msgs {
if err := writeLiveMessage(conn, []byte(msg), true, connType, websocket); err != nil {
return nil // nil return is fine here
}
}
s.statsTotalMsgsSent.Add(int64(len(msgs)))
lb.cond.L.Lock()
}
lb.cond.Wait()
lb.cond.L.Unlock()
}
}
================================================
FILE: internal/server/metrics.go
================================================
package server
import (
"fmt"
"net/http"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/collection"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
metricDescriptions = map[string]*prometheus.Desc{
/*
these metrics are taken from basicStats() / extStats()
by accessing the map and directly exporting the value found
*/
"num_collections": prometheus.NewDesc("tile38_collections", "Total number of collections", nil, nil),
"pid": prometheus.NewDesc("tile38_pid", "", nil, nil),
"aof_size": prometheus.NewDesc("tile38_aof_size_bytes", "", nil, nil),
"num_hooks": prometheus.NewDesc("tile38_hooks", "", nil, nil),
"in_memory_size": prometheus.NewDesc("tile38_in_memory_size_bytes", "", nil, nil),
"heap_size": prometheus.NewDesc("tile38_heap_size_bytes", "", nil, nil),
"heap_released": prometheus.NewDesc("tile38_memory_reap_released_bytes", "", nil, nil),
"max_heap_size": prometheus.NewDesc("tile38_memory_max_heap_size_bytes", "", nil, nil),
"avg_item_size": prometheus.NewDesc("tile38_avg_item_size_bytes", "", nil, nil),
"pointer_size": prometheus.NewDesc("tile38_pointer_size_bytes", "", nil, nil),
"cpus": prometheus.NewDesc("tile38_num_cpus", "", nil, nil),
"tile38_connected_clients": prometheus.NewDesc("tile38_connected_clients", "", nil, nil),
"tile38_total_connections_received": prometheus.NewDesc("tile38_connections_received_total", "", nil, nil),
"tile38_total_messages_sent": prometheus.NewDesc("tile38_messages_sent_total", "", nil, nil),
"tile38_expired_keys": prometheus.NewDesc("tile38_expired_keys_total", "", nil, nil),
/*
these metrics are NOT taken from basicStats() / extStats()
but are calculated independently
*/
"collection_objects": prometheus.NewDesc("tile38_collection_objects", "Total number of objects per collection", []string{"col"}, nil),
"collection_points": prometheus.NewDesc("tile38_collection_points", "Total number of points per collection", []string{"col"}, nil),
"collection_strings": prometheus.NewDesc("tile38_collection_strings", "Total number of strings per collection", []string{"col"}, nil),
"collection_weight": prometheus.NewDesc("tile38_collection_weight_bytes", "Total weight of collection in bytes", []string{"col"}, nil),
"server_info": prometheus.NewDesc("tile38_server_info", "Server info", []string{"id", "version"}, nil),
"replication": prometheus.NewDesc("tile38_replication_info", "Replication info", []string{"role", "following", "caught_up", "caught_up_once"}, nil),
"start_time": prometheus.NewDesc("tile38_start_time_seconds", "", nil, nil),
}
cmdDurations = prometheus.NewSummaryVec(prometheus.SummaryOpts{
Name: "tile38_cmd_duration_seconds",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
}, []string{"cmd"},
)
)
func (s *Server) MetricsIndexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
Tile38 ` + core.Version + `