Skip to content

Commit 357b2ee

Browse files
Upgrade Sonyflake to v2 (#66)
* Add v2 * Introduce staticcheck * Introduce golangci-lint * Add error checks * Add error checks * Lint v2 code * Improve CI trigger * Use io.ReadAll * Use int64 * Remove NewSonyflake * Fix errors * v2: Change MachineID, sequence, and AmazonEC2MachineID to int; update all usage and tests for type consistency * docs: update Settings struct in README to use int for MachineID and CheckMachineID (v2) * docs(v2): clarify Settings, StartTime, MachineID, and CheckMachineID comments and update README links and explanations * docs(v2/mock): improve comments and docstrings for mock implementations * docs(types): unify and clarify package and type docstrings for types.go in v1 and v2 * test(v2): refactor and modernize tests, improve error assertions, and update mocks for v2 * test(v2): normalize whitespace in pseudoSleep calls for consistency * feat(v2): add configurable TimeUnit and refactor time handling for So… (#67) * feat(v2): add configurable TimeUnit and refactor time handling for Sonyflake v2 * test(v2): add ToTime tests, clarify TimeUnit behavior, and update docs for v2 * gofmt
1 parent 8d195df commit 357b2ee

19 files changed

Lines changed: 896 additions & 56 deletions

.github/workflows/test-v1.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: CI for v1 Module
2+
3+
on:
4+
push:
5+
paths-ignore:
6+
- .github/workflows/test-v2.yml
7+
- 'v2/**'
8+
pull_request:
9+
paths-ignore:
10+
- .github/workflows/test-v2.yml
11+
- 'v2/**'
12+
13+
jobs:
14+
test-v1:
15+
strategy:
16+
matrix:
17+
go-version: [1.23.x, 1.24.x]
18+
os: [ubuntu-latest]
19+
20+
runs-on: ${{matrix.os}}
21+
22+
defaults:
23+
run:
24+
working-directory: ./
25+
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v4
29+
30+
- name: Set up Go
31+
uses: actions/setup-go@v2
32+
with:
33+
go-version: ${{matrix.go-version}}
34+
35+
- name: Lint Code
36+
uses: golangci/golangci-lint-action@v6
37+
38+
- name: Check format
39+
run: test -z "`gofmt -l .`"
40+
41+
- name: Run tests
42+
run: go test -v ./...
43+
44+
- name: Build example
45+
run: cd example && ./linux64_build.sh

.github/workflows/test-v2.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: CI for v2 Module
2+
3+
on:
4+
push:
5+
paths:
6+
- .github/workflows/test-v2.yml
7+
- 'v2/**'
8+
pull_request:
9+
paths:
10+
- .github/workflows/test-v2.yml
11+
- 'v2/**'
12+
13+
jobs:
14+
test-v2:
15+
strategy:
16+
matrix:
17+
go-version: [1.23.x, 1.24.x]
18+
os: [ubuntu-latest]
19+
20+
runs-on: ${{matrix.os}}
21+
22+
defaults:
23+
run:
24+
working-directory: ./v2
25+
26+
steps:
27+
- name: Checkout code
28+
uses: actions/checkout@v4
29+
30+
- name: Set up Go
31+
uses: actions/setup-go@v5
32+
with:
33+
go-version-file: ./v2/go.mod
34+
35+
- name: Tidy modules
36+
run: go mod tidy
37+
38+
- name: Lint Code
39+
uses: golangci/golangci-lint-action@v6
40+
with:
41+
working-directory: ./v2
42+
43+
- name: Check format
44+
run: test -z "`gofmt -l .`"
45+
46+
- name: Run tests
47+
run: go test ./... -v
48+
49+
- name: Build example
50+
run: cd example && ./linux64_build.sh

.github/workflows/test.yml

Lines changed: 0 additions & 24 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
example/sonyflake_server
2+
v2/example/sonyflake_server

README.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ As a result, Sonyflake has the following advantages and disadvantages:
1818

1919
- The lifetime (174 years) is longer than that of Snowflake (69 years)
2020
- It can work in more distributed machines (2^16) than Snowflake (2^10)
21-
- It can generate 2^8 IDs per 10 msec at most in a single machine/thread (slower than Snowflake)
21+
- It can generate 2^8 IDs per 10 msec at most in a single instance (fewer than Snowflake)
2222

2323
However, if you want more generation rate in a single host,
24-
you can easily run multiple Sonyflake ID generators concurrently using goroutines.
24+
you can easily run multiple Sonyflake instances parallelly using goroutines.
2525

2626
Installation
2727
------------
2828

2929
```
30-
go get github.com/sony/sonyflake
30+
go get github.com/sony/sonyflake/v2
3131
```
3232

3333
Usage
@@ -43,42 +43,42 @@ You can configure Sonyflake by the struct Settings:
4343

4444
```go
4545
type Settings struct {
46+
TimeUnit time.Duration
4647
StartTime time.Time
47-
MachineID func() (uint16, error)
48-
CheckMachineID func(uint16) bool
48+
MachineID func() (int, error)
49+
CheckMachineID func(int) bool
4950
}
5051
```
5152

53+
- TimeUnit is the time unit of Sonyflake.
54+
If TimeUnit is 0, the default time unit is used, which is 10 msec.
55+
TimeUnit must be equal to or greater than 1 msec.
56+
5257
- StartTime is the time since which the Sonyflake time is defined as the elapsed time.
53-
If StartTime is 0, the start time of the Sonyflake is set to "2014-09-01 00:00:00 +0000 UTC".
54-
If StartTime is ahead of the current time, Sonyflake is not created.
58+
If StartTime is 0, the start time of the Sonyflake instance is set to "2025-01-01 00:00:00 +0000 UTC".
59+
StartTime must be before the current time.
5560

56-
- MachineID returns the unique ID of the Sonyflake instance.
57-
If MachineID returns an error, Sonyflake is not created.
58-
If MachineID is nil, default MachineID is used.
59-
Default MachineID returns the lower 16 bits of the private IP address.
61+
- MachineID returns the unique ID of a Sonyflake instance.
62+
If MachineID returns an error, the instance will not be created.
63+
If MachineID is nil, the default MachineID is used, which returns the lower 16 bits of the private IP address.
6064

61-
- CheckMachineID validates the uniqueness of the machine ID.
62-
If CheckMachineID returns false, Sonyflake is not created.
65+
- CheckMachineID validates the uniqueness of a machine ID.
66+
If CheckMachineID returns false, the instance will not be created.
6367
If CheckMachineID is nil, no validation is done.
6468

6569
In order to get a new unique ID, you just have to call the method NextID.
6670

6771
```go
68-
func (sf *Sonyflake) NextID() (uint64, error)
72+
func (sf *Sonyflake) NextID() (int64, error)
6973
```
7074

71-
NextID can continue to generate IDs for about 174 years from StartTime.
75+
NextID can continue to generate IDs for about 174 years from StartTime by default.
7276
But after the Sonyflake time is over the limit, NextID returns an error.
7377

74-
> **Note:**
75-
> Sonyflake currently does not use the most significant bit of IDs,
76-
> so you can convert Sonyflake IDs from `uint64` to `int64` safely.
77-
7878
AWS VPC and Docker
7979
------------------
8080

81-
The [awsutil](https://github.com/sony/sonyflake/blob/master/awsutil) package provides
81+
The [awsutil](https://github.com/sony/sonyflake/blob/master/v2/awsutil) package provides
8282
the function AmazonEC2MachineID that returns the lower 16-bit private IP address of the Amazon EC2 instance.
8383
It also works correctly on Docker
8484
by retrieving [instance metadata](http://docs.aws.amazon.com/en_us/AWSEC2/latest/UserGuide/ec2-instance-metadata.html).
@@ -89,7 +89,7 @@ So if each EC2 instance has a unique private IP address in AWS VPC,
8989
the lower 16 bits of the address is also unique.
9090
In this common case, you can use AmazonEC2MachineID as Settings.MachineID.
9191

92-
See [example](https://github.com/sony/sonyflake/blob/master/example) that runs Sonyflake on AWS Elastic Beanstalk.
92+
See [example](https://github.com/sony/sonyflake/blob/master/v2/example) that runs Sonyflake on AWS Elastic Beanstalk.
9393

9494
License
9595
-------

example/sonyflake_server.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@ func handler(w http.ResponseWriter, r *http.Request) {
3333
}
3434

3535
w.Header()["Content-Type"] = []string{"application/json; charset=utf-8"}
36-
w.Write(body)
36+
_, err = w.Write(body)
37+
if err != nil {
38+
http.Error(w, err.Error(), http.StatusInternalServerError)
39+
return
40+
}
3741
}
3842

3943
func main() {
4044
http.HandleFunc("/", handler)
41-
http.ListenAndServe(":8080", nil)
45+
err := http.ListenAndServe(":8080", nil)
46+
if err != nil {
47+
panic(err)
48+
}
4249
}

mock/sonyflake_mock.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// Package mock offers implementations of interfaces defined in types.go
2-
// This allows complete control over input / output for any given method that consumes
3-
// a given type
1+
// Package mock offers mock implementations of interfaces defined in types.go.
2+
// This allows complete control over input / output for any given method that consumes a given type.
43
package mock
54

65
import (
@@ -10,7 +9,7 @@ import (
109
"github.com/sony/sonyflake/types"
1110
)
1211

13-
// NewSuccessfulInterfaceAddrs returns a single private IP address
12+
// NewSuccessfulInterfaceAddrs returns a single private IP address.
1413
func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs {
1514
ifat := make([]net.Addr, 0, 1)
1615
ifat = append(ifat, &net.IPNet{IP: []byte{192, 168, 0, 1}, Mask: []byte{255, 0, 0, 0}})
@@ -20,14 +19,14 @@ func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs {
2019
}
2120
}
2221

23-
// NewFailingInterfaceAddrs returns an error
22+
// NewFailingInterfaceAddrs returns an error.
2423
func NewFailingInterfaceAddrs() types.InterfaceAddrs {
2524
return func() ([]net.Addr, error) {
2625
return nil, fmt.Errorf("test error")
2726
}
2827
}
2928

30-
// NewFailingInterfaceAddrs returns an empty slice of addresses
29+
// NewNilInterfaceAddrs returns an empty slice of addresses.
3130
func NewNilInterfaceAddrs() types.InterfaceAddrs {
3231
return func() ([]net.Addr, error) {
3332
return []net.Addr{}, nil

types/types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
// Package Types defines type signatures used throughout SonyFlake. This allows for
2-
// fine-tuned control over imports, and the ability to mock out imports as well
1+
// Package types defines type signatures used throughout sonyflake.
2+
// This provides the ability to mock out imports.
33
package types
44

55
import "net"
66

7-
// InterfaceAddrs defines the interface used for retrieving network addresses
7+
// InterfaceAddrs defines the interface used for retrieving network addresses.
88
type InterfaceAddrs func() ([]net.Addr, error)

v2/awsutil/awsutil.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Package awsutil provides utility functions for using Sonyflake on AWS.
2+
package awsutil
3+
4+
import (
5+
"errors"
6+
"io"
7+
"net"
8+
"net/http"
9+
"os/exec"
10+
"regexp"
11+
"strconv"
12+
"time"
13+
)
14+
15+
func amazonEC2PrivateIPv4() (net.IP, error) {
16+
res, err := http.Get("http://169.254.169.254/latest/meta-data/local-ipv4")
17+
if err != nil {
18+
return nil, err
19+
}
20+
defer res.Body.Close()
21+
22+
body, err := io.ReadAll(res.Body)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
ip := net.ParseIP(string(body))
28+
if ip == nil {
29+
return nil, errors.New("invalid ip address")
30+
}
31+
return ip.To4(), nil
32+
}
33+
34+
// AmazonEC2MachineID retrieves the private IP address of the Amazon EC2 instance
35+
// and returns its lower 16 bits.
36+
// It works correctly on Docker as well.
37+
func AmazonEC2MachineID() (int, error) {
38+
ip, err := amazonEC2PrivateIPv4()
39+
if err != nil {
40+
return 0, err
41+
}
42+
43+
return int(ip[2])<<8 + int(ip[3]), nil
44+
}
45+
46+
// TimeDifference returns the time difference between the localhost and the given NTP server.
47+
func TimeDifference(server string) (time.Duration, error) {
48+
output, err := exec.Command("/usr/sbin/ntpdate", "-q", server).CombinedOutput()
49+
if err != nil {
50+
return time.Duration(0), err
51+
}
52+
53+
re, _ := regexp.Compile("offset (.*) sec")
54+
submatched := re.FindSubmatch(output)
55+
if len(submatched) != 2 {
56+
return time.Duration(0), errors.New("invalid ntpdate output")
57+
}
58+
59+
f, err := strconv.ParseFloat(string(submatched[1]), 64)
60+
if err != nil {
61+
return time.Duration(0), err
62+
}
63+
return time.Duration(f*1000) * time.Millisecond, nil
64+
}

v2/example/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM ubuntu:24.04
2+
3+
ADD ./sonyflake_server /
4+
ENTRYPOINT ["/sonyflake_server"]
5+
6+
EXPOSE 8080

0 commit comments

Comments
 (0)