diff --git a/app.go b/app.go index 061d718..1857357 100644 --- a/app.go +++ b/app.go @@ -19,10 +19,11 @@ import ( ) const ( - redeemPath = "/idm/api/v1/appregistry/otp/redeem" - unlinkPath = "/idm/api/v1/appregistry/applications/%s/tenants/%s" - getDevicesPath = "/api/uno/v1/registry/devices" - directModePath = "/api/dxhub/v2/apiproxy/request/%s/direct%s" + newAppInstancePath = "/idm/api/v1/appregistry/otp/new" + redeemPath = "/idm/api/v1/appregistry/otp/redeem" + unlinkPath = "/idm/api/v1/appregistry/applications/%s/tenants/%s" + getDevicesPath = "/api/uno/v1/registry/devices" + directModePath = "/api/dxhub/v2/apiproxy/request/%s/direct%s" ) var tlsConfig = tls.Config{ @@ -82,8 +83,12 @@ type Config struct { Transport *http.Transport // GetCredentials is used to retrieve the client credentials provided to the app during onboarding + // Either use this or ApiKey GetCredentials func() (*Credentials, error) + // ApiKey is used when GetCredentials is not specified + ApiKey string + // DeviceActivationHandler notifies when a device is activated DeviceActivationHandler func(device *Device) @@ -105,10 +110,11 @@ type App struct { deviceMap sync.Map // Error channel should be used to monitor any errors - Error chan error - wg sync.WaitGroup - ctx context.Context - ctxCancel context.CancelFunc + Error chan error + wg sync.WaitGroup + ctx context.Context + ctxCancel context.CancelFunc + startPubsubConnectOnce sync.Once } func (app *App) String() string { @@ -131,13 +137,18 @@ func New(config Config) (*App, error) { httpClient := resty.New(). SetBaseURL(hostURL.String()). OnBeforeRequest(func(_ *resty.Client, request *resty.Request) error { - credentials, err := config.GetCredentials() - if err != nil { - return err + var key string + if config.GetCredentials != nil { + credentials, err := config.GetCredentials() + if err != nil { + return err + } + key = string(credentials.ApiKey) + zeroByteArray(credentials.ApiKey) + } else { + key = config.ApiKey } - - request.SetHeader("X-Api-Key", string(credentials.ApiKey)) - zeroByteArray(credentials.ApiKey) + request.SetHeader("X-Api-Key", key) return nil }) @@ -157,48 +168,6 @@ func New(config Config) (*App, error) { } app.ctx, app.ctxCancel = context.WithCancel(context.Background()) - - app.wg.Add(1) - go func() { - defer app.wg.Done() - backoffFactor := 0 - maxbackoffFactor := 3 - reconnectBackoff := 30 * time.Second - reconnectDelay := 30 * time.Second - - //loop to call app.connect with a reconnect delay with gradual backoff - for { - err := app.connect("") - if err != nil { - log.Logger.Errorf("Failed to connect the app: %v", err) - app.close() - app.reportError(err) - } else { - //obtain the device list - app.loadTenantsDevices() - //reset backoff factor for successful connection - backoffFactor = 0 - - select { - case err = <-app.conn.Error: - app.close() - app.reportError(err) - case <-app.ctx.Done(): - return - } - } - - select { - case <-time.After(reconnectDelay + reconnectBackoff*time.Duration(backoffFactor)): - //increment backoff factor by 1 for gradual backoff - if backoffFactor < maxbackoffFactor { - backoffFactor += 1 - } - case <-app.ctx.Done(): - return - } - } - }() return app, nil } @@ -279,19 +248,22 @@ func (app *App) close() error { return nil } -// connect opens a websocket connection to pxGrid Cloud -// WORKAROUND provide an option to use previous subscription ID -func (app *App) connect(subscriptionID string) error { +// pubsubConnect opens a websocket connection to pxGrid Cloud +func (app *App) pubsubConnect() error { var err error app.conn, err = pubsub.NewConnection(pubsub.Config{ GroupID: app.config.GroupID, Domain: url.PathEscape(app.config.RegionalFQDN), APIKeyProvider: func() ([]byte, error) { - credentials, e := app.config.GetCredentials() - if e != nil { - return nil, e + if app.config.GetCredentials != nil { + credentials, e := app.config.GetCredentials() + if e != nil { + return nil, e + } + return credentials.ApiKey, e + } else { + return []byte(app.config.ApiKey), nil } - return credentials.ApiKey, e }, Transport: app.config.Transport, }) @@ -566,6 +538,8 @@ func (app *App) setTenant(tenant *Tenant) error { } app.deviceMap.Store(tenant.id, &deviceMapInternal) + // Once a tenant is added, we can start pubsub + app.startPubsubConnect() return nil } @@ -592,3 +566,49 @@ func (app *App) loadTenantsDevices() { return true }) } + +func (app *App) startPubsubConnect() { + app.startPubsubConnectOnce.Do(func() { + app.wg.Add(1) + go func() { + defer app.wg.Done() + backoffFactor := 0 + maxbackoffFactor := 3 + reconnectBackoff := 30 * time.Second + reconnectDelay := 30 * time.Second + + //loop to call app.connect with a reconnect delay with gradual backoff + for { + err := app.pubsubConnect() + if err != nil { + log.Logger.Errorf("Failed to connect the app: %v", err) + app.close() + app.reportError(err) + } else { + //obtain the device list + app.loadTenantsDevices() + //reset backoff factor for successful connection + backoffFactor = 0 + + select { + case err = <-app.conn.Error: + app.close() + app.reportError(err) + case <-app.ctx.Done(): + return + } + } + + select { + case <-time.After(reconnectDelay + reconnectBackoff*time.Duration(backoffFactor)): + //increment backoff factor by 1 for gradual backoff + if backoffFactor < maxbackoffFactor { + backoffFactor += 1 + } + case <-app.ctx.Done(): + return + } + } + }() + }) +} diff --git a/app_test.go b/app_test.go index 0a2d3ba..1a17634 100644 --- a/app_test.go +++ b/app_test.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net/http" + "net/http/httptest" "net/url" "sync" "testing" @@ -57,14 +58,15 @@ var ( type AppTestSuite struct { suite.Suite config Config + s *httptest.Server } func (suite *AppTestSuite) SetupTest() { - s := test.NewRPCServer(suite.T(), test.Config{ + suite.s = test.NewRPCServer(suite.T(), test.Config{ PubSubPath: apiPaths.pubsub, SubscriptionsPath: apiPaths.subscriptions, }) - u, _ := url.Parse(s.URL) + u, _ := url.Parse(suite.s.URL) suite.config = Config{ ID: "appId", @@ -89,6 +91,10 @@ func (suite *AppTestSuite) SetupTest() { } } +func (suite *AppTestSuite) TearDownTest() { + suite.s.Close() +} + func (suite *AppTestSuite) TestNew() { app, err := New(suite.config) suite.Nil(err) diff --git a/appinstance.go b/appinstance.go new file mode 100644 index 0000000..8e73ab2 --- /dev/null +++ b/appinstance.go @@ -0,0 +1,92 @@ +package cloud + +import ( + "errors" + + "github.com/rs/xid" +) + +type appInstanceRequest struct { + OTP string `json:"otp"` + ParentAppID string `json:"parent_application_id"` + InstanceID string `json:"instance_identifier"` + InstanceName string `json:"instance_name"` +} + +type appInstanceResponse struct { + AppID string `json:"app_id"` + AppApiKey string `json:"app_api_key"` + TenantToken string `json:"api_token"` + TenantID string `json:"tenant_id"` + TenantName string `json:"tenant_name"` +} + +// LinkTenantWithNewAppInstance redeems the OTP and links a tenant to a new application instance. +// InstanceName will be shown in UI to signify the new application instance. +// The returned App.ID(), App.ApiKey(), Tenant.ID(), Tenant.Name() and Tenant.ApiToken() must be stored securely. +func (app *App) LinkTenantWithNewAppInstance(otp, instanceName string) (*App, *Tenant, error) { + appReq := appInstanceRequest{ + OTP: otp, + ParentAppID: app.config.ID, + InstanceID: xid.New().String(), + InstanceName: instanceName, + } + + var appResp appInstanceResponse + var errorResp errorResponse + + r, err := app.httpClient.R(). + SetBody(appReq). + SetResult(&appResp). + SetError(&errorResp). + Post(newAppInstancePath) + if err != nil { + return nil, nil, err + } + if r.IsError() { + return nil, nil, errors.New(errorResp.GetError()) + } + + appConfig := app.newAppConfig(appResp.AppID, appResp.AppApiKey) + + childApp, err := New(appConfig) + if err != nil { + return nil, nil, err + } + tenant, err := childApp.SetTenant(appResp.TenantID, appResp.TenantName, appResp.TenantToken) + if err != nil { + return nil, nil, err + } + return childApp, tenant, nil +} + +// SetAppInstance adds an application instance. +// The appID and appApiKey are obtained from calling LinkTenantWithNewAppInstance. +// This should be used by application after restart to reload application instances +func (app *App) SetAppInstance(appID, appApiKey string) (*App, error) { + config := app.newAppConfig(appID, appApiKey) + return New(config) +} + +func (app *App) newAppConfig(appID, appApiKey string) Config { + return Config{ + ID: appID, + GlobalFQDN: app.config.GlobalFQDN, + RegionalFQDN: app.config.RegionalFQDN, + ReadStreamID: "app--" + appID + "-R", + WriteStreamID: "app--" + appID + "-W", + Transport: app.config.Transport, + ApiKey: appApiKey, + DeviceActivationHandler: app.config.DeviceActivationHandler, + DeviceDeactivationHandler: app.config.DeviceDeactivationHandler, + DeviceMessageHandler: app.config.DeviceMessageHandler, + } +} + +func (app *App) ID() string { + return app.config.ID +} + +func (app *App) ApiKey() string { + return app.config.ApiKey +} diff --git a/examples/multi-instance/config_sample.yaml b/examples/multi-instance/config_sample.yaml new file mode 100644 index 0000000..5854603 --- /dev/null +++ b/examples/multi-instance/config_sample.yaml @@ -0,0 +1,18 @@ +app: + id: replace_with_app_id + apiKey: replace_with_api_key + globalFQDN: dnaservices.cisco.com + regionalFQDN: neoffers.cisco.com + readStream: replace_with_read_stream + writeStream: replace_with_write_stream + +appInstance: + otp: replace_with_otp_from_dna_cloud + name: Instance 001 + # config file will be rewritten with id, key after using OTP + id: + apiKey: + tenant: + id: + name: + token: diff --git a/examples/multi-instance/main.go b/examples/multi-instance/main.go new file mode 100644 index 0000000..8cd208d --- /dev/null +++ b/examples/multi-instance/main.go @@ -0,0 +1,243 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/cisco-pxgrid/cloud-sdk-go/log" + "gopkg.in/yaml.v2" + + sdk "github.com/cisco-pxgrid/cloud-sdk-go" +) + +var logger *log.DefaultLogger = &log.DefaultLogger{Level: log.LogLevelInfo} + +type appConfig struct { + Id string `yaml:"id"` + ApiKey string `yaml:"apiKey"` + GlobalFQDN string `yaml:"globalFQDN"` + RegionalFQDN string `yaml:"regionalFQDN"` + ReadStream string `yaml:"readStream"` + WriteStream string `yaml:"writeStream"` +} + +type appInstanceConfig struct { + Otp string `yaml:"otp"` + Name string `yaml:"name"` + Id string `yaml:"id"` + ApiKey string `yaml:"apiKey"` + Tenant tenantConfig `yaml:"tenant"` +} + +type tenantConfig struct { + Id string `yaml:"id"` + Name string `yaml:"name"` + Token string `yaml:"token"` +} + +type config struct { + App appConfig `yaml:"app"` + AppInstance appInstanceConfig `yaml:"appInstance"` +} + +var selectedDevice *sdk.Device + +func messageHandler(id string, d *sdk.Device, stream string, p []byte) { + logger.Infof("Received message id=%s tenant=%s device=%s stream=%s payload=%s", + id, d.Tenant().Name(), d.Name(), stream, string(p)) +} + +func activationHandler(d *sdk.Device) { + logger.Infof("Device activation: %v", d) + devices, err := d.Tenant().GetDevices() + if err == nil { + selectedDevice = &devices[0] + logger.Infof("Selected first device. name=%s tenant=%s id=%s ", selectedDevice.Name(), selectedDevice.Tenant().Name(), selectedDevice.ID()) + } else { + logger.Errorf("Failed to GetDevices: %v", err) + } +} + +func deactivationHandler(d *sdk.Device) { + logger.Infof("Device deactivation: %v", d) + devices, err := d.Tenant().GetDevices() + if err == nil { + if len(devices) > 0 { + selectedDevice = &devices[0] + logger.Infof("Selected first device. name=%s tenant=%s id=%s ", selectedDevice.Name(), selectedDevice.Tenant().Name(), selectedDevice.ID()) + } else { + selectedDevice = nil + logger.Infof("No device found") + } + } else { + logger.Errorf("Failed to GetDevices: %v", err) + } +} + +func loadConfig(file string) (*config, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + c := config{} + err = yaml.Unmarshal(data, &c) + if err != nil { + return nil, err + } + return &c, nil +} + +func (c *config) store(file string) error { + data, err := yaml.Marshal(c) + if err != nil { + return err + } + return os.WriteFile(file, data, 0644) +} + +func main() { + // Load config + configFile := flag.String("config", "", "Configuration yaml file to use (required)") + debug := flag.Bool("debug", false, "Enable debug output") + insecure := flag.Bool("insecure", false, "Insecure TLS") + flag.Parse() + config, err := loadConfig(*configFile) + if err != nil { + panic(err) + } + + // Set logger + log.Logger = logger + if *debug { + logger.Level = log.LogLevelDebug + } + + // Log after set logger + logger.Debugf("Config: %+v", config) + + // SDK App config + getCredentials := func() (*sdk.Credentials, error) { + return &sdk.Credentials{ + ApiKey: []byte(config.App.ApiKey), + }, nil + } + appConfig := sdk.Config{ + ID: config.App.Id, + GetCredentials: getCredentials, + GlobalFQDN: config.App.GlobalFQDN, + RegionalFQDN: config.App.RegionalFQDN, + DeviceActivationHandler: activationHandler, + DeviceDeactivationHandler: deactivationHandler, + DeviceMessageHandler: messageHandler, + ReadStreamID: config.App.ReadStream, + WriteStreamID: config.App.WriteStream, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: *insecure, + }, + Proxy: http.ProxyFromEnvironment, + }, + } + // SDK App create + app, err := sdk.New(appConfig) + if err != nil { + panic(err) + } + logger.Debugf("App config: %+v", appConfig) + + var ac = &config.AppInstance + var tc = &ac.Tenant + var tenant *sdk.Tenant + var appInstance *sdk.App + if ac.Otp != "" { + // SDK link tenant with OTP + appInstance, tenant, err = app.LinkTenantWithNewAppInstance(ac.Otp, ac.Name) + if err != nil { + logger.Errorf("Failed to link tenant: %v", err) + os.Exit(-1) + } + ac.Otp = "" + ac.Id = appInstance.ID() + ac.ApiKey = appInstance.ApiKey() + tc.Id = tenant.ID() + tc.Name = tenant.Name() + tc.Token = tenant.ApiToken() + config.store(*configFile) + } else { + // SDK set app instance with existing id and key + appInstance, err = app.SetAppInstance(ac.Id, ac.ApiKey) + if err != nil { + logger.Errorf("Failed to set app: %v", err) + os.Exit(-1) + } + + // SDK set tenant with existing id, name and token + tenant, err = appInstance.SetTenant(tc.Id, tc.Name, tc.Token) + if err != nil { + logger.Errorf("Failed to set tenant to app: %v", err) + os.Exit(-1) + } + } + + // SDK get devices + devices, err := tenant.GetDevices() + if err != nil { + logger.Errorf("Failed to get devices: %v", err) + os.Exit(-1) + } + if len(devices) > 0 { + device := devices[0] + logger.Infof("Selected first device name=%s tenant=%s id=%s ", device.Name(), device.Tenant().Name(), device.ID()) + } else { + logger.Infof("No device found") + } + + // Wait for commands + go commandLoop(appInstance, tenant) + + // Catch termination signal + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + select { + case <-ctx.Done(): + logger.Infof("Terminating...") + case err := <-appInstance.Error: + logger.Errorf("AppInstance error: %v", err) + case err := <-app.Error: + logger.Errorf("App error: %v", err) + } + if err = app.Close(); err != nil { + panic(err) + } +} + +func commandLoop(app *sdk.App, tenant *sdk.Tenant) { + var command string + for { + fmt.Printf("Enter q to getSessions: ") + n, _ := fmt.Scanln(&command) + if n == 1 && command == "q" { + req, _ := http.NewRequest(http.MethodPost, "/pxgrid/session/getSessions", bytes.NewBuffer([]byte("{}"))) + resp, err := selectedDevice.Query(req) + if err == nil { + b, err := io.ReadAll(resp.Body) + if err == nil { + logger.Infof("Status=%s Body=%s", resp.Status, string(b)) + } else { + logger.Errorf("Failed to read response body: %v", err) + } + } else { + logger.Errorf("Failed to invoke %s on %s: %v", req, selectedDevice, err) + } + } + } +} diff --git a/go.mod b/go.mod index dbf8ad3..c4b6a9e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.3.0 github.com/jarcoal/httpmock v1.1.0 github.com/klauspost/compress v1.15.1 // indirect + github.com/rs/xid v1.2.1 github.com/stretchr/testify v1.7.1 golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index c5233da..aa8c9d3 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/internal/pubsub/connection_test.go b/internal/pubsub/connection_test.go index 6a004fc..679a523 100644 --- a/internal/pubsub/connection_test.go +++ b/internal/pubsub/connection_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/cisco-pxgrid/cloud-sdk-go/internal/pubsub/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -57,9 +56,9 @@ func Test_E2E(t *testing.T) { receivedMu.Lock() receivedMsgs[stream]++ receivedMu.Unlock() - assert.NoError(t, e) + require.NoError(t, e) }) - assert.NoError(t, err) + require.NoError(t, err) } // publish to 5 streams simultaneously @@ -75,7 +74,7 @@ func Test_E2E(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) r, err := c.Publish(ctx, stream, nil, payload) cancel() - assert.NoError(t, err) + require.NoError(t, err) t.Logf("Published message: %s to stream: %s, payload: %s", r.ID, stream, payload) } }() @@ -89,15 +88,15 @@ func Test_E2E(t *testing.T) { for i := 0; i < numSubs; i++ { stream := fmt.Sprintf("test-stream-%d", i) receivedMu.Lock() - assert.Equal(t, numMessages, receivedMsgs[stream], "Did not receive all the mssages from stream", i) + require.Equal(t, numMessages, receivedMsgs[stream], "Did not receive all the mssages from stream", i) receivedMu.Unlock() } c.disconnect() - assert.Equal(t, true, c.isDisconnected(), "Connection is still connected") + require.Equal(t, true, c.isDisconnected(), "Connection is still connected") t.Logf("subs table: %#v", c.subs.table) - assert.Zero(t, len(c.subs.table)) + require.Zero(t, len(c.subs.table)) select { case err = <-c.Error: @@ -133,7 +132,7 @@ func Test_ConnectionError(t *testing.T) { }) err = c.connect(context.Background()) - assert.Error(t, err, "Did not receive expected error") + require.Error(t, err, "Did not receive expected error") } func Test_ConnectionMissingAppName(t *testing.T) { @@ -143,9 +142,9 @@ func Test_ConnectionMissingAppName(t *testing.T) { return []byte("xyz"), nil }, }) - assert.Error(t, err, "Did not receive expected error") - assert.Equal(t, "Config must contain GroupID", err.Error()) - assert.Nil(t, c, "Connection is not nil") + require.Error(t, err, "Did not receive expected error") + require.Equal(t, "Config must contain GroupID", err.Error()) + require.Nil(t, c, "Connection is not nil") } func Test_ConnectionMissingDomain(t *testing.T) { @@ -155,9 +154,9 @@ func Test_ConnectionMissingDomain(t *testing.T) { return []byte("xyz"), nil }, }) - assert.Error(t, err, "Did not receive expected error") - assert.Equal(t, "Config must contain Domain", err.Error()) - assert.Nil(t, c, "Connection is not nil") + require.Error(t, err, "Did not receive expected error") + require.Equal(t, "Config must contain Domain", err.Error()) + require.Nil(t, c, "Connection is not nil") } func Test_ConnectionMissingAuthProvider(t *testing.T) { @@ -165,9 +164,9 @@ func Test_ConnectionMissingAuthProvider(t *testing.T) { GroupID: "test-app", Domain: "example.com", // doesn't matter for this case }) - assert.Error(t, err, "Did not receive expected error") - assert.Equal(t, "Config must contain either APIKeyProvider or AuthTokenProvider", err.Error()) - assert.Nil(t, c, "Connection is not nil") + require.Error(t, err, "Did not receive expected error") + require.Equal(t, "Config must contain either APIKeyProvider or AuthTokenProvider", err.Error()) + require.Nil(t, c, "Connection is not nil") } func Test_AuthProviders(t *testing.T) { @@ -284,24 +283,24 @@ func Test_ConsumeError(t *testing.T) { _, err = c.subscribe("test-stream", "", func(e error, _ string, _ map[string]string, _ []byte) { t.Logf("Got error: %v", e) - assert.Error(t, e) + require.Error(t, e) if e == nil { count++ } }) - assert.NoError(t, err) + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() _, err = c.Publish(ctx, "test-stream", nil, []byte("test payload")) - assert.NoError(t, err) + require.NoError(t, err) time.Sleep(2 * time.Second) c.disconnect() - assert.True(t, c.isDisconnected()) - assert.Zero(t, len(c.subs.table)) - assert.Zero(t, count) + require.True(t, c.isDisconnected()) + require.Zero(t, len(c.subs.table)) + require.Zero(t, count) } func Test_PublishError1(t *testing.T) { @@ -317,7 +316,7 @@ func Test_PublishError1(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() _, err = c.Publish(ctx, "test-stream", nil, []byte("test payload")) - assert.Error(t, err, "Did not receive expected error") + require.Error(t, err, "Did not receive expected error") } func Test_PublishError2(t *testing.T) { @@ -355,8 +354,8 @@ func Test_PublishError2(t *testing.T) { c.disconnect() - assert.True(t, c.isDisconnected()) - assert.Zero(t, len(c.subs.table)) + require.True(t, c.isDisconnected()) + require.Zero(t, len(c.subs.table)) } func Test_PublishAsync(t *testing.T) { @@ -388,11 +387,11 @@ func Test_PublishAsync(t *testing.T) { subCh := make(chan []byte) _, err = c.subscribe("test-stream", "", func(e error, id string, _ map[string]string, payload []byte) { - assert.NoError(t, e) + require.NoError(t, e) t.Logf("Received message %s: %s", id, payload) subCh <- payload }) - assert.NoError(t, err) + require.NoError(t, err) ack := make(chan *PublishResult) id, cancel, err := c.PublishAsync("test-stream", nil, []byte("test payload"), ack) @@ -403,7 +402,7 @@ func Test_PublishAsync(t *testing.T) { require.Equal(t, id, r.ID) require.NoError(t, r.Error) case <-time.After(time.Second): - assert.FailNow(t, "Publish timed out") + require.FailNow(t, "Publish timed out") } cancel() @@ -411,13 +410,13 @@ func Test_PublishAsync(t *testing.T) { case payload := <-subCh: require.Equal(t, payload, []byte("test payload")) case <-time.After(time.Second): - assert.FailNow(t, "Consume timed out") + require.FailNow(t, "Consume timed out") } c.disconnect() - assert.True(t, c.isDisconnected()) - assert.Zero(t, len(c.subs.table)) + require.True(t, c.isDisconnected()) + require.Zero(t, len(c.subs.table)) } func Test_PublishAsyncCanceled(t *testing.T) { @@ -449,11 +448,11 @@ func Test_PublishAsyncCanceled(t *testing.T) { count := 0 _, err = c.subscribe("test-stream", "", func(e error, id string, _ map[string]string, payload []byte) { - assert.NoError(t, e) + require.NoError(t, e) t.Logf("Received message %s: %s", id, payload) count++ }) - assert.NoError(t, err) + require.NoError(t, err) ack := make(chan *PublishResult) _, cancel, err := c.PublishAsync("test-stream", nil, []byte("test payload"), ack) @@ -462,15 +461,15 @@ func Test_PublishAsyncCanceled(t *testing.T) { select { case <-ack: - assert.Fail(t, "did not expect ack") + require.Fail(t, "did not expect ack") case <-time.After(time.Second): } c.disconnect() - assert.True(t, c.isDisconnected()) - assert.Zero(t, len(c.subs.table)) - assert.Equal(t, 1, count) + require.True(t, c.isDisconnected()) + require.Zero(t, len(c.subs.table)) + require.Equal(t, 1, count) } func Test_ConsumeTimeout(t *testing.T) { @@ -507,7 +506,7 @@ func Test_ConsumeTimeout(t *testing.T) { func(_ error, _ string, _ map[string]string, _ []byte) { require.Fail(t, "Unexpected message") }) - assert.NoError(t, err) + require.NoError(t, err) select { case <-c.Error: diff --git a/vendor/github.com/rs/xid/.appveyor.yml b/vendor/github.com/rs/xid/.appveyor.yml new file mode 100644 index 0000000..c73bb33 --- /dev/null +++ b/vendor/github.com/rs/xid/.appveyor.yml @@ -0,0 +1,27 @@ +version: 1.0.0.{build} + +platform: x64 + +branches: + only: + - master + +clone_folder: c:\gopath\src\github.com\rs\xid + +environment: + GOPATH: c:\gopath + +install: + - echo %PATH% + - echo %GOPATH% + - set PATH=%GOPATH%\bin;c:\go\bin;%PATH% + - go version + - go env + - go get -t . + +build_script: + - go build + +test_script: + - go test + diff --git a/vendor/github.com/rs/xid/.travis.yml b/vendor/github.com/rs/xid/.travis.yml new file mode 100644 index 0000000..b37da15 --- /dev/null +++ b/vendor/github.com/rs/xid/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: +- "1.9" +- "1.10" +- "master" +matrix: + allow_failures: + - go: "master" diff --git a/vendor/github.com/rs/xid/LICENSE b/vendor/github.com/rs/xid/LICENSE new file mode 100644 index 0000000..47c5e9d --- /dev/null +++ b/vendor/github.com/rs/xid/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Olivier Poitrey + +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. diff --git a/vendor/github.com/rs/xid/README.md b/vendor/github.com/rs/xid/README.md new file mode 100644 index 0000000..1f886fd --- /dev/null +++ b/vendor/github.com/rs/xid/README.md @@ -0,0 +1,112 @@ +# Globally Unique ID Generator + +[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/xid) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/xid/master/LICENSE) [![Build Status](https://travis-ci.org/rs/xid.svg?branch=master)](https://travis-ci.org/rs/xid) [![Coverage](http://gocover.io/_badge/github.com/rs/xid)](http://gocover.io/github.com/rs/xid) + +Package xid is a globally unique id generator library, ready to be used safely directly in your server code. + +Xid is using Mongo Object ID algorithm to generate globally unique ids with a different serialization (base64) to make it shorter when transported as a string: +https://docs.mongodb.org/manual/reference/object-id/ + +- 4-byte value representing the seconds since the Unix epoch, +- 3-byte machine identifier, +- 2-byte process id, and +- 3-byte counter, starting with a random value. + +The binary representation of the id is compatible with Mongo 12 bytes Object IDs. +The string representation is using base32 hex (w/o padding) for better space efficiency +when stored in that form (20 bytes). The hex variant of base32 is used to retain the +sortable property of the id. + +Xid doesn't use base64 because case sensitivity and the 2 non alphanum chars may be an +issue when transported as a string between various systems. Base36 wasn't retained either +because 1/ it's not standard 2/ the resulting size is not predictable (not bit aligned) +and 3/ it would not remain sortable. To validate a base32 `xid`, expect a 20 chars long, +all lowercase sequence of `a` to `v` letters and `0` to `9` numbers (`[0-9a-v]{20}`). + +UUIDs are 16 bytes (128 bits) and 36 chars as string representation. Twitter Snowflake +ids are 8 bytes (64 bits) but require machine/data-center configuration and/or central +generator servers. xid stands in between with 12 bytes (96 bits) and a more compact +URL-safe string representation (20 chars). No configuration or central generator server +is required so it can be used directly in server's code. + +| Name | Binary Size | String Size | Features +|-------------|-------------|----------------|---------------- +| [UUID] | 16 bytes | 36 chars | configuration free, not sortable +| [shortuuid] | 16 bytes | 22 chars | configuration free, not sortable +| [Snowflake] | 8 bytes | up to 20 chars | needs machin/DC configuration, needs central server, sortable +| [MongoID] | 12 bytes | 24 chars | configuration free, sortable +| xid | 12 bytes | 20 chars | configuration free, sortable + +[UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier +[shortuuid]: https://github.com/stochastic-technologies/shortuuid +[Snowflake]: https://blog.twitter.com/2010/announcing-snowflake +[MongoID]: https://docs.mongodb.org/manual/reference/object-id/ + +Features: + +- Size: 12 bytes (96 bits), smaller than UUID, larger than snowflake +- Base32 hex encoded by default (20 chars when transported as printable string, still sortable) +- Non configured, you don't need set a unique machine and/or data center id +- K-ordered +- Embedded time with 1 second precision +- Unicity guaranteed for 16,777,216 (24 bits) unique ids per second and per host/process +- Lock-free (i.e.: unlike UUIDv1 and v2) + +Best used with [zerolog](https://github.com/rs/zerolog)'s +[RequestIDHandler](https://godoc.org/github.com/rs/zerolog/hlog#RequestIDHandler). + +Notes: + +- Xid is dependent on the system time, a monotonic counter and so is not cryptographically secure. If unpredictability of IDs is important, you should not use Xids. It is worth noting that most of the other UUID like implementations are also not cryptographically secure. You shoud use libraries that rely on cryptographically secure sources (like /dev/urandom on unix, crypto/rand in golang), if you want a truly random ID generator. + +References: + +- http://www.slideshare.net/davegardnerisme/unique-id-generation-in-distributed-systems +- https://en.wikipedia.org/wiki/Universally_unique_identifier +- https://blog.twitter.com/2010/announcing-snowflake +- Python port by [Graham Abbott](https://github.com/graham): https://github.com/graham/python_xid +- Scala port by [Egor Kolotaev](https://github.com/kolotaev): https://github.com/kolotaev/ride + +## Install + + go get github.com/rs/xid + +## Usage + +```go +guid := xid.New() + +println(guid.String()) +// Output: 9m4e2mr0ui3e8a215n4g +``` + +Get `xid` embedded info: + +```go +guid.Machine() +guid.Pid() +guid.Time() +guid.Counter() +``` + +## Benchmark + +Benchmark against Go [Maxim Bublis](https://github.com/satori)'s [UUID](https://github.com/satori/go.uuid). + +``` +BenchmarkXID 20000000 91.1 ns/op 32 B/op 1 allocs/op +BenchmarkXID-2 20000000 55.9 ns/op 32 B/op 1 allocs/op +BenchmarkXID-4 50000000 32.3 ns/op 32 B/op 1 allocs/op +BenchmarkUUIDv1 10000000 204 ns/op 48 B/op 1 allocs/op +BenchmarkUUIDv1-2 10000000 160 ns/op 48 B/op 1 allocs/op +BenchmarkUUIDv1-4 10000000 195 ns/op 48 B/op 1 allocs/op +BenchmarkUUIDv4 1000000 1503 ns/op 64 B/op 2 allocs/op +BenchmarkUUIDv4-2 1000000 1427 ns/op 64 B/op 2 allocs/op +BenchmarkUUIDv4-4 1000000 1452 ns/op 64 B/op 2 allocs/op +``` + +Note: UUIDv1 requires a global lock, hence the performence degrading as we add more CPUs. + +## Licenses + +All source code is licensed under the [MIT License](https://raw.github.com/rs/xid/master/LICENSE). diff --git a/vendor/github.com/rs/xid/go.mod b/vendor/github.com/rs/xid/go.mod new file mode 100644 index 0000000..95b8338 --- /dev/null +++ b/vendor/github.com/rs/xid/go.mod @@ -0,0 +1 @@ +module github.com/rs/xid diff --git a/vendor/github.com/rs/xid/hostid_darwin.go b/vendor/github.com/rs/xid/hostid_darwin.go new file mode 100644 index 0000000..08351ff --- /dev/null +++ b/vendor/github.com/rs/xid/hostid_darwin.go @@ -0,0 +1,9 @@ +// +build darwin + +package xid + +import "syscall" + +func readPlatformMachineID() (string, error) { + return syscall.Sysctl("kern.uuid") +} diff --git a/vendor/github.com/rs/xid/hostid_fallback.go b/vendor/github.com/rs/xid/hostid_fallback.go new file mode 100644 index 0000000..7fbd3c0 --- /dev/null +++ b/vendor/github.com/rs/xid/hostid_fallback.go @@ -0,0 +1,9 @@ +// +build !darwin,!linux,!freebsd,!windows + +package xid + +import "errors" + +func readPlatformMachineID() (string, error) { + return "", errors.New("not implemented") +} diff --git a/vendor/github.com/rs/xid/hostid_freebsd.go b/vendor/github.com/rs/xid/hostid_freebsd.go new file mode 100644 index 0000000..be25a03 --- /dev/null +++ b/vendor/github.com/rs/xid/hostid_freebsd.go @@ -0,0 +1,9 @@ +// +build freebsd + +package xid + +import "syscall" + +func readPlatformMachineID() (string, error) { + return syscall.Sysctl("kern.hostuuid") +} diff --git a/vendor/github.com/rs/xid/hostid_linux.go b/vendor/github.com/rs/xid/hostid_linux.go new file mode 100644 index 0000000..7d0c4a9 --- /dev/null +++ b/vendor/github.com/rs/xid/hostid_linux.go @@ -0,0 +1,10 @@ +// +build linux + +package xid + +import "io/ioutil" + +func readPlatformMachineID() (string, error) { + b, err := ioutil.ReadFile("/sys/class/dmi/id/product_uuid") + return string(b), err +} diff --git a/vendor/github.com/rs/xid/hostid_windows.go b/vendor/github.com/rs/xid/hostid_windows.go new file mode 100644 index 0000000..ec2593e --- /dev/null +++ b/vendor/github.com/rs/xid/hostid_windows.go @@ -0,0 +1,38 @@ +// +build windows + +package xid + +import ( + "fmt" + "syscall" + "unsafe" +) + +func readPlatformMachineID() (string, error) { + // source: https://github.com/shirou/gopsutil/blob/master/host/host_syscall.go + var h syscall.Handle + err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, syscall.StringToUTF16Ptr(`SOFTWARE\Microsoft\Cryptography`), 0, syscall.KEY_READ|syscall.KEY_WOW64_64KEY, &h) + if err != nil { + return "", err + } + defer syscall.RegCloseKey(h) + + const syscallRegBufLen = 74 // len(`{`) + len(`abcdefgh-1234-456789012-123345456671` * 2) + len(`}`) // 2 == bytes/UTF16 + const uuidLen = 36 + + var regBuf [syscallRegBufLen]uint16 + bufLen := uint32(syscallRegBufLen) + var valType uint32 + err = syscall.RegQueryValueEx(h, syscall.StringToUTF16Ptr(`MachineGuid`), nil, &valType, (*byte)(unsafe.Pointer(®Buf[0])), &bufLen) + if err != nil { + return "", err + } + + hostID := syscall.UTF16ToString(regBuf[:]) + hostIDLen := len(hostID) + if hostIDLen != uuidLen { + return "", fmt.Errorf("HostID incorrect: %q\n", hostID) + } + + return hostID, nil +} diff --git a/vendor/github.com/rs/xid/id.go b/vendor/github.com/rs/xid/id.go new file mode 100644 index 0000000..466faf2 --- /dev/null +++ b/vendor/github.com/rs/xid/id.go @@ -0,0 +1,365 @@ +// Package xid is a globally unique id generator suited for web scale +// +// Xid is using Mongo Object ID algorithm to generate globally unique ids: +// https://docs.mongodb.org/manual/reference/object-id/ +// +// - 4-byte value representing the seconds since the Unix epoch, +// - 3-byte machine identifier, +// - 2-byte process id, and +// - 3-byte counter, starting with a random value. +// +// The binary representation of the id is compatible with Mongo 12 bytes Object IDs. +// The string representation is using base32 hex (w/o padding) for better space efficiency +// when stored in that form (20 bytes). The hex variant of base32 is used to retain the +// sortable property of the id. +// +// Xid doesn't use base64 because case sensitivity and the 2 non alphanum chars may be an +// issue when transported as a string between various systems. Base36 wasn't retained either +// because 1/ it's not standard 2/ the resulting size is not predictable (not bit aligned) +// and 3/ it would not remain sortable. To validate a base32 `xid`, expect a 20 chars long, +// all lowercase sequence of `a` to `v` letters and `0` to `9` numbers (`[0-9a-v]{20}`). +// +// UUID is 16 bytes (128 bits), snowflake is 8 bytes (64 bits), xid stands in between +// with 12 bytes with a more compact string representation ready for the web and no +// required configuration or central generation server. +// +// Features: +// +// - Size: 12 bytes (96 bits), smaller than UUID, larger than snowflake +// - Base32 hex encoded by default (16 bytes storage when transported as printable string) +// - Non configured, you don't need set a unique machine and/or data center id +// - K-ordered +// - Embedded time with 1 second precision +// - Unicity guaranteed for 16,777,216 (24 bits) unique ids per second and per host/process +// +// Best used with xlog's RequestIDHandler (https://godoc.org/github.com/rs/xlog#RequestIDHandler). +// +// References: +// +// - http://www.slideshare.net/davegardnerisme/unique-id-generation-in-distributed-systems +// - https://en.wikipedia.org/wiki/Universally_unique_identifier +// - https://blog.twitter.com/2010/announcing-snowflake +package xid + +import ( + "bytes" + "crypto/md5" + "crypto/rand" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "hash/crc32" + "io/ioutil" + "os" + "sort" + "sync/atomic" + "time" +) + +// Code inspired from mgo/bson ObjectId + +// ID represents a unique request id +type ID [rawLen]byte + +const ( + encodedLen = 20 // string encoded len + rawLen = 12 // binary raw len + + // encoding stores a custom version of the base32 encoding with lower case + // letters. + encoding = "0123456789abcdefghijklmnopqrstuv" +) + +var ( + // ErrInvalidID is returned when trying to unmarshal an invalid ID + ErrInvalidID = errors.New("xid: invalid ID") + + // objectIDCounter is atomically incremented when generating a new ObjectId + // using NewObjectId() function. It's used as a counter part of an id. + // This id is initialized with a random value. + objectIDCounter = randInt() + + // machineId stores machine id generated once and used in subsequent calls + // to NewObjectId function. + machineID = readMachineID() + + // pid stores the current process id + pid = os.Getpid() + + nilID ID + + // dec is the decoding map for base32 encoding + dec [256]byte +) + +func init() { + for i := 0; i < len(dec); i++ { + dec[i] = 0xFF + } + for i := 0; i < len(encoding); i++ { + dec[encoding[i]] = byte(i) + } + + // If /proc/self/cpuset exists and is not /, we can assume that we are in a + // form of container and use the content of cpuset xor-ed with the PID in + // order get a reasonable machine global unique PID. + b, err := ioutil.ReadFile("/proc/self/cpuset") + if err == nil && len(b) > 1 { + pid ^= int(crc32.ChecksumIEEE(b)) + } +} + +// readMachineId generates machine id and puts it into the machineId global +// variable. If this function fails to get the hostname, it will cause +// a runtime error. +func readMachineID() []byte { + id := make([]byte, 3) + hid, err := readPlatformMachineID() + if err != nil || len(hid) == 0 { + hid, err = os.Hostname() + } + if err == nil && len(hid) != 0 { + hw := md5.New() + hw.Write([]byte(hid)) + copy(id, hw.Sum(nil)) + } else { + // Fallback to rand number if machine id can't be gathered + if _, randErr := rand.Reader.Read(id); randErr != nil { + panic(fmt.Errorf("xid: cannot get hostname nor generate a random number: %v; %v", err, randErr)) + } + } + return id +} + +// randInt generates a random uint32 +func randInt() uint32 { + b := make([]byte, 3) + if _, err := rand.Reader.Read(b); err != nil { + panic(fmt.Errorf("xid: cannot generate random number: %v;", err)) + } + return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2]) +} + +// New generates a globally unique ID +func New() ID { + return NewWithTime(time.Now()) +} + +// NewWithTime generates a globally unique ID with the passed in time +func NewWithTime(t time.Time) ID { + var id ID + // Timestamp, 4 bytes, big endian + binary.BigEndian.PutUint32(id[:], uint32(t.Unix())) + // Machine, first 3 bytes of md5(hostname) + id[4] = machineID[0] + id[5] = machineID[1] + id[6] = machineID[2] + // Pid, 2 bytes, specs don't specify endianness, but we use big endian. + id[7] = byte(pid >> 8) + id[8] = byte(pid) + // Increment, 3 bytes, big endian + i := atomic.AddUint32(&objectIDCounter, 1) + id[9] = byte(i >> 16) + id[10] = byte(i >> 8) + id[11] = byte(i) + return id +} + +// FromString reads an ID from its string representation +func FromString(id string) (ID, error) { + i := &ID{} + err := i.UnmarshalText([]byte(id)) + return *i, err +} + +// String returns a base32 hex lowercased with no padding representation of the id (char set is 0-9, a-v). +func (id ID) String() string { + text := make([]byte, encodedLen) + encode(text, id[:]) + return string(text) +} + +// MarshalText implements encoding/text TextMarshaler interface +func (id ID) MarshalText() ([]byte, error) { + text := make([]byte, encodedLen) + encode(text, id[:]) + return text, nil +} + +// MarshalJSON implements encoding/json Marshaler interface +func (id ID) MarshalJSON() ([]byte, error) { + if id.IsNil() { + return []byte("null"), nil + } + text, err := id.MarshalText() + return []byte(`"` + string(text) + `"`), err +} + +// encode by unrolling the stdlib base32 algorithm + removing all safe checks +func encode(dst, id []byte) { + dst[0] = encoding[id[0]>>3] + dst[1] = encoding[(id[1]>>6)&0x1F|(id[0]<<2)&0x1F] + dst[2] = encoding[(id[1]>>1)&0x1F] + dst[3] = encoding[(id[2]>>4)&0x1F|(id[1]<<4)&0x1F] + dst[4] = encoding[id[3]>>7|(id[2]<<1)&0x1F] + dst[5] = encoding[(id[3]>>2)&0x1F] + dst[6] = encoding[id[4]>>5|(id[3]<<3)&0x1F] + dst[7] = encoding[id[4]&0x1F] + dst[8] = encoding[id[5]>>3] + dst[9] = encoding[(id[6]>>6)&0x1F|(id[5]<<2)&0x1F] + dst[10] = encoding[(id[6]>>1)&0x1F] + dst[11] = encoding[(id[7]>>4)&0x1F|(id[6]<<4)&0x1F] + dst[12] = encoding[id[8]>>7|(id[7]<<1)&0x1F] + dst[13] = encoding[(id[8]>>2)&0x1F] + dst[14] = encoding[(id[9]>>5)|(id[8]<<3)&0x1F] + dst[15] = encoding[id[9]&0x1F] + dst[16] = encoding[id[10]>>3] + dst[17] = encoding[(id[11]>>6)&0x1F|(id[10]<<2)&0x1F] + dst[18] = encoding[(id[11]>>1)&0x1F] + dst[19] = encoding[(id[11]<<4)&0x1F] +} + +// UnmarshalText implements encoding/text TextUnmarshaler interface +func (id *ID) UnmarshalText(text []byte) error { + if len(text) != encodedLen { + return ErrInvalidID + } + for _, c := range text { + if dec[c] == 0xFF { + return ErrInvalidID + } + } + decode(id, text) + return nil +} + +// UnmarshalJSON implements encoding/json Unmarshaler interface +func (id *ID) UnmarshalJSON(b []byte) error { + s := string(b) + if s == "null" { + *id = nilID + return nil + } + return id.UnmarshalText(b[1 : len(b)-1]) +} + +// decode by unrolling the stdlib base32 algorithm + removing all safe checks +func decode(id *ID, src []byte) { + id[0] = dec[src[0]]<<3 | dec[src[1]]>>2 + id[1] = dec[src[1]]<<6 | dec[src[2]]<<1 | dec[src[3]]>>4 + id[2] = dec[src[3]]<<4 | dec[src[4]]>>1 + id[3] = dec[src[4]]<<7 | dec[src[5]]<<2 | dec[src[6]]>>3 + id[4] = dec[src[6]]<<5 | dec[src[7]] + id[5] = dec[src[8]]<<3 | dec[src[9]]>>2 + id[6] = dec[src[9]]<<6 | dec[src[10]]<<1 | dec[src[11]]>>4 + id[7] = dec[src[11]]<<4 | dec[src[12]]>>1 + id[8] = dec[src[12]]<<7 | dec[src[13]]<<2 | dec[src[14]]>>3 + id[9] = dec[src[14]]<<5 | dec[src[15]] + id[10] = dec[src[16]]<<3 | dec[src[17]]>>2 + id[11] = dec[src[17]]<<6 | dec[src[18]]<<1 | dec[src[19]]>>4 +} + +// Time returns the timestamp part of the id. +// It's a runtime error to call this method with an invalid id. +func (id ID) Time() time.Time { + // First 4 bytes of ObjectId is 32-bit big-endian seconds from epoch. + secs := int64(binary.BigEndian.Uint32(id[0:4])) + return time.Unix(secs, 0) +} + +// Machine returns the 3-byte machine id part of the id. +// It's a runtime error to call this method with an invalid id. +func (id ID) Machine() []byte { + return id[4:7] +} + +// Pid returns the process id part of the id. +// It's a runtime error to call this method with an invalid id. +func (id ID) Pid() uint16 { + return binary.BigEndian.Uint16(id[7:9]) +} + +// Counter returns the incrementing value part of the id. +// It's a runtime error to call this method with an invalid id. +func (id ID) Counter() int32 { + b := id[9:12] + // Counter is stored as big-endian 3-byte value + return int32(uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])) +} + +// Value implements the driver.Valuer interface. +func (id ID) Value() (driver.Value, error) { + if id.IsNil() { + return nil, nil + } + b, err := id.MarshalText() + return string(b), err +} + +// Scan implements the sql.Scanner interface. +func (id *ID) Scan(value interface{}) (err error) { + switch val := value.(type) { + case string: + return id.UnmarshalText([]byte(val)) + case []byte: + return id.UnmarshalText(val) + case nil: + *id = nilID + return nil + default: + return fmt.Errorf("xid: scanning unsupported type: %T", value) + } +} + +// IsNil Returns true if this is a "nil" ID +func (id ID) IsNil() bool { + return id == nilID +} + +// NilID returns a zero value for `xid.ID`. +func NilID() ID { + return nilID +} + +// Bytes returns the byte array representation of `ID` +func (id ID) Bytes() []byte { + return id[:] +} + +// FromBytes convert the byte array representation of `ID` back to `ID` +func FromBytes(b []byte) (ID, error) { + var id ID + if len(b) != rawLen { + return id, ErrInvalidID + } + copy(id[:], b) + return id, nil +} + +// Compare returns an integer comparing two IDs. It behaves just like `bytes.Compare`. +// The result will be 0 if two IDs are identical, -1 if current id is less than the other one, +// and 1 if current id is greater than the other. +func (id ID) Compare(other ID) int { + return bytes.Compare(id[:], other[:]) +} + +type sorter []ID + +func (s sorter) Len() int { + return len(s) +} + +func (s sorter) Less(i, j int) bool { + return s[i].Compare(s[j]) < 0 +} + +func (s sorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Sort sorts an array of IDs inplace. +// It works by wrapping `[]ID` and use `sort.Sort`. +func Sort(ids []ID) { + sort.Sort(sorter(ids)) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 30d94e1..d410f40 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -29,6 +29,9 @@ github.com/jarcoal/httpmock/internal github.com/klauspost/compress/flate # github.com/pmezard/go-difflib v1.0.0 github.com/pmezard/go-difflib/difflib +# github.com/rs/xid v1.2.1 +## explicit +github.com/rs/xid # github.com/stretchr/testify v1.7.1 ## explicit github.com/stretchr/testify/assert