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  }