ducky/subscriptions
ducky/subscriptions/subscription.go
Implement PostgreSQL support, drop subscription IDs. Create a Postgres object that wraps database/sql, so we can attach methods to it and fulfill interfaces. Create a postgres_init.sql script that will create the subscriptions table in a PostgreSQL database. Make our period type fulfill the driver.Valuer and driver.Scanner types, so it can be stored in and retrieved from SQL databases. Create a SubscriptionStats type, and add a method to our subscriptionStore interface that will allow us to retrieve current stats about the Subscriptions it is storing. Deprecated the ID property of our Subscription type, and use the Subscription.UserID property instead as our primary key. Subscriptions should be unique per user and we generally will want to access Subscriptions in the context of the User they belong to, so the UserID is a better primary key. This also means we removed the getSubscriptionByUserID method (and implementations) from our subscriptionStore, as getSubscriptions now fills that role. Implement our getSubscriptionStats method in the memstore. Implement the subscriptionStore interface on our new Postgres type. Run the subscription store tests on our Postgres type, as well, if the PG_TEST_DB environment variable is set. Round all our timestamps in our tests to the nearest millisecond, as Postgres silently truncates all timestamps to the nearest millisecond, and it was causing false test failures. Remove the tests for our getSubscriptionStoreByUser method, as that was removed.
1.1 --- a/subscription.go Thu Jun 11 23:15:01 2015 -0400 1.2 +++ b/subscription.go Sun Jun 14 02:48:08 2015 -0400 1.3 @@ -1,6 +1,7 @@ 1.4 package subscriptions 1.5 1.6 import ( 1.7 + "database/sql/driver" 1.8 "errors" 1.9 "time" 1.10 1.11 @@ -30,16 +31,39 @@ 1.12 // ErrNoSubscriptionID is returned when one or more Subscription IDs 1.13 // are required, but none are provided. 1.14 ErrNoSubscriptionID = errors.New("no Subscription ID provided") 1.15 + // ErrPeriodInvalid is returned when attempting to use a period that 1.16 + // is not a valid period. 1.17 + ErrPeriodInvalid = errors.New("invalid period") 1.18 ) 1.19 1.20 type period string 1.21 1.22 +func (p period) Value() (driver.Value, error) { 1.23 + return string(p), nil 1.24 +} 1.25 + 1.26 +func (p *period) Scan(src interface{}) error { 1.27 + if src == nil { 1.28 + *p = period("") 1.29 + return nil 1.30 + } 1.31 + switch src.(type) { 1.32 + case []byte: 1.33 + *p = period(string(src.([]byte))) 1.34 + return nil 1.35 + case string: 1.36 + *p = period(src.(string)) 1.37 + return nil 1.38 + default: 1.39 + return ErrPeriodInvalid 1.40 + } 1.41 +} 1.42 + 1.43 // Subscription represents the state of a user's payments. It holds 1.44 // metadata about the last time a user was charged, how much a user 1.45 // should be charged, how to charge a user and how much to charge 1.46 // the user. 1.47 type Subscription struct { 1.48 - ID uuid.ID 1.49 UserID uuid.ID 1.50 StripeCustomer string 1.51 Amount int 1.52 @@ -138,6 +162,27 @@ 1.53 return s[i].LastCharged.Before(s[j].LastCharged) 1.54 } 1.55 1.56 +// SubscriptionStats represents a set of statistics about our Subscription 1.57 +// data that will be exposed to the Prometheus scraper. 1.58 +type SubscriptionStats struct { 1.59 + Number int64 1.60 + TotalAmount int64 1.61 + MeanAmount float64 1.62 + // BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service. 1.63 + // Because of this, we can only report stats that will be identical across nodes, e.g. stats 1.64 + // that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666 1.65 + // In the future, we'll need per-node metrics. For now, we'll make do. 1.66 + // 1.67 + // Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set: 1.68 + // "SRV Records are created for named ports that are part of normal or Headless Services. For each named port, 1.69 + // the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local. 1.70 + // For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local. 1.71 + // For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and 1.72 + // contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local 1.73 + // SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment 1.74 + // was omitted." 1.75 +} 1.76 + 1.77 type subscriptionStore interface { 1.78 reset() error 1.79 createSubscription(sub Subscription) error 1.80 @@ -145,5 +190,5 @@ 1.81 deleteSubscription(id uuid.ID) error 1.82 listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error) 1.83 getSubscriptions(ids []uuid.ID) (map[string]Subscription, error) 1.84 - getSubscriptionByUser(id uuid.ID) (Subscription, error) 1.85 + getSubscriptionStats() (SubscriptionStats, error) 1.86 }