11 Commits

Author SHA1 Message Date
Yoshiyuki Mineo
8d195df6f7 Fix a missing link (#65) 2025-05-01 19:25:49 +09:00
Yoshiyuki Mineo
410eb250e3 Update Go versions (#64) 2025-05-01 19:15:37 +09:00
Yoshiyuki Mineo
94f43cfd99 Update Go versions (#60) 2024-10-13 22:30:52 +09:00
Yoshiyuki Mineo
b9b40b47a5 Update go versions (#54) 2024-04-30 15:50:36 +09:00
Yoshiyuki Mineo
a0558cef64 Link local (#53)
* Allow IPv4 Link Local addresses (#50)

* Allow IPv4 Link Local addresses

Allow the use of link local addresses

* Update sonyflake.go

* Update sonyflake.go

* Update a comment

---------

Co-authored-by: Flavio Crisciani <f.crisciani@gmail.com>
2024-04-30 15:06:58 +09:00
David E. Wheeler
fc2f84a086 Use net.IP.Equal instead of bytes.Equal (#49)
As suggested by go-staticcheck.
2023-11-06 14:32:46 +09:00
Yoshiyuki Mineo
06f9b47996 Introduce New function (#47)
* feat(Sonyflake): define error variables

* feat(Sonyflake): add New() function
- minor logic improvements
- return errors

* tests(Sonyflake): remove old TestNilSonyflake test function in favour of the New() function coverage

* gofmt

* Update error messages and comments

* Introduce New function

---------

Co-authored-by: Quetzy Garcia <quetzy.garcia@integrate.com>
2023-08-14 01:27:55 +09:00
Yoshiyuki Mineo
18c4908321 Update go versions 2023-08-13 23:36:13 +09:00
Yoshiyuki Mineo
eafab81cd5 Update go versions (#43) 2023-05-04 02:08:54 +09:00
Yoshiyuki Mineo
597171da2e Increase coverage (#42)
* Introduce mocking framework and increase coverage (#22)

This change introduces two common golang patterns:

- types: this will allow fine-tuned control over imported
types by defining where they will be used and how

- mock: this allows the generation of mock constructors,
which allows for testing any individual path in a method by
"injecting" a mock method which matches the expected type

This change also increases test coverage to 100%

Co-authored-by: Yoshiyuki Mineo <Yoshiyuki.Mineo@jp.sony.com>

* gofmt

---------

Co-authored-by: Bradley Boutcher <btboutcher@icloud.com>
2023-05-04 00:08:20 +09:00
Yoshiyuki Mineo
cc94b60628 Check time order explicitly (#37)
* Add sortable test (#11)

* add TestSortableID to make sure that generated ID(s) can be sorted like you are using increment id database

* add TestSortableID to make sure that generated ID(s) can be sorted like you are using increment id database

Co-authored-by: Yoshiyuki Mineo <Yoshiyuki.Mineo@jp.sony.com>

* gofmt

* Check time order explicitly

Co-authored-by: Yusuf Syaifudin <yusuf.syaifudin@gmail.com>
2022-08-17 14:33:52 +09:00
6 changed files with 242 additions and 47 deletions

View File

@@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.18.x, 1.19.x]
go-version: [1.23.x, 1.24.x]
os: [ubuntu-latest]
runs-on: ${{matrix.os}}
steps:

View File

@@ -33,10 +33,10 @@ go get github.com/sony/sonyflake
Usage
-----
The function NewSonyflake creates a new Sonyflake instance.
The function New creates a new Sonyflake instance.
```go
func NewSonyflake(st Settings) *Sonyflake
func New(st Settings) (*Sonyflake, error)
```
You can configure Sonyflake by the struct Settings:
@@ -83,8 +83,8 @@ the function AmazonEC2MachineID that returns the lower 16-bit private IP address
It also works correctly on Docker
by retrieving [instance metadata](http://docs.aws.amazon.com/en_us/AWSEC2/latest/UserGuide/ec2-instance-metadata.html).
[AWS VPC](http://docs.aws.amazon.com/en_us/AmazonVPC/latest/UserGuide/VPC_Subnets.html)
is assigned a single CIDR with a netmask between /28 and /16.
[AWS IPv4 VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html)
is usually assigned a single CIDR with a netmask between /28 and /16.
So if each EC2 instance has a unique private IP address in AWS VPC,
the lower 16 bits of the address is also unique.
In this common case, you can use AmazonEC2MachineID as Settings.MachineID.

35
mock/sonyflake_mock.go Normal file
View File

@@ -0,0 +1,35 @@
// Package mock offers implementations of interfaces defined in types.go
// This allows complete control over input / output for any given method that consumes
// a given type
package mock
import (
"fmt"
"net"
"github.com/sony/sonyflake/types"
)
// NewSuccessfulInterfaceAddrs returns a single private IP address
func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs {
ifat := make([]net.Addr, 0, 1)
ifat = append(ifat, &net.IPNet{IP: []byte{192, 168, 0, 1}, Mask: []byte{255, 0, 0, 0}})
return func() ([]net.Addr, error) {
return ifat, nil
}
}
// NewFailingInterfaceAddrs returns an error
func NewFailingInterfaceAddrs() types.InterfaceAddrs {
return func() ([]net.Addr, error) {
return nil, fmt.Errorf("test error")
}
}
// NewFailingInterfaceAddrs returns an empty slice of addresses
func NewNilInterfaceAddrs() types.InterfaceAddrs {
return func() ([]net.Addr, error) {
return []net.Addr{}, nil
}
}

View File

@@ -12,6 +12,8 @@ import (
"net"
"sync"
"time"
"github.com/sony/sonyflake/types"
)
// These constants are the bit lengths of Sonyflake ID parts.
@@ -50,19 +52,29 @@ type Sonyflake struct {
machineID uint16
}
// NewSonyflake returns a new Sonyflake configured with the given Settings.
// NewSonyflake returns nil in the following cases:
var (
ErrStartTimeAhead = errors.New("start time is ahead of now")
ErrNoPrivateAddress = errors.New("no private ip address")
ErrOverTimeLimit = errors.New("over the time limit")
ErrInvalidMachineID = errors.New("invalid machine id")
)
var defaultInterfaceAddrs = net.InterfaceAddrs
// New returns a new Sonyflake configured with the given Settings.
// New returns an error in the following cases:
// - Settings.StartTime is ahead of the current time.
// - Settings.MachineID returns an error.
// - Settings.CheckMachineID returns false.
func NewSonyflake(st Settings) *Sonyflake {
func New(st Settings) (*Sonyflake, error) {
if st.StartTime.After(time.Now()) {
return nil, ErrStartTimeAhead
}
sf := new(Sonyflake)
sf.mutex = new(sync.Mutex)
sf.sequence = uint16(1<<BitLenSequence - 1)
if st.StartTime.After(time.Now()) {
return nil
}
if st.StartTime.IsZero() {
sf.startTime = toSonyflakeTime(time.Date(2014, 9, 1, 0, 0, 0, 0, time.UTC))
} else {
@@ -71,14 +83,28 @@ func NewSonyflake(st Settings) *Sonyflake {
var err error
if st.MachineID == nil {
sf.machineID, err = lower16BitPrivateIP()
sf.machineID, err = lower16BitPrivateIP(defaultInterfaceAddrs)
} else {
sf.machineID, err = st.MachineID()
}
if err != nil || (st.CheckMachineID != nil && !st.CheckMachineID(sf.machineID)) {
return nil
if err != nil {
return nil, err
}
if st.CheckMachineID != nil && !st.CheckMachineID(sf.machineID) {
return nil, ErrInvalidMachineID
}
return sf, nil
}
// NewSonyflake returns a new Sonyflake configured with the given Settings.
// NewSonyflake returns nil in the following cases:
// - Settings.StartTime is ahead of the current time.
// - Settings.MachineID returns an error.
// - Settings.CheckMachineID returns false.
func NewSonyflake(st Settings) *Sonyflake {
sf, _ := New(st)
return sf
}
@@ -123,7 +149,7 @@ func sleepTime(overtime int64) time.Duration {
func (sf *Sonyflake) toID() (uint64, error) {
if sf.elapsedTime >= 1<<BitLenTime {
return 0, errors.New("over the time limit")
return 0, ErrOverTimeLimit
}
return uint64(sf.elapsedTime)<<(BitLenSequence+BitLenMachineID) |
@@ -131,8 +157,8 @@ func (sf *Sonyflake) toID() (uint64, error) {
uint64(sf.machineID), nil
}
func privateIPv4() (net.IP, error) {
as, err := net.InterfaceAddrs()
func privateIPv4(interfaceAddrs types.InterfaceAddrs) (net.IP, error) {
as, err := interfaceAddrs()
if err != nil {
return nil, err
}
@@ -148,16 +174,17 @@ func privateIPv4() (net.IP, error) {
return ip, nil
}
}
return nil, errors.New("no private ip address")
return nil, ErrNoPrivateAddress
}
func isPrivateIPv4(ip net.IP) bool {
// Allow private IP addresses (RFC1918) and link-local addresses (RFC3927)
return ip != nil &&
(ip[0] == 10 || ip[0] == 172 && (ip[1] >= 16 && ip[1] < 32) || ip[0] == 192 && ip[1] == 168)
(ip[0] == 10 || ip[0] == 172 && (ip[1] >= 16 && ip[1] < 32) || ip[0] == 192 && ip[1] == 168 || ip[0] == 169 && ip[1] == 254)
}
func lower16BitPrivateIP() (uint16, error) {
ip, err := privateIPv4()
func lower16BitPrivateIP(interfaceAddrs types.InterfaceAddrs) (uint16, error) {
ip, err := privateIPv4(interfaceAddrs)
if err != nil {
return 0, err
}

View File

@@ -1,10 +1,15 @@
package sonyflake
import (
"errors"
"fmt"
"net"
"runtime"
"testing"
"time"
"github.com/sony/sonyflake/mock"
"github.com/sony/sonyflake/types"
)
var sf *Sonyflake
@@ -23,7 +28,7 @@ func init() {
startTime = toSonyflakeTime(st.StartTime)
ip, _ := lower16BitPrivateIP()
ip, _ := lower16BitPrivateIP(defaultInterfaceAddrs)
machineID = uint64(ip)
}
@@ -35,6 +40,60 @@ func nextID(t *testing.T) uint64 {
return id
}
func TestNew(t *testing.T) {
genError := fmt.Errorf("an error occurred while generating ID")
tests := []struct {
name string
settings Settings
err error
}{
{
name: "failure: time ahead",
settings: Settings{
StartTime: time.Now().Add(time.Minute),
},
err: ErrStartTimeAhead,
},
{
name: "failure: machine ID",
settings: Settings{
MachineID: func() (uint16, error) {
return 0, genError
},
},
err: genError,
},
{
name: "failure: invalid machine ID",
settings: Settings{
CheckMachineID: func(uint16) bool {
return false
},
},
err: ErrInvalidMachineID,
},
{
name: "success",
settings: Settings{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sonyflake, err := New(test.settings)
if !errors.Is(err, test.err) {
t.Fatalf("unexpected value, want %#v, got %#v", test.err, err)
}
if sonyflake == nil && err == nil {
t.Fatal("unexpected value, sonyflake should not be nil")
}
})
}
}
func TestSonyflakeOnce(t *testing.T) {
sleepTime := time.Duration(50 * sonyflakeTimeUnit)
time.Sleep(sleepTime)
@@ -76,9 +135,12 @@ func TestSonyflakeFor10Sec(t *testing.T) {
parts := Decompose(id)
numID++
if id <= lastID {
if id == lastID {
t.Fatal("duplicated id")
}
if id < lastID {
t.Fatal("must increase with time")
}
lastID = id
current = currentTime()
@@ -142,30 +204,6 @@ func TestSonyflakeInParallel(t *testing.T) {
fmt.Println("number of id:", len(set))
}
func TestNilSonyflake(t *testing.T) {
var startInFuture Settings
startInFuture.StartTime = time.Now().Add(time.Duration(1) * time.Minute)
if NewSonyflake(startInFuture) != nil {
t.Errorf("sonyflake starting in the future")
}
var noMachineID Settings
noMachineID.MachineID = func() (uint16, error) {
return 0, fmt.Errorf("no machine id")
}
if NewSonyflake(noMachineID) != nil {
t.Errorf("sonyflake with no machine id")
}
var invalidMachineID Settings
invalidMachineID.CheckMachineID = func(uint16) bool {
return false
}
if NewSonyflake(invalidMachineID) != nil {
t.Errorf("sonyflake with invalid machine id")
}
}
func pseudoSleep(period time.Duration) {
sf.startTime -= int64(period) / sonyflakeTimeUnit
}
@@ -182,6 +220,93 @@ func TestNextIDError(t *testing.T) {
}
}
func TestPrivateIPv4(t *testing.T) {
testCases := []struct {
description string
expected net.IP
interfaceAddrs types.InterfaceAddrs
error string
}{
{
description: "InterfaceAddrs returns an error",
expected: nil,
interfaceAddrs: mock.NewFailingInterfaceAddrs(),
error: "test error",
},
{
description: "InterfaceAddrs returns an empty or nil list",
expected: nil,
interfaceAddrs: mock.NewNilInterfaceAddrs(),
error: "no private ip address",
},
{
description: "InterfaceAddrs returns one or more IPs",
expected: net.IP{192, 168, 0, 1},
interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(),
error: "",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
actual, err := privateIPv4(tc.interfaceAddrs)
if (err != nil) && (tc.error == "") {
t.Errorf("expected no error, but got: %s", err)
return
} else if (err != nil) && (tc.error != "") {
return
}
if net.IP.Equal(actual, tc.expected) {
return
} else {
t.Errorf("error: expected: %s, but got: %s", tc.expected, actual)
}
})
}
}
func TestLower16BitPrivateIP(t *testing.T) {
testCases := []struct {
description string
expected uint16
interfaceAddrs types.InterfaceAddrs
error string
}{
{
description: "InterfaceAddrs returns an empty or nil list",
expected: 0,
interfaceAddrs: mock.NewNilInterfaceAddrs(),
error: "no private ip address",
},
{
description: "InterfaceAddrs returns one or more IPs",
expected: 1,
interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(),
error: "",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
actual, err := lower16BitPrivateIP(tc.interfaceAddrs)
if (err != nil) && (tc.error == "") {
t.Errorf("expected no error, but got: %s", err)
return
} else if (err != nil) && (tc.error != "") {
return
}
if actual == tc.expected {
return
} else {
t.Errorf("error: expected: %v, but got: %v", tc.expected, actual)
}
})
}
}
func TestSonyflakeTimeUnit(t *testing.T) {
if time.Duration(sonyflakeTimeUnit) != 10*time.Millisecond {
t.Errorf("unexpected time unit")

8
types/types.go Normal file
View File

@@ -0,0 +1,8 @@
// Package Types defines type signatures used throughout SonyFlake. This allows for
// fine-tuned control over imports, and the ability to mock out imports as well
package types
import "net"
// InterfaceAddrs defines the interface used for retrieving network addresses
type InterfaceAddrs func() ([]net.Addr, error)