go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/butler/callback.go (about) 1 // Copyright 2018 The LUCI Authors. 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 15 package butler 16 17 import ( 18 "github.com/golang/protobuf/proto" 19 "go.chromium.org/luci/logdog/api/logpb" 20 "go.chromium.org/luci/logdog/common/types" 21 ) 22 23 // StreamRegistrationCallback is a callback to invoke when a new stream is 24 // registered with this butler. 25 // 26 // Expects passed *logpb.LogStreamDescriptor reference to be safe to keep, and 27 // should treat it as read-only. 28 // 29 // See streamConfig.callback. 30 type StreamRegistrationCallback func(*logpb.LogStreamDescriptor) StreamChunkCallback 31 32 // StreamChunkCallback is a callback to invoke on a complete LogEntry. 33 // 34 // Called once with nil when this stream has come to an end. 35 // 36 // See streamConfig.callback. 37 type StreamChunkCallback func(*logpb.LogEntry) 38 39 // AddStreamRegistrationCallback adds a new callback to this Butler. 40 // 41 // The callback is called on new streams and returns a chunk callback to attach 42 // to the stream or nil if you don't want to monitor the stream. 43 // 44 // If multiple callbacks are all interested in the same stream, the first one 45 // wins. 46 // 47 // # Wrapping 48 // 49 // If `wrap` is true, the callback is wrapped internally to buffer LogEntries 50 // until they're complete. 51 // 52 // In wrapped callbacks for text and datagram streams, LogEntry .TimeOffset, 53 // and .PrefixIndex will be 0. .StreamIndex and .Sequence WILL NOT correspond 54 // to the values that the logdog service sees. They will, however, be 55 // internally consistent within the stream. 56 // 57 // Wrapped datagram streams never send a partial datagram; If the logdog 58 // server or stream is shut down while we have a partial datagram buffered, 59 // the partially buffered datagram will not be observed by the buffered 60 // callback. 61 // 62 // Wrapping a binary stream is a noop (i.e. your callback will see the exact 63 // same values wrapped and unwrapped). 64 // 65 // When the stream ends (either due to EOF from the user, or when the butler 66 // is stopped), your callback will be invoked exactly once with `nil`. 67 func (b *Butler) AddStreamRegistrationCallback(cb StreamRegistrationCallback, wrap bool) { 68 b.streamRegistrationCallbacksMu.Lock() 69 defer b.streamRegistrationCallbacksMu.Unlock() 70 b.streamRegistrationCallbacks = append(b.streamRegistrationCallbacks, 71 registeredCallback{cb, wrap}) 72 } 73 74 func (b *Butler) maybeAddStreamCallback(d *logpb.LogStreamDescriptor) { 75 b.streamRegistrationCallbacksMu.RLock() 76 defer b.streamRegistrationCallbacksMu.RUnlock() 77 for _, cb := range b.streamRegistrationCallbacks { 78 if ret := cb.cb(proto.Clone(d).(*logpb.LogStreamDescriptor)); ret != nil { 79 if cb.wrap { 80 switch d.StreamType { 81 case logpb.StreamType_TEXT: 82 ret = getWrappedTextCallback(ret) 83 case logpb.StreamType_DATAGRAM: 84 ret = getWrappedDatagramCallback(ret) 85 } 86 } 87 b.streamCallbacksMu.Lock() 88 defer b.streamCallbacksMu.Unlock() 89 b.streamCallbacks[d.Name] = ret 90 return 91 } 92 } 93 } 94 95 func (b *Butler) runCallbacks(bundle *logpb.ButlerLogBundle) { 96 b.streamCallbacksMu.RLock() 97 cbsCopy := make(map[string]StreamChunkCallback, len(b.streamCallbacks)) 98 for k, v := range b.streamCallbacks { 99 cbsCopy[k] = v 100 } 101 b.streamCallbacksMu.RUnlock() 102 103 for _, entry := range bundle.Entries { 104 name := entry.Desc.Name 105 cb := cbsCopy[name] 106 if cb != nil { 107 for _, log := range entry.Logs { 108 cb(log) 109 } 110 } 111 if entry.Terminal { 112 b.finalCallback(name) 113 } 114 } 115 } 116 117 // finalCallback is called from two places. 118 // 119 // Once, from runStreams, in the event that the stream read NO data from the 120 // user. 121 // 122 // The other, from runCallbacks, in the event that the bundler produced the 123 // terminal index for the stream. 124 // 125 // Ideally this would ONLY be used from runCallbacks... however bundler treats 126 // streams-with-data and streams-without-data asymmetrically. 127 // 128 // TODO(iannucci): Make ALL streams appear via bundler, even empty ones. 129 func (b *Butler) finalCallback(name string) { 130 b.streamCallbacksMu.Lock() 131 cb, ok := b.streamCallbacks[name] 132 if ok { 133 delete(b.streamCallbacks, name) 134 } 135 b.streamCallbacksMu.Unlock() 136 if cb != nil { 137 cb(nil) 138 } 139 b.streams.DrainedStream(types.StreamName(name)) 140 }