├── .github └── workflows │ ├── codeql.yml │ ├── macos_latest.yml │ └── ubuntu_latest.yml ├── .gitignore ├── .travis.yml ├── .vscode ├── c_cpp_properties.json └── settings.json ├── Dockerfile ├── Dockerfile.bench ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── main.c ├── perf └── benchmark.c ├── snowid.c ├── snowid.h ├── snowid_checkpoint.c ├── snowid_checkpoint.h ├── snowid_util.c ├── snowid_util.h └── tests └── snowid_test.c /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '33 10 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'cpp' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/macos_latest.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI (MacOS) 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | macos-build: 11 | 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use make build and test 16 | run: | 17 | make clean && 18 | DEBUGBUILD=true make test 19 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu_latest.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use make build and test 17 | run: | 18 | make clean && 19 | DEBUGBUILD=true make test 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | #snowid executable 55 | snowid 56 | unit 57 | benchmark -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: false 3 | compiler: 4 | - gcc 5 | - clang 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | script: make test && ./unit -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Mac", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "macFrameworkPath": [ 10 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks" 11 | ], 12 | "compilerPath": "/usr/bin/clang", 13 | "cStandard": "c99", 14 | "cppStandard": "c++17", 15 | "intelliSenseMode": "macos-clang-x64" 16 | } 17 | ], 18 | "version": 4 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "snowid.h": "c", 4 | "cstdlib": "c", 5 | "exception": "c", 6 | "ios": "c", 7 | "optional": "c", 8 | "stdexcept": "c", 9 | "string": "c", 10 | "typeinfo": "c", 11 | "variant": "c", 12 | "vector": "c", 13 | "functional": "c", 14 | "memory": "c", 15 | "stdio.h": "c", 16 | "locale": "c", 17 | "ostream": "c", 18 | "new": "c", 19 | "snowid_util.h": "c", 20 | "ifaddrs.h": "c", 21 | "snowid_checkpoint.h": "c", 22 | "unistd.h": "c", 23 | "__locale": "c", 24 | "if_dl.h": "c", 25 | "ctime": "c" 26 | } 27 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu as build-base 2 | RUN apt-get update && apt-get install build-essential -y 3 | 4 | WORKDIR /build 5 | COPY . . 6 | 7 | # compile the code 8 | RUN make clean && make 9 | 10 | CMD ["/build/snowid"] -------------------------------------------------------------------------------- /Dockerfile.bench: -------------------------------------------------------------------------------- 1 | FROM ubuntu as build-base 2 | RUN apt-get update && apt-get install build-essential -y 3 | 4 | WORKDIR /build 5 | COPY . . 6 | 7 | # compile the code 8 | RUN make clean && make bench 9 | 10 | CMD ["bash", "-c", "time /build/benchmark"] -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM ubuntu as build-base 2 | RUN apt-get update && apt-get install build-essential -y 3 | 4 | WORKDIR /build 5 | COPY . . 6 | 7 | # compile the code 8 | RUN make clean && DEBUGBUILD=true make test 9 | 10 | CMD ["/build/unit"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 beyonddream 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | CC ?= cc 3 | ifdef DEBUGBUILD 4 | CFLAGS = -std=c99 -Wall -Werror -Wextra -fsanitize=address,undefined 5 | LDFLAGS = -g3 6 | else 7 | CFLAGS = -std=c99 -Wall -Werror -Wextra -Os 8 | LDFLAGS = -g 9 | endif 10 | 11 | 12 | all: snowid 13 | 14 | test: unit 15 | ./unit 16 | 17 | bench: benchmark 18 | ./benchmark 19 | 20 | unit: snowid.o snowid_util.o snowid_checkpoint.o snowid_test.o 21 | $(CC) $(CFLAGS) $(LDFLAGS) -o $@ snowid.o snowid_util.o snowid_checkpoint.o snowid_test.o 22 | 23 | snowid_test.o: tests/snowid_test.c snowid.h 24 | $(CC) $(CFLAGS) $(LDFLAGS) -c tests/snowid_test.c -o $@ -I$(PWD) 25 | 26 | snowid: snowid.o snowid_util.o snowid_checkpoint.o main.o 27 | $(CC) $(CFLAGS) $(LDFLAGS) -o $@ snowid.o snowid_util.o snowid_checkpoint.o main.o 28 | 29 | snowid.o: snowid.c snowid.h 30 | $(CC) $(CFLAGS) -c snowid.c -o $@ 31 | 32 | snowid_util.o: snowid_util.c snowid_util.h 33 | $(CC) $(CFLAGS) -c snowid_util.c -o $@ 34 | 35 | snowid_checkpoint.o: snowid_checkpoint.c snowid_checkpoint.h 36 | $(CC) $(CFLAGS) -c snowid_checkpoint.c -o $@ 37 | 38 | main.o: main.c snowid.h 39 | $(CC) $(CFLAGS) -c main.c -o $@ -I$(PWD) 40 | 41 | benchmark: snowid.o snowid_util.o snowid_checkpoint.o benchmark.o 42 | $(CC) $(CFLAGS) $(LDFLAGS) -o $@ snowid.o snowid_util.o snowid_checkpoint.o benchmark.o 43 | 44 | benchmark.o: perf/benchmark.c snowid.h 45 | $(CC) $(CFLAGS) -c perf/benchmark.c -o $@ -I$(PWD) 46 | 47 | clean: 48 | rm -rf main.o snowid.o snowid_util.o snowid_checkpoint.o snowid_test.o benchmark.o 49 | rm -rf snowid benchmark unit timestamp.out 50 | .PHONY: clean 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Ubuntu Latest CI](https://github.com/beyonddream/snowid/workflows/Makefile%20CI/badge.svg) 2 | ![Macos Latest CI](https://github.com/beyonddream/snowid/workflows/Makefile%20CI%20(MacOS)/badge.svg) 3 | ![CodeQL](https://github.com/beyonddream/snowid/actions/workflows/codeql.yml/badge.svg) 4 | 5 | # snowid 6 | 7 | A Decentralized, K-Ordered Unique ID Generator library in C. 8 | 9 | It generates 128-bit k-ordered id's (time-ordered lexically). It can be run on each node in your 10 | infrastructure and it will generate conflict-free id's on-demand without coordination. Here, k-ordered means that id's generated within a given millisecond will be within a millisecond of one another in the id space as well. 11 | 12 | ## Design 13 | 14 | #### UUID format (128 bits) 15 | 16 | ``` 17 | {timestamp:64, worker_id:48, seq: 16} 18 | ``` 19 | 20 | * `timestamp` - unix timestamp (millisecond timestamp resolution) 21 | * `worker_id` - pulled from a network interface a 48-bit MAC address 22 | * `seq` - incremented each time id is requested in the same millisecond. This means each server can produce 2^16 - 1 unique id's per millisecond without overflow. 23 | 24 | Example: 25 | 26 | ```sh 27 | # hexadecimal representation of 128-bit id's in colon notation 28 | # e.g. 0:0:1:84:40:9b:ff:a5 = 8 byte unix timestamp, 2:0:12:ac:42:3 = 6 byte MAC address, 0:0 = 2 byte sequence number 29 | .... 30 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:0 31 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:1 32 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:2 33 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:3 34 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:4 35 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:5 36 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:6 37 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:7 38 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:8 39 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:9 40 | 0:0:1:84:40:9b:ff:a5:2:0:12:ac:42:3:0:a 41 | .... 42 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:0 43 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:1 44 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:2 45 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:3 46 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:4 47 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:5 48 | 0:0:1:84:40:9b:ff:a8:2:0:12:ac:42:3:0:6 49 | .... 50 | ``` 51 | **NOTE**: Since these id's are predictable, **do not** use them where predictability is **not** a desirable property like in passwords, security tokens or anything that can be easily guessed or brute-forced. Since it also exposes the identity of the machine that generated the id (via MAC address) it could be a problem for security sensitive applications. 52 | 53 | #### Config 54 | 55 | `struct snow_config` takes below fields: 56 | 57 | * `interface` - Set to available network interface to pull a 48-bit mac address as worker id. 58 | * `timestamp_path` - To periodically save the current time. When snow_init() is called and if it detects 59 | that this file contains timestamp in the future or distant past then it refuses to generate id. This prevents problematic id's (that might break time-orderness) from getting distributed. 60 | * `allowable_downtime` - Safeguard to prevent id generation if it detects it hasn't been run in a long time 61 | since this might be an indication that the system clock has been skewed far into the future. 62 | 63 | Example: 64 | ``` 65 | interface="en0" 66 | timestamp_path="/data/snowid/timestamp.out" 67 | allowable_downtime=1665373570 68 | ``` 69 | 70 | ## API/Usage 71 | 72 | Two main functions to generate id are provided: 73 | 74 | A) 75 | ```c 76 | /* 77 | * Attempts to set `dest_as_binary` with a 128-bit id (represented as byte 78 | * array). Return true if operation succeeds. 79 | */ 80 | bool snow_get_id_as_binary(snow_id_binary_t snowid_as_bin); 81 | ``` 82 | B) 83 | ```c 84 | /* 85 | * Attempts to set `dest` with a `struct snow_id` that consists of individual 86 | * components of the id. Return true if operation succeeds. 87 | */ 88 | bool snow_get_id(snow_id_t *snowid); 89 | ``` 90 | 91 | The third function to convert A) from B). 92 | ```c 93 | /* 94 | * Convenience function that attempts to convert `snowid` of type `struct 95 | * snow_id` into binary `dest_as_bin`. Return true if operation succeeds. 96 | */ 97 | bool snow_id_convert(snow_id_binary_t snowid_as_bin, const snow_id_t *snowid); 98 | ``` 99 | 100 | For full usage of rest of the public API's (`snowid.h`), checkout `main.c` that provides a driver program on how to use the library from an application (e.g. a (web)service). 101 | 102 | ## Dependencies 103 | 104 | * Tested on macOS and GNU/Linux (via Docker). PR welcome to make it work on Windows. 105 | * C99 compatible C compiler (e.g. GCC or Clang). 106 | * This library doesn't depend on any external libraries. 107 | 108 | ## Build/Test/Benchmark 109 | 110 | #### Build and Run 111 | 112 | ``` 113 | $ make 114 | ``` 115 | This will create a binary by the name `snowid` which is an example driver program and run it like this: 116 | ``` 117 | $ ./snowid 118 | ``` 119 | and it should print a series of time ordered unique id's. 120 | 121 | #### Test 122 | 123 | ``` 124 | $ make test 125 | ``` 126 | This should run a series of unit tests to verify the basic functionality of the library. 127 | 128 | Debug mode build and test can be enabled like below: 129 | 130 | ``` 131 | $ DEBUGBUILD=true make test 132 | ``` 133 | 134 | #### Benchmark 135 | 136 | Timing benchmark below on how long it takes to generate 100,000 unique ids on my MacBook 2.2 GHz 6-Core Intel Core i7 (note this may vary based on your machine): 137 | 138 | ``` 139 | $ time make bench 140 | 141 | real 0m0.613s 142 | user 0m0.089s 143 | sys 0m0.066s 144 | ``` 145 | 146 | If you have python3 in your system, then u can run it like below: 147 | 148 | ``` 149 | $ python3 -m timeit -n 3 "__import__('os').system('make bench')" 150 | ... 151 | 3 loops, best of 5: 509 msec per loop 152 | ``` 153 | 154 | #### Docker build 155 | 156 | There is a Dockerfile which provides a way to build the code in GNU/Linux. 157 | 158 | ``` 159 | 1) docker build -t snowid -f Dockerfile . 160 | 2) docker run -it --rm snowid 161 | ``` 162 | 163 | #### Docker test 164 | 165 | ``` 166 | 1) docker build -t unit -f Dockerfile.test . 167 | 2) docker run -it --rm unit 168 | ``` 169 | 170 | #### Docker bench 171 | 172 | ``` 173 | 1) docker build -t benchmark -f Dockerfile.bench . 174 | 2) docker run -it --rm benchmark 175 | ``` 176 | 177 | ## Credits 178 | 179 | * General inspiration is from Twitter snowflake [id generator](https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake). 180 | 181 | * The idea/design of `snowid` is borrowed from BMC's (formerly Boundary) flake server written in Erlang. 182 | More details are in the original [post](http://archive.is/2015.07.08-082503/http://www.boundary.com/blog/2012/01/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang/). 183 | 184 | 185 | ## License 186 | 187 | MIT License 188 | Copyright (c) 2022 beyonddream 189 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "snowid.h" 6 | 7 | /* Driver code to show how snowid library can be used in a program */ 8 | int main(void) 9 | { 10 | puts("Test program for snowid..."); 11 | 12 | snow_config_t config = { 13 | .interface = NULL, 14 | .timestamp_path = "./timestamp.out", 15 | .allowable_downtime = 2592000000, 16 | }; 17 | 18 | snow_init(&config); 19 | 20 | snow_dump(NULL); 21 | 22 | #if 0 23 | snow_id_t snow_id; 24 | #endif 25 | unsigned char out[16] = {0}; 26 | 27 | for(int i = 1; i <= 1000; i++) { 28 | #if 0 29 | /* alternate way of getting a unique id but you can access 30 | individual members of the struct. 31 | */ 32 | if (snow_get_id(&snow_id) == false) { 33 | puts("unable to generate snowid"); 34 | break; 35 | } 36 | #endif 37 | if (snow_get_id_as_binary(out) == false) { 38 | puts("unable to generate snowid as binary"); 39 | break; 40 | } 41 | 42 | for (int8_t i = 0; i < 16; i++) { 43 | printf("%x", out[i]); 44 | if (i != 15) { 45 | printf(":"); 46 | } 47 | } 48 | printf("\n"); 49 | 50 | if (i == 500) { 51 | puts("sleeping for 4 sec..."); 52 | sleep(4); 53 | } 54 | } 55 | 56 | snow_dump(NULL); 57 | 58 | snow_shutdown(); 59 | 60 | return EXIT_SUCCESS; 61 | } -------------------------------------------------------------------------------- /perf/benchmark.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "snowid.h" 6 | 7 | static inline bool bench() 8 | { 9 | snow_config_t config = { 10 | .interface = "en0", 11 | .timestamp_path = "./timestamp.out", 12 | .allowable_downtime = 2592000000, 13 | }; 14 | 15 | snow_init(&config); 16 | 17 | unsigned char out[16] = {0}; 18 | 19 | for(int i = 1; i <= 100000; i++) { 20 | 21 | if (snow_get_id_as_binary(out) == false) { 22 | puts("unable to generate snowid as binary"); 23 | return false; 24 | } 25 | /* print the last byte as hex just so the compiler doesn't optimize `out` away. */ 26 | printf("%x\n", out[15]); 27 | } 28 | 29 | snow_shutdown(); 30 | 31 | return true; 32 | } 33 | 34 | int main(void) 35 | { 36 | if (bench() == false) { 37 | printf("calling benchmark failed..."); 38 | return EXIT_FAILURE; 39 | } 40 | 41 | return EXIT_SUCCESS; 42 | } -------------------------------------------------------------------------------- /snowid.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include "snowid.h" 33 | #include "snowid_util.h" 34 | #include "snowid_checkpoint.h" 35 | 36 | /* private internal snowid state representation */ 37 | typedef struct snow_state { 38 | /* Id will be generated only if true */ 39 | bool enabled; 40 | /** 41 | * `checkpoint` is last time when an id was generated. 42 | * If we detect current time is < checkpoint, then 43 | * clock has moved backward and we refuse to generate 44 | * the id. 45 | */ 46 | uint64_t checkpoint; 47 | /* 48-bits MAC address */ 48 | uint64_t worker_id; 49 | /* 16 bit increments within same millisecond */ 50 | uint16_t sequence_id; 51 | } snow_state_t; 52 | 53 | /** 54 | * Global variable to store the state. 55 | * Client should use some form of mutex if multiple threads are going to access the API's. 56 | */ 57 | static snow_state_t *state; 58 | 59 | #define DEFAULT_CHECKPOINT_FILE_PATH "/data/snowid/timestamp.out" 60 | 61 | static bool get_checkpoint_mutable(uint64_t *out, char *timestamp_path); 62 | static bool get_worker_id_from_nw_if(uint64_t *out, char *interface); 63 | static bool get_snowid_to_binary(snow_id_binary_t out, const snow_id_t *snowid); 64 | 65 | static bool get_worker_id_from_nw_if(uint64_t *workerid, char *interface) 66 | { 67 | 68 | if (workerid == NULL) { 69 | return false; 70 | } 71 | 72 | return get_hw_addr_as_binary(workerid, interface); 73 | } 74 | 75 | static bool get_checkpoint_mutable(uint64_t *checkpoint, char *timestamp_path) 76 | { 77 | 78 | if (checkpoint == NULL) { 79 | return false; 80 | } 81 | 82 | *checkpoint = 0; 83 | 84 | if (timestamp_path == NULL) { 85 | timestamp_path = DEFAULT_CHECKPOINT_FILE_PATH; 86 | } 87 | 88 | bool success = true; 89 | FILE *file = fopen(timestamp_path, "r"); 90 | 91 | if (file == NULL) { 92 | /* create a new file at timestamp_path */ 93 | file = fopen(timestamp_path, "w"); 94 | if (file == NULL) { 95 | fprintf(stderr, "Couldn't open timestamp_path for write.\n"); 96 | success = false; 97 | } else { 98 | if (get_current_ts(checkpoint) == false) { 99 | fprintf(stderr, "Couldn't read current timestamp.\n"); 100 | success = false; 101 | } 102 | if (*checkpoint == 0) { 103 | fprintf(stderr, "Checkpoint value seem to be zero.\n"); 104 | success = false; 105 | } 106 | int ret = fwrite(checkpoint, sizeof(uint64_t), 1, file); 107 | if (ret != 1) { 108 | fprintf(stderr, "Couldn't write to timestamp_path.\n"); 109 | success = false; 110 | } 111 | fclose(file); 112 | } 113 | } else { 114 | /* read from the existing file at timestamp_path */ 115 | int ret = fread(checkpoint, sizeof(uint64_t), 1, file); 116 | if (ret != 1) { 117 | fprintf(stderr, "Couldn't read from timestamp_path.\n"); 118 | success = false; 119 | } 120 | if (*checkpoint == 0) { 121 | fprintf(stderr, "Checkpoint value seem to be zero.\n"); 122 | success = false; 123 | } 124 | fclose(file); 125 | } 126 | 127 | return success; 128 | } 129 | 130 | bool snow_get_id(snow_id_t *dest) 131 | { 132 | uint64_t current_time; 133 | 134 | if (dest == NULL) { 135 | return false; 136 | } 137 | 138 | if (state == NULL || state->enabled == false) { 139 | return false; 140 | } 141 | 142 | if (get_current_ts(¤t_time) == false) { 143 | return false; 144 | } 145 | 146 | if (state->checkpoint > current_time) { 147 | fprintf(stderr, "Clock is running backwards.\n"); 148 | state->enabled = false; 149 | return false; 150 | } 151 | 152 | if (state->checkpoint == current_time) { 153 | state->sequence_id++; 154 | } else { 155 | state->sequence_id = 0; 156 | } 157 | 158 | state->checkpoint = current_time; 159 | 160 | snow_id_t current = { 161 | .timestamp = state->checkpoint, 162 | .worker_id = state->worker_id, 163 | .sequence_id = state->sequence_id 164 | }; 165 | 166 | *dest = current; 167 | 168 | return true; 169 | } 170 | 171 | static bool get_snowid_to_binary(snow_id_binary_t out, const snow_id_t *snowid) 172 | { 173 | int idx = 0; 174 | 175 | uint64_t timestamp = snowid->timestamp; 176 | uint64_t workerid = snowid->worker_id; 177 | uint16_t sequenceid = snowid->sequence_id; 178 | 179 | /* convert the timestamp into 64 bits */ 180 | for(int8_t i = 7; i >= 0; i--) { 181 | out[idx++] = (timestamp >> (CHAR_BIT * i)) & 0xff; 182 | } 183 | 184 | /* convert the worker id into 48 bits */ 185 | for(int8_t i = 5; i >= 0; i--) { 186 | out[idx++] = (workerid >> (CHAR_BIT * i)) & 0xff; 187 | } 188 | 189 | /* convert the sequence id into 16 bits */ 190 | for(int8_t i = 1; i >= 0; i--) { 191 | out[idx++] = (sequenceid >> (CHAR_BIT * i)) & 0xff; 192 | } 193 | 194 | return true; 195 | } 196 | 197 | bool snow_id_convert(snow_id_binary_t out, const snow_id_t *snowid) { 198 | 199 | if (out == NULL || snowid == NULL) { 200 | return false; 201 | } 202 | 203 | return get_snowid_to_binary(out, snowid); 204 | } 205 | 206 | bool snow_get_id_as_binary(snow_id_binary_t dest_as_bin) 207 | { 208 | 209 | if (dest_as_bin == NULL) { 210 | return false; 211 | } 212 | 213 | snow_id_t dest; 214 | 215 | if (snow_get_id(&dest) == false) { 216 | return false; 217 | } 218 | 219 | return get_snowid_to_binary(dest_as_bin, &dest); 220 | } 221 | 222 | void snow_dump(FILE *stream) 223 | { 224 | 225 | if (state == NULL) { 226 | return; 227 | } 228 | 229 | if (stream == NULL) { 230 | stream = stdout; 231 | } 232 | 233 | #define LOG(KEY, VALUE) \ 234 | do { \ 235 | fprintf(stream, (KEY), (VALUE)); \ 236 | } while(0) 237 | 238 | LOG("%s", "\n{"); 239 | LOG("\"enabled\":%d,", state->enabled); 240 | LOG("\"worker_id\":%" PRIu64 ",", state->worker_id); 241 | LOG("\"checkpoint\":%" PRIu64 ",", state->checkpoint); 242 | LOG("\"sequence_id\":%hu", state->sequence_id); 243 | LOG("%s", "}\n"); 244 | #undef LOG 245 | 246 | return; 247 | } 248 | 249 | void snow_init(snow_config_t *config) 250 | { 251 | uint64_t current_time; 252 | uint64_t checkpoint; 253 | uint64_t allowable_downtime; 254 | 255 | state = malloc(sizeof(snow_state_t)); 256 | 257 | if (state == NULL) { 258 | fprintf(stderr, "malloc of snow_state_t failed.\n"); 259 | return; 260 | } 261 | 262 | state->enabled = false; 263 | 264 | if (config == NULL) { 265 | fprintf(stderr, "snow config is NULL.\n"); 266 | return; 267 | } 268 | 269 | /* if timestamp_path is new, then checkpoint can be current timestamp */ 270 | if (get_checkpoint_mutable(&checkpoint, config->timestamp_path) == false) { 271 | return; 272 | } 273 | 274 | /* get the current timestamp again now */ 275 | if (get_current_ts(¤t_time) == false) { 276 | return; 277 | } 278 | 279 | if (checkpoint > current_time) { 280 | fprintf(stderr, "Clock is running backwards, failing to generate id.\n"); 281 | return; 282 | } 283 | 284 | allowable_downtime = config->allowable_downtime; 285 | 286 | if ((current_time - checkpoint) > allowable_downtime) { 287 | fprintf(stderr, "Clock is too far advanced, failing to generate id.\n"); 288 | return; 289 | } 290 | 291 | state->checkpoint = current_time; 292 | state->sequence_id = 0; 293 | 294 | if (get_worker_id_from_nw_if(&state->worker_id, config->interface) == false) { 295 | return; 296 | } 297 | 298 | /* start a child process to periodically save current time in `timestamp_path`*/ 299 | snow_checkpoint_start(config->timestamp_path); 300 | 301 | /* init has succeeded */ 302 | state->enabled = true; 303 | 304 | return; 305 | } 306 | 307 | void snow_shutdown(void) 308 | { 309 | 310 | free(state); 311 | state = NULL; 312 | 313 | return; 314 | } -------------------------------------------------------------------------------- /snowid.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #ifndef __SNOWID_H__ 26 | #define __SNOWID_H__ 27 | 28 | #include 29 | #include 30 | #include 31 | 32 | typedef struct snow_config { 33 | char *interface; /* network interface name */ 34 | char *timestamp_path; /* file location to save current time periodically */ 35 | uint64_t allowable_downtime; /* time since snowid is called last - default 0 */ 36 | } snow_config_t; 37 | 38 | /* public type that represents a snowid at any given time */ 39 | struct snow_id { 40 | uint64_t timestamp; 41 | uint64_t worker_id; 42 | uint16_t sequence_id; 43 | }; 44 | 45 | typedef struct snow_id snow_id_t; 46 | 47 | typedef unsigned char snow_id_binary_t[16]; 48 | 49 | /** 50 | * Generates unique 128-bit id from current timestamp,worker_id,sequence_id. 51 | * sequence_id is incremented as many times as the function is called within the 52 | * same timestamp. 53 | * 54 | * @warning Not multi-thread safe 55 | * @param snowid - If able to generate an id, set the value of id to `snow_id`. 56 | * @return bool - true if successfully able to generate an id, false if not. 57 | */ 58 | bool snow_get_id(snow_id_t *snowid); 59 | 60 | /** 61 | * Dump snow state and config to stdout for debugging. 62 | * 63 | * @warning Not multi-thread safe 64 | * @param stream - any file descriptor (stdout if NULL) 65 | * @return void 66 | */ 67 | void snow_dump(FILE *stream); 68 | 69 | /** 70 | * Initializes the snowid engine with the config. It has to be called only once before 71 | * calling any other API functions. 72 | * 73 | * @warning Not multi-thread safe 74 | * @param config - The snowid configuration 75 | * @return void 76 | */ 77 | void snow_init(snow_config_t *config); 78 | 79 | /** 80 | * Deinitializes the snowid engine. It has to be called only once at the end 81 | * and no other API functions should be called after it except snow_init(). 82 | * 83 | * @warning Not multi-thread safe 84 | * @param void 85 | * @return void 86 | */ 87 | void snow_shutdown(void); 88 | 89 | /** 90 | * Generate a new snow_id as binary - unsigned char[16] representing 128 bits. 91 | * Represented as: 92 | * 93 | * 94 | * @param snowid_as_bin - set to snowid as binary 95 | * @return bool - true if success, else false. 96 | */ 97 | bool snow_get_id_as_binary(snow_id_binary_t snowid_as_bin); 98 | 99 | /** 100 | * Convert an already generated snow_id as binary (represented as byte array). 101 | * 102 | * @param snowid_as_bin - set the converted snowid as binary 103 | * @param snowid - pass in already generated snowid 104 | * @return bool - true if success, else false. 105 | */ 106 | bool snow_id_convert(snow_id_binary_t snowid_as_bin, const snow_id_t *snowid); 107 | 108 | #endif /* __SNOWID_H__ */ -------------------------------------------------------------------------------- /snowid_checkpoint.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "snowid_checkpoint.h" 31 | #include "snowid_util.h" 32 | 33 | #define CHECKPOINT_PERIOD_SECS 2 34 | 35 | static void snow_checkpoint_periodic(char *timestamp_path); 36 | 37 | static void snow_checkpoint_periodic(char *timestamp_path) 38 | { 39 | int fd; 40 | uint64_t buf[1]; 41 | 42 | if ((fd = open(timestamp_path, O_WRONLY)) == -1) { 43 | _exit(1); 44 | } 45 | 46 | uint64_t checkpoint; 47 | 48 | for(;;) { 49 | /* check if child is reparented to init(1), if so then exit. */ 50 | /* XXX - may not guarenteed to work if reparent is not pid == 1 */ 51 | if (getppid() == 1) { 52 | _exit(0); 53 | } 54 | 55 | if (get_current_ts(&checkpoint) == false) { 56 | continue; 57 | } 58 | 59 | buf[0] = checkpoint; 60 | 61 | /* XXX - ignore lseek() errors - it may recover during next try (: */ 62 | if (lseek(fd, 0, SEEK_SET) == -1) { 63 | continue; 64 | } 65 | 66 | /* XXX - ignore write() errors - it may recover during next try (: */ 67 | if (write(fd, buf, 1) != -1) { 68 | /* XXX - ignore fsync() errors - it may recover during next try (: */ 69 | fsync(fd); 70 | } 71 | 72 | sleep(CHECKPOINT_PERIOD_SECS); 73 | } 74 | 75 | _exit(1); 76 | } 77 | 78 | bool snow_checkpoint_start(char *timestamp_path) 79 | { 80 | 81 | if (timestamp_path == NULL) { 82 | fprintf(stderr, "snow_checkpoint_start():timestamp_path is NULL."); 83 | return false; 84 | } 85 | 86 | if (access(timestamp_path, W_OK) != 0) { 87 | perror("snow_checkpoint_start():error while checking access to `timestamp_path`."); 88 | return false; 89 | } 90 | 91 | int pid = fork(); 92 | 93 | if (pid < 0) { 94 | perror("snow_checkpoint_start():Call to fork failed."); 95 | return false; 96 | } 97 | 98 | /* child */ 99 | if (pid == 0) { 100 | snow_checkpoint_periodic(timestamp_path); 101 | } 102 | 103 | return true; 104 | } -------------------------------------------------------------------------------- /snowid_checkpoint.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #ifndef __SNOWID_CHECKPOINT_H__ 26 | #define __SNOWID_CHECKPOINT_H__ 27 | 28 | #include 29 | 30 | /** 31 | * Start a child process to periodically save current time in `timestamp_path` 32 | * location. 33 | * 34 | * @param timestamp_path - filepath to store timestamp 35 | * @return bool - true if success, else false. 36 | */ 37 | bool snow_checkpoint_start(char *timestamp_path); 38 | 39 | #endif /* __SNOWID_CHECKPOINT_H__ */ -------------------------------------------------------------------------------- /snowid_util.c: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #include "snowid_util.h" 38 | 39 | #ifdef __linux__ 40 | #include 41 | #include 42 | #define IF_HW_FAMILY AF_PACKET 43 | #else 44 | #include 45 | #define IF_HW_FAMILY AF_LINK /* MacOS/BSD */ 46 | #endif 47 | 48 | bool get_hw_addr_as_binary(uint64_t *workerid, char *interface) 49 | { 50 | struct ifaddrs *ifaddr; 51 | int family; 52 | bool found = false; 53 | 54 | if (getifaddrs(&ifaddr) == -1) { 55 | perror("get_all_hw_ifs():Call to getifaddrs failed"); 56 | goto fail; 57 | } 58 | 59 | for (struct ifaddrs *ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { 60 | if (ifa->ifa_addr == NULL) { 61 | continue; 62 | } 63 | 64 | family = ifa->ifa_addr->sa_family; 65 | 66 | /* skip if family is not link layer type */ 67 | if (family != IF_HW_FAMILY) { 68 | continue; 69 | } 70 | 71 | /* skip loopback - anything starting with lo */ 72 | if ((ifa->ifa_name == NULL) || strncmp(ifa->ifa_name, "lo", 2) == 0) { 73 | continue; 74 | } 75 | 76 | #ifdef __linux__ 77 | struct sockaddr_ll *sock_addr = (struct sockaddr_ll*)ifa->ifa_addr; 78 | for (int8_t i = 5; i >=0; --i) { 79 | *workerid |= (uint64_t)sock_addr->sll_addr[i] << (CHAR_BIT * i); 80 | } 81 | #else 82 | struct sockaddr_dl *sock_addr = (struct sockaddr_dl*)ifa->ifa_addr; 83 | unsigned char *ptr = (unsigned char *)LLADDR(sock_addr); 84 | for (int8_t i = 5; i >= 0; --i) { 85 | *workerid |= (uint64_t)*ptr++ << (CHAR_BIT * i); 86 | } 87 | #endif 88 | 89 | found = true; 90 | 91 | if (interface != NULL && strncmp(ifa->ifa_name, interface, strlen(interface) + 1) == 0) { 92 | break; 93 | } 94 | } 95 | 96 | freeifaddrs(ifaddr); 97 | 98 | fail: 99 | return found; 100 | } 101 | 102 | bool get_current_ts(uint64_t *result) 103 | { 104 | struct timeval now; 105 | 106 | if (result == NULL) { 107 | return false; 108 | } 109 | 110 | if (gettimeofday(&now, NULL) == -1) { 111 | return false; 112 | } 113 | 114 | *result = (uint64_t)((now.tv_sec * 1000) + (now.tv_usec / 1000)); 115 | 116 | return true; 117 | } -------------------------------------------------------------------------------- /snowid_util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * MIT License 4 | * 5 | * Copyright (c) 2022 beyonddream 6 | 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | 14 | * The above copyright notice and this permission notice shall be included in all 15 | * copies or substantial portions of the Software. 16 | 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | * SOFTWARE. 24 | */ 25 | #ifndef __SNOWID_UTIL_H__ 26 | #define __SNOWID_UTIL_H__ 27 | 28 | #include 29 | #include 30 | 31 | /** 32 | * Retrive a binary representation of the H/W (MAC) address either from the interface name 33 | * but if the interface name is not present then one of the H/W address from any of the 34 | * interfaces except loopback. 35 | * 36 | * @param workerid Binary representation of the mac address of the interface. 37 | * @param interface Interface name to pull the address from. 38 | * @return bool - true if we could return hw address, else false. 39 | */ 40 | bool get_hw_addr_as_binary(uint64_t *workerid, char *interface); 41 | 42 | /** 43 | * Get current timestamp 44 | * 45 | * @param result - set the current timestamp to the value pointed by result. 46 | * @return bool - true if we can get current timestamp, else false. 47 | */ 48 | bool get_current_ts(uint64_t *result); 49 | 50 | #endif /* __SNOWID_UTIL_H__ */ -------------------------------------------------------------------------------- /tests/snowid_test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "snowid.h" 7 | 8 | #define TEST_PRE_SETUP(config) \ 9 | snow_init(&(config)) 10 | 11 | #define TEST_POST_SETUP() \ 12 | snow_shutdown() 13 | 14 | #define TEST_CHECK_RESULT(desc, result) \ 15 | do { \ 16 | if ((result) != true) { \ 17 | fprintf(stderr, "%s: NOT_OK\n", (desc)); \ 18 | exit(1); \ 19 | } else { \ 20 | fprintf(stdout, "%s: OK\n", (desc)); \ 21 | } \ 22 | } while(0) 23 | 24 | #define PRINT_SNOWID_AS_BINARY(desc, snowid)\ 25 | do { \ 26 | printf("%s", desc); \ 27 | for(int8_t i = 0; i < 16; i++) { \ 28 | printf("%x", snowid[i]); \ 29 | if (i != 15) { \ 30 | printf(":"); \ 31 | } else { \ 32 | printf("\n"); \ 33 | } \ 34 | } \ 35 | } while(0) 36 | 37 | bool test_snow_get_id() 38 | { 39 | snow_config_t config = { 40 | .interface = "en0", 41 | .timestamp_path = "./timestamp.out", 42 | .allowable_downtime = 2592000000, 43 | }; 44 | 45 | TEST_PRE_SETUP(config); 46 | snow_id_t snow_id; 47 | bool expected = snow_get_id(&snow_id); 48 | TEST_POST_SETUP(); 49 | 50 | return expected; 51 | } 52 | 53 | bool test_snow_get_id_as_binary() 54 | { 55 | snow_config_t config = { 56 | .interface = "en0", 57 | .timestamp_path = "./timestamp.out", 58 | .allowable_downtime = 2592000000, 59 | }; 60 | 61 | TEST_PRE_SETUP(config); 62 | unsigned char out[16] = {0}; 63 | bool expected = snow_get_id_as_binary(out); 64 | TEST_POST_SETUP(); 65 | 66 | return expected; 67 | } 68 | 69 | bool test_snow_get_id_for_unknown_interface() 70 | { 71 | snow_config_t config = { 72 | .interface = "xxx", 73 | .timestamp_path = "./timestamp.out", 74 | .allowable_downtime = 2592000000, 75 | }; 76 | 77 | TEST_PRE_SETUP(config); 78 | snow_id_t snow_id; 79 | bool expected = snow_get_id(&snow_id); 80 | TEST_POST_SETUP(); 81 | 82 | return expected; 83 | } 84 | 85 | bool test_snow_get_id_as_binary_for_unknown_interface() 86 | { 87 | snow_config_t config = { 88 | .interface = "xxx", 89 | .timestamp_path = "./timestamp.out", 90 | .allowable_downtime = 2592000000, 91 | }; 92 | 93 | TEST_PRE_SETUP(config); 94 | unsigned char out[16] = {0}; 95 | bool expected = snow_get_id_as_binary(out); 96 | TEST_POST_SETUP(); 97 | 98 | return expected; 99 | } 100 | 101 | bool test_snow_get_id_multiple() 102 | { 103 | snow_config_t config = { 104 | .interface = "eno", 105 | .timestamp_path = "./timestamp.out", 106 | .allowable_downtime = 2592000000, 107 | }; 108 | 109 | TEST_PRE_SETUP(config); 110 | 111 | #define NO_OF_IDS 5 112 | 113 | snow_id_t arr_out[NO_OF_IDS] = {{0}, {0}, {0}, {0}, {0}}; 114 | snow_id_t out; 115 | bool expected; 116 | for (int8_t i = 0; i < NO_OF_IDS; i++) { 117 | expected = snow_get_id(&out); 118 | if (expected == false) { 119 | goto fail; 120 | } 121 | arr_out[i] = (snow_id_t) { 122 | .timestamp = out.timestamp, 123 | .worker_id = out.worker_id, 124 | .sequence_id = out.sequence_id 125 | }; 126 | } 127 | 128 | int8_t first, second; 129 | first = 0; 130 | second = 1; 131 | while (first < NO_OF_IDS && second < NO_OF_IDS) { 132 | snow_id_t f = arr_out[first]; 133 | snow_id_t s = arr_out[second]; 134 | /* verify ordering */ 135 | if ((f.timestamp > s.timestamp) 136 | || (f.worker_id != s.worker_id) 137 | || (f.sequence_id > s.sequence_id)) { 138 | expected = false; 139 | goto fail; 140 | } 141 | 142 | ++first; 143 | ++second; 144 | } 145 | #undef NO_OF_IDS 146 | 147 | fail: 148 | 149 | TEST_POST_SETUP(); 150 | 151 | return expected; 152 | } 153 | 154 | bool test_snow_get_id_and_verify_unique_timestamp() 155 | { 156 | snow_config_t config = { 157 | .interface = "eno", 158 | .timestamp_path = "./timestamp.out", 159 | .allowable_downtime = 2592000000, 160 | }; 161 | 162 | TEST_PRE_SETUP(config); 163 | 164 | #define NO_OF_IDS 2 165 | 166 | snow_id_t arr_out[NO_OF_IDS] = {{0}, {0}}; 167 | snow_id_t out; 168 | bool expected; 169 | for (int8_t i = 0; i < NO_OF_IDS; i++) { 170 | /* not ideal in a unit test but exception here to verify behavior */ 171 | /* we should also sleep just before every call to snow_get_id() */ 172 | sleep(1); 173 | expected = snow_get_id(&out); 174 | if (expected == false) { 175 | goto fail; 176 | } 177 | arr_out[i] = (snow_id_t) { 178 | .timestamp = out.timestamp, 179 | .worker_id = out.worker_id, 180 | .sequence_id = out.sequence_id 181 | }; 182 | } 183 | 184 | int8_t first, second; 185 | first = 0; 186 | second = 1; 187 | while (first < NO_OF_IDS && second < NO_OF_IDS) { 188 | snow_id_t f = arr_out[first]; 189 | snow_id_t s = arr_out[second]; 190 | /* verify ordering */ 191 | if ((f.timestamp >= s.timestamp) 192 | || (f.worker_id != s.worker_id) 193 | || (f.sequence_id != s.sequence_id)) { 194 | expected = false; 195 | goto fail; 196 | } 197 | 198 | ++first; 199 | ++second; 200 | } 201 | #undef NO_OF_IDS 202 | 203 | fail: 204 | 205 | TEST_POST_SETUP(); 206 | 207 | return expected; 208 | } 209 | 210 | int main(void) 211 | { 212 | 213 | TEST_CHECK_RESULT("test_snow_get_id", test_snow_get_id()); 214 | TEST_CHECK_RESULT("test_snow_get_id_as_binary", test_snow_get_id_as_binary()); 215 | TEST_CHECK_RESULT("test_snow_get_id_for_garbage_interface", 216 | test_snow_get_id_for_unknown_interface()); 217 | TEST_CHECK_RESULT("test_snow_get_id_as_binary_for_garbage_interface", 218 | test_snow_get_id_as_binary_for_unknown_interface()); 219 | TEST_CHECK_RESULT("test_snow_get_id_multiple", 220 | test_snow_get_id_multiple()); 221 | TEST_CHECK_RESULT("test_snow_get_id_and_verify_unique_timestamp", 222 | test_snow_get_id_and_verify_unique_timestamp()); 223 | 224 | return EXIT_SUCCESS; 225 | } --------------------------------------------------------------------------------