You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

topic.go 18 kB

4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. // Copyright 2016 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package pubsub
  15. import (
  16. "context"
  17. "errors"
  18. "fmt"
  19. "log"
  20. "runtime"
  21. "strings"
  22. "sync"
  23. "time"
  24. "cloud.google.com/go/iam"
  25. "github.com/golang/protobuf/proto"
  26. gax "github.com/googleapis/gax-go/v2"
  27. "go.opencensus.io/stats"
  28. "go.opencensus.io/tag"
  29. "google.golang.org/api/support/bundler"
  30. pb "google.golang.org/genproto/googleapis/pubsub/v1"
  31. fmpb "google.golang.org/genproto/protobuf/field_mask"
  32. "google.golang.org/grpc"
  33. "google.golang.org/grpc/codes"
  34. "google.golang.org/grpc/status"
  35. )
  36. const (
  37. // MaxPublishRequestCount is the maximum number of messages that can be in
  38. // a single publish request, as defined by the PubSub service.
  39. MaxPublishRequestCount = 1000
  40. // MaxPublishRequestBytes is the maximum size of a single publish request
  41. // in bytes, as defined by the PubSub service.
  42. MaxPublishRequestBytes = 1e7
  43. )
  44. // ErrOversizedMessage indicates that a message's size exceeds MaxPublishRequestBytes.
  45. var ErrOversizedMessage = bundler.ErrOversizedItem
  46. // Topic is a reference to a PubSub topic.
  47. //
  48. // The methods of Topic are safe for use by multiple goroutines.
  49. type Topic struct {
  50. c *Client
  51. // The fully qualified identifier for the topic, in the format "projects/<projid>/topics/<name>"
  52. name string
  53. // Settings for publishing messages. All changes must be made before the
  54. // first call to Publish. The default is DefaultPublishSettings.
  55. PublishSettings PublishSettings
  56. mu sync.RWMutex
  57. stopped bool
  58. bundler *bundler.Bundler
  59. }
  60. // PublishSettings control the bundling of published messages.
  61. type PublishSettings struct {
  62. // Publish a non-empty batch after this delay has passed.
  63. DelayThreshold time.Duration
  64. // Publish a batch when it has this many messages. The maximum is
  65. // MaxPublishRequestCount.
  66. CountThreshold int
  67. // Publish a batch when its size in bytes reaches this value.
  68. ByteThreshold int
  69. // The number of goroutines that invoke the Publish RPC concurrently.
  70. //
  71. // Defaults to a multiple of GOMAXPROCS.
  72. NumGoroutines int
  73. // The maximum time that the client will attempt to publish a bundle of messages.
  74. Timeout time.Duration
  75. // The maximum number of bytes that the Bundler will keep in memory before
  76. // returning ErrOverflow.
  77. //
  78. // Defaults to DefaultPublishSettings.BufferedByteLimit.
  79. BufferedByteLimit int
  80. }
  81. // DefaultPublishSettings holds the default values for topics' PublishSettings.
  82. var DefaultPublishSettings = PublishSettings{
  83. DelayThreshold: 1 * time.Millisecond,
  84. CountThreshold: 100,
  85. ByteThreshold: 1e6,
  86. Timeout: 60 * time.Second,
  87. // By default, limit the bundler to 10 times the max message size. The number 10 is
  88. // chosen as a reasonable amount of messages in the worst case whilst still
  89. // capping the number to a low enough value to not OOM users.
  90. BufferedByteLimit: 10 * MaxPublishRequestBytes,
  91. }
  92. // CreateTopic creates a new topic.
  93. //
  94. // The specified topic ID must start with a letter, and contain only letters
  95. // ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.),
  96. // tildes (~), plus (+) or percent signs (%). It must be between 3 and 255
  97. // characters in length, and must not start with "goog". For more information,
  98. // see: https://cloud.google.com/pubsub/docs/admin#resource_names
  99. //
  100. // If the topic already exists an error will be returned.
  101. func (c *Client) CreateTopic(ctx context.Context, topicID string) (*Topic, error) {
  102. t := c.Topic(topicID)
  103. _, err := c.pubc.CreateTopic(ctx, &pb.Topic{Name: t.name})
  104. if err != nil {
  105. return nil, err
  106. }
  107. return t, nil
  108. }
  109. // CreateTopicWithConfig creates a topic from TopicConfig.
  110. //
  111. // The specified topic ID must start with a letter, and contain only letters
  112. // ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.),
  113. // tildes (~), plus (+) or percent signs (%). It must be between 3 and 255
  114. // characters in length, and must not start with "goog". For more information,
  115. // see: https://cloud.google.com/pubsub/docs/admin#resource_names.
  116. //
  117. // If the topic already exists, an error will be returned.
  118. func (c *Client) CreateTopicWithConfig(ctx context.Context, topicID string, tc *TopicConfig) (*Topic, error) {
  119. t := c.Topic(topicID)
  120. _, err := c.pubc.CreateTopic(ctx, &pb.Topic{
  121. Name: t.name,
  122. Labels: tc.Labels,
  123. MessageStoragePolicy: messageStoragePolicyToProto(&tc.MessageStoragePolicy),
  124. KmsKeyName: tc.KMSKeyName,
  125. })
  126. if err != nil {
  127. return nil, err
  128. }
  129. return t, nil
  130. }
  131. // Topic creates a reference to a topic in the client's project.
  132. //
  133. // If a Topic's Publish method is called, it has background goroutines
  134. // associated with it. Clean them up by calling Topic.Stop.
  135. //
  136. // Avoid creating many Topic instances if you use them to publish.
  137. func (c *Client) Topic(id string) *Topic {
  138. return c.TopicInProject(id, c.projectID)
  139. }
  140. // TopicInProject creates a reference to a topic in the given project.
  141. //
  142. // If a Topic's Publish method is called, it has background goroutines
  143. // associated with it. Clean them up by calling Topic.Stop.
  144. //
  145. // Avoid creating many Topic instances if you use them to publish.
  146. func (c *Client) TopicInProject(id, projectID string) *Topic {
  147. return newTopic(c, fmt.Sprintf("projects/%s/topics/%s", projectID, id))
  148. }
  149. func newTopic(c *Client, name string) *Topic {
  150. return &Topic{
  151. c: c,
  152. name: name,
  153. PublishSettings: DefaultPublishSettings,
  154. }
  155. }
  156. // TopicConfig describes the configuration of a topic.
  157. type TopicConfig struct {
  158. // The set of labels for the topic.
  159. Labels map[string]string
  160. // The topic's message storage policy.
  161. MessageStoragePolicy MessageStoragePolicy
  162. // The name of the Cloud KMS key to be used to protect access to messages
  163. // published to this topic, in the format
  164. // "projects/P/locations/L/keyRings/R/cryptoKeys/K".
  165. KMSKeyName string
  166. }
  167. // TopicConfigToUpdate describes how to update a topic.
  168. type TopicConfigToUpdate struct {
  169. // If non-nil, the current set of labels is completely
  170. // replaced by the new set.
  171. Labels map[string]string
  172. // If non-nil, the existing policy (containing the list of regions)
  173. // is completely replaced by the new policy.
  174. //
  175. // Use the zero value &MessageStoragePolicy{} to reset the topic back to
  176. // using the organization's Resource Location Restriction policy.
  177. //
  178. // If nil, the policy remains unchanged.
  179. //
  180. // This field has beta status. It is not subject to the stability guarantee
  181. // and may change.
  182. MessageStoragePolicy *MessageStoragePolicy
  183. }
  184. func protoToTopicConfig(pbt *pb.Topic) TopicConfig {
  185. return TopicConfig{
  186. Labels: pbt.Labels,
  187. MessageStoragePolicy: protoToMessageStoragePolicy(pbt.MessageStoragePolicy),
  188. KMSKeyName: pbt.KmsKeyName,
  189. }
  190. }
  191. // MessageStoragePolicy constrains how messages published to the topic may be stored. It
  192. // is determined when the topic is created based on the policy configured at
  193. // the project level.
  194. type MessageStoragePolicy struct {
  195. // AllowedPersistenceRegions is the list of GCP regions where messages that are published
  196. // to the topic may be persisted in storage. Messages published by publishers running in
  197. // non-allowed GCP regions (or running outside of GCP altogether) will be
  198. // routed for storage in one of the allowed regions.
  199. //
  200. // If empty, it indicates a misconfiguration at the project or organization level, which
  201. // will result in all Publish operations failing. This field cannot be empty in updates.
  202. //
  203. // If nil, then the policy is not defined on a topic level. When used in updates, it resets
  204. // the regions back to the organization level Resource Location Restriction policy.
  205. //
  206. // For more information, see
  207. // https://cloud.google.com/pubsub/docs/resource-location-restriction#pubsub-storage-locations.
  208. AllowedPersistenceRegions []string
  209. }
  210. func protoToMessageStoragePolicy(msp *pb.MessageStoragePolicy) MessageStoragePolicy {
  211. if msp == nil {
  212. return MessageStoragePolicy{}
  213. }
  214. return MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions}
  215. }
  216. func messageStoragePolicyToProto(msp *MessageStoragePolicy) *pb.MessageStoragePolicy {
  217. if msp == nil || msp.AllowedPersistenceRegions == nil {
  218. return nil
  219. }
  220. return &pb.MessageStoragePolicy{AllowedPersistenceRegions: msp.AllowedPersistenceRegions}
  221. }
  222. // Config returns the TopicConfig for the topic.
  223. func (t *Topic) Config(ctx context.Context) (TopicConfig, error) {
  224. pbt, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name})
  225. if err != nil {
  226. return TopicConfig{}, err
  227. }
  228. return protoToTopicConfig(pbt), nil
  229. }
  230. // Update changes an existing topic according to the fields set in cfg. It returns
  231. // the new TopicConfig.
  232. func (t *Topic) Update(ctx context.Context, cfg TopicConfigToUpdate) (TopicConfig, error) {
  233. req := t.updateRequest(cfg)
  234. if len(req.UpdateMask.Paths) == 0 {
  235. return TopicConfig{}, errors.New("pubsub: UpdateTopic call with nothing to update")
  236. }
  237. rpt, err := t.c.pubc.UpdateTopic(ctx, req)
  238. if err != nil {
  239. return TopicConfig{}, err
  240. }
  241. return protoToTopicConfig(rpt), nil
  242. }
  243. func (t *Topic) updateRequest(cfg TopicConfigToUpdate) *pb.UpdateTopicRequest {
  244. pt := &pb.Topic{Name: t.name}
  245. var paths []string
  246. if cfg.Labels != nil {
  247. pt.Labels = cfg.Labels
  248. paths = append(paths, "labels")
  249. }
  250. if cfg.MessageStoragePolicy != nil {
  251. pt.MessageStoragePolicy = messageStoragePolicyToProto(cfg.MessageStoragePolicy)
  252. paths = append(paths, "message_storage_policy")
  253. }
  254. return &pb.UpdateTopicRequest{
  255. Topic: pt,
  256. UpdateMask: &fmpb.FieldMask{Paths: paths},
  257. }
  258. }
  259. // Topics returns an iterator which returns all of the topics for the client's project.
  260. func (c *Client) Topics(ctx context.Context) *TopicIterator {
  261. it := c.pubc.ListTopics(ctx, &pb.ListTopicsRequest{Project: c.fullyQualifiedProjectName()})
  262. return &TopicIterator{
  263. c: c,
  264. next: func() (string, error) {
  265. topic, err := it.Next()
  266. if err != nil {
  267. return "", err
  268. }
  269. return topic.Name, nil
  270. },
  271. }
  272. }
  273. // TopicIterator is an iterator that returns a series of topics.
  274. type TopicIterator struct {
  275. c *Client
  276. next func() (string, error)
  277. }
  278. // Next returns the next topic. If there are no more topics, iterator.Done will be returned.
  279. func (tps *TopicIterator) Next() (*Topic, error) {
  280. topicName, err := tps.next()
  281. if err != nil {
  282. return nil, err
  283. }
  284. return newTopic(tps.c, topicName), nil
  285. }
  286. // ID returns the unique identifier of the topic within its project.
  287. func (t *Topic) ID() string {
  288. slash := strings.LastIndex(t.name, "/")
  289. if slash == -1 {
  290. // name is not a fully-qualified name.
  291. panic("bad topic name")
  292. }
  293. return t.name[slash+1:]
  294. }
  295. // String returns the printable globally unique name for the topic.
  296. func (t *Topic) String() string {
  297. return t.name
  298. }
  299. // Delete deletes the topic.
  300. func (t *Topic) Delete(ctx context.Context) error {
  301. return t.c.pubc.DeleteTopic(ctx, &pb.DeleteTopicRequest{Topic: t.name})
  302. }
  303. // Exists reports whether the topic exists on the server.
  304. func (t *Topic) Exists(ctx context.Context) (bool, error) {
  305. if t.name == "_deleted-topic_" {
  306. return false, nil
  307. }
  308. _, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name})
  309. if err == nil {
  310. return true, nil
  311. }
  312. if status.Code(err) == codes.NotFound {
  313. return false, nil
  314. }
  315. return false, err
  316. }
  317. // IAM returns the topic's IAM handle.
  318. func (t *Topic) IAM() *iam.Handle {
  319. return iam.InternalNewHandle(t.c.pubc.Connection(), t.name)
  320. }
  321. // Subscriptions returns an iterator which returns the subscriptions for this topic.
  322. //
  323. // Some of the returned subscriptions may belong to a project other than t.
  324. func (t *Topic) Subscriptions(ctx context.Context) *SubscriptionIterator {
  325. it := t.c.pubc.ListTopicSubscriptions(ctx, &pb.ListTopicSubscriptionsRequest{
  326. Topic: t.name,
  327. })
  328. return &SubscriptionIterator{
  329. c: t.c,
  330. next: it.Next,
  331. }
  332. }
  333. var errTopicStopped = errors.New("pubsub: Stop has been called for this topic")
  334. // Publish publishes msg to the topic asynchronously. Messages are batched and
  335. // sent according to the topic's PublishSettings. Publish never blocks.
  336. //
  337. // Publish returns a non-nil PublishResult which will be ready when the
  338. // message has been sent (or has failed to be sent) to the server.
  339. //
  340. // Publish creates goroutines for batching and sending messages. These goroutines
  341. // need to be stopped by calling t.Stop(). Once stopped, future calls to Publish
  342. // will immediately return a PublishResult with an error.
  343. func (t *Topic) Publish(ctx context.Context, msg *Message) *PublishResult {
  344. // TODO(jba): if this turns out to take significant time, try to approximate it.
  345. // Or, convert the messages to protos in Publish, instead of in the service.
  346. msg.size = proto.Size(&pb.PubsubMessage{
  347. Data: msg.Data,
  348. Attributes: msg.Attributes,
  349. })
  350. r := &PublishResult{ready: make(chan struct{})}
  351. t.initBundler()
  352. t.mu.RLock()
  353. defer t.mu.RUnlock()
  354. // TODO(aboulhosn) [from bcmills] consider changing the semantics of bundler to perform this logic so we don't have to do it here
  355. if t.stopped {
  356. r.set("", errTopicStopped)
  357. return r
  358. }
  359. // TODO(jba) [from bcmills] consider using a shared channel per bundle
  360. // (requires Bundler API changes; would reduce allocations)
  361. err := t.bundler.Add(&bundledMessage{msg, r}, msg.size)
  362. if err != nil {
  363. r.set("", err)
  364. }
  365. return r
  366. }
  367. // Stop sends all remaining published messages and stop goroutines created for handling
  368. // publishing. Returns once all outstanding messages have been sent or have
  369. // failed to be sent.
  370. func (t *Topic) Stop() {
  371. t.mu.Lock()
  372. noop := t.stopped || t.bundler == nil
  373. t.stopped = true
  374. t.mu.Unlock()
  375. if noop {
  376. return
  377. }
  378. t.bundler.Flush()
  379. }
  380. // A PublishResult holds the result from a call to Publish.
  381. type PublishResult struct {
  382. ready chan struct{}
  383. serverID string
  384. err error
  385. }
  386. // Ready returns a channel that is closed when the result is ready.
  387. // When the Ready channel is closed, Get is guaranteed not to block.
  388. func (r *PublishResult) Ready() <-chan struct{} { return r.ready }
  389. // Get returns the server-generated message ID and/or error result of a Publish call.
  390. // Get blocks until the Publish call completes or the context is done.
  391. func (r *PublishResult) Get(ctx context.Context) (serverID string, err error) {
  392. // If the result is already ready, return it even if the context is done.
  393. select {
  394. case <-r.Ready():
  395. return r.serverID, r.err
  396. default:
  397. }
  398. select {
  399. case <-ctx.Done():
  400. return "", ctx.Err()
  401. case <-r.Ready():
  402. return r.serverID, r.err
  403. }
  404. }
  405. func (r *PublishResult) set(sid string, err error) {
  406. r.serverID = sid
  407. r.err = err
  408. close(r.ready)
  409. }
  410. type bundledMessage struct {
  411. msg *Message
  412. res *PublishResult
  413. }
  414. func (t *Topic) initBundler() {
  415. t.mu.RLock()
  416. noop := t.stopped || t.bundler != nil
  417. t.mu.RUnlock()
  418. if noop {
  419. return
  420. }
  421. t.mu.Lock()
  422. defer t.mu.Unlock()
  423. // Must re-check, since we released the lock.
  424. if t.stopped || t.bundler != nil {
  425. return
  426. }
  427. timeout := t.PublishSettings.Timeout
  428. t.bundler = bundler.NewBundler(&bundledMessage{}, func(items interface{}) {
  429. // TODO(jba): use a context detached from the one passed to NewClient.
  430. ctx := context.TODO()
  431. if timeout != 0 {
  432. var cancel func()
  433. ctx, cancel = context.WithTimeout(ctx, timeout)
  434. defer cancel()
  435. }
  436. t.publishMessageBundle(ctx, items.([]*bundledMessage))
  437. })
  438. t.bundler.DelayThreshold = t.PublishSettings.DelayThreshold
  439. t.bundler.BundleCountThreshold = t.PublishSettings.CountThreshold
  440. if t.bundler.BundleCountThreshold > MaxPublishRequestCount {
  441. t.bundler.BundleCountThreshold = MaxPublishRequestCount
  442. }
  443. t.bundler.BundleByteThreshold = t.PublishSettings.ByteThreshold
  444. bufferedByteLimit := DefaultPublishSettings.BufferedByteLimit
  445. if t.PublishSettings.BufferedByteLimit > 0 {
  446. bufferedByteLimit = t.PublishSettings.BufferedByteLimit
  447. }
  448. t.bundler.BufferedByteLimit = bufferedByteLimit
  449. t.bundler.BundleByteLimit = MaxPublishRequestBytes
  450. // Unless overridden, allow many goroutines per CPU to call the Publish RPC concurrently.
  451. // The default value was determined via extensive load testing (see the loadtest subdirectory).
  452. if t.PublishSettings.NumGoroutines > 0 {
  453. t.bundler.HandlerLimit = t.PublishSettings.NumGoroutines
  454. } else {
  455. t.bundler.HandlerLimit = 25 * runtime.GOMAXPROCS(0)
  456. }
  457. }
  458. func (t *Topic) publishMessageBundle(ctx context.Context, bms []*bundledMessage) {
  459. ctx, err := tag.New(ctx, tag.Insert(keyStatus, "OK"), tag.Upsert(keyTopic, t.name))
  460. if err != nil {
  461. log.Printf("pubsub: cannot create context with tag in publishMessageBundle: %v", err)
  462. }
  463. pbMsgs := make([]*pb.PubsubMessage, len(bms))
  464. for i, bm := range bms {
  465. pbMsgs[i] = &pb.PubsubMessage{
  466. Data: bm.msg.Data,
  467. Attributes: bm.msg.Attributes,
  468. }
  469. bm.msg = nil // release bm.msg for GC
  470. }
  471. start := time.Now()
  472. res, err := t.c.pubc.Publish(ctx, &pb.PublishRequest{
  473. Topic: t.name,
  474. Messages: pbMsgs,
  475. }, gax.WithGRPCOptions(grpc.MaxCallSendMsgSize(maxSendRecvBytes)))
  476. end := time.Now()
  477. if err != nil {
  478. // Update context with error tag for OpenCensus,
  479. // using same stats.Record() call as success case.
  480. ctx, _ = tag.New(ctx, tag.Upsert(keyStatus, "ERROR"),
  481. tag.Upsert(keyError, err.Error()))
  482. }
  483. stats.Record(ctx,
  484. PublishLatency.M(float64(end.Sub(start)/time.Millisecond)),
  485. PublishedMessages.M(int64(len(bms))))
  486. for i, bm := range bms {
  487. if err != nil {
  488. bm.res.set("", err)
  489. } else {
  490. bm.res.set(res.MessageIds[i], nil)
  491. }
  492. }
  493. }