go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/coordinator/stream.go (about)

     1  // Copyright 2015 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 coordinator
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"time"
    22  
    23  	logdog "go.chromium.org/luci/logdog/api/endpoints/coordinator/logs/v1"
    24  	"go.chromium.org/luci/logdog/api/logpb"
    25  	"go.chromium.org/luci/logdog/common/types"
    26  )
    27  
    28  // StreamState represents the client-side state of the log stream.
    29  //
    30  // It is a type-promoted version of logdog.LogStreamState.
    31  type StreamState struct {
    32  	// Created is the time, represented as a UTC RFC3339 string, when the log
    33  	// stream was created.
    34  	Created time.Time
    35  	// Updated is the time, represented as a UTC RFC3339 string, when the log
    36  	// stream was last updated.
    37  	Updated time.Time
    38  
    39  	// TerminalIndex is the stream index of the log stream's terminal message. If
    40  	// its value is <0, then the log stream has not terminated yet.
    41  	// In this case, FinishedIndex is the index of that terminal message.
    42  	TerminalIndex types.MessageIndex
    43  
    44  	// Archived is true if the stream is marked as archived.
    45  	Archived bool
    46  	// ArchiveIndexURL is the Google Storage URL where the log stream's index is
    47  	// archived.
    48  	ArchiveIndexURL string
    49  	// ArchiveStreamURL is the Google Storage URL where the log stream's raw
    50  	// stream data is archived. If this is not empty, the log stream is considered
    51  	// archived.
    52  	ArchiveStreamURL string
    53  	// ArchiveDataURL is the Google Storage URL where the log stream's assembled
    54  	// data is archived. If this is not empty, the log stream is considered
    55  	// archived.
    56  	ArchiveDataURL string
    57  
    58  	// Purged indicates the purged state of a log. A log that has been purged is
    59  	// only acknowledged to administrative clients.
    60  	Purged bool
    61  }
    62  
    63  // LogStream is returned metadata about a log stream.
    64  type LogStream struct {
    65  	// Project is the log stream's project.
    66  	Project string
    67  	// Path is the path of the log stream.
    68  	Path types.StreamPath
    69  
    70  	// Desc is the log stream's descriptor.
    71  	//
    72  	// TODO(iannucci): Do not embed proto messages! This should be a pointer, and
    73  	// all existing shallow clones of it should be converted to proto.Clone or
    74  	// similar.
    75  	Desc logpb.LogStreamDescriptor
    76  
    77  	// State is the stream's current state.
    78  	State StreamState
    79  }
    80  
    81  func loadLogStream(proj string, path types.StreamPath, s *logdog.LogStreamState, d *logpb.LogStreamDescriptor) *LogStream {
    82  	ls := LogStream{
    83  		Project: proj,
    84  		Path:    path,
    85  	}
    86  	if d != nil {
    87  		ls.Desc = *d
    88  	}
    89  	if s != nil {
    90  		ls.State = StreamState{
    91  			Created:       s.Created.AsTime(),
    92  			TerminalIndex: types.MessageIndex(s.TerminalIndex),
    93  			Purged:        s.Purged,
    94  		}
    95  
    96  		if a := s.Archive; a != nil {
    97  			ls.State.Archived = true
    98  			ls.State.ArchiveIndexURL = a.IndexUrl
    99  			ls.State.ArchiveStreamURL = a.StreamUrl
   100  			ls.State.ArchiveDataURL = a.DataUrl
   101  		}
   102  	}
   103  	return &ls
   104  }
   105  
   106  // Stream is an interface to Coordinator stream-level commands. It is bound to
   107  // and operates on a single log stream path.
   108  type Stream struct {
   109  	// c is the Coordinator instance that this Stream is bound to.
   110  	c *Client
   111  
   112  	// project is this stream's project.
   113  	project string
   114  	// path is the log stream's prefix.
   115  	path types.StreamPath
   116  }
   117  
   118  // State fetches the LogStreamDescriptor for a given log stream.
   119  func (s *Stream) State(ctx context.Context) (*LogStream, error) {
   120  	req := logdog.GetRequest{
   121  		Project:  string(s.project),
   122  		Path:     string(s.path),
   123  		State:    true,
   124  		LogCount: -1, // Don't fetch any logs.
   125  	}
   126  
   127  	resp, err := s.c.C.Get(ctx, &req)
   128  	if err != nil {
   129  		return nil, normalizeError(err)
   130  	}
   131  
   132  	path := types.StreamPath(req.Path)
   133  	if desc := resp.Desc; desc != nil {
   134  		path = desc.Path()
   135  	}
   136  
   137  	return loadLogStream(resp.Project, path, resp.State, resp.Desc), nil
   138  }
   139  
   140  // Get retrieves log stream entries from the Coordinator. The supplied
   141  // parameters shape which entries are requested and what information is
   142  // returned.
   143  func (s *Stream) Get(ctx context.Context, params ...GetParam) ([]*logpb.LogEntry, error) {
   144  	p := getParamsInst{
   145  		r: logdog.GetRequest{
   146  			Project: string(s.project),
   147  			Path:    string(s.path),
   148  		},
   149  	}
   150  	for _, param := range params {
   151  		param.applyGet(&p)
   152  	}
   153  
   154  	if p.stateP != nil {
   155  		p.r.State = true
   156  	}
   157  
   158  	resp, err := s.c.C.Get(ctx, &p.r)
   159  	if err != nil {
   160  		return nil, normalizeError(err)
   161  	}
   162  	if err := loadStatePointer(p.stateP, resp); err != nil {
   163  		return nil, err
   164  	}
   165  	return resp.Logs, nil
   166  }
   167  
   168  // Tail performs a tail call, returning the last log entry in the stream. If
   169  // stateP is not nil, the stream's state will be requested and loaded into the
   170  // variable.
   171  func (s *Stream) Tail(ctx context.Context, params ...TailParam) (*logpb.LogEntry, error) {
   172  	p := tailParamsInst{
   173  		r: logdog.TailRequest{
   174  			Project: string(s.project),
   175  			Path:    string(s.path),
   176  		},
   177  	}
   178  	for _, param := range params {
   179  		param.applyTail(&p)
   180  	}
   181  
   182  	resp, err := s.c.C.Tail(ctx, &p.r)
   183  	if err != nil {
   184  		return nil, normalizeError(err)
   185  	}
   186  	if err := loadStatePointer(p.stateP, resp); err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	switch len(resp.Logs) {
   191  	case 0:
   192  		return nil, nil
   193  
   194  	case 1:
   195  		le := resp.Logs[0]
   196  		if p.complete {
   197  			if dg := le.GetDatagram(); dg != nil && dg.Partial != nil {
   198  				// This is a partial; datagram. Fetch and assemble the full datagram.
   199  				return s.fetchFullDatagram(ctx, le, true)
   200  			}
   201  		}
   202  		return le, nil
   203  
   204  	default:
   205  		return nil, fmt.Errorf("tail call returned %d logs", len(resp.Logs))
   206  	}
   207  }
   208  
   209  func (s *Stream) fetchFullDatagram(ctx context.Context, le *logpb.LogEntry, fetchIfMid bool) (*logpb.LogEntry, error) {
   210  	// Re-evaluate our partial state.
   211  	dg := le.GetDatagram()
   212  	if dg == nil {
   213  		return nil, fmt.Errorf("entry is not a datagram")
   214  	}
   215  
   216  	p := dg.Partial
   217  	if p == nil {
   218  		// Not partial, return the full message.
   219  		return le, nil
   220  	}
   221  
   222  	if uint64(p.Index) > le.StreamIndex {
   223  		// Something is wrong. The datagram identifies itself as an index in the
   224  		// stream that exceeds the actual number of entries in the stream.
   225  		return nil, fmt.Errorf("malformed partial datagram; index (%d) > stream index (%d)",
   226  			p.Index, le.StreamIndex)
   227  	}
   228  
   229  	if !p.Last {
   230  		// This is the last log entry (b/c we Tail'd), but it is part of a larger
   231  		// datagram. We can't fetch the full datagram since presumably the remainder
   232  		// doesn't exist. Therefore, fetch the previous datagram.
   233  		switch {
   234  		case !fetchIfMid:
   235  			return nil, fmt.Errorf("mid-fragment partial datagram not allowed")
   236  
   237  		case uint64(p.Index) == le.StreamIndex:
   238  			// If we equal the stream index, then we are the first datagram in the
   239  			// stream, so return nil.
   240  			return nil, nil
   241  
   242  		default:
   243  			// Perform a Get on the previous entry in the stream.
   244  			prevIdx := le.StreamIndex - uint64(p.Index) - 1
   245  			logs, err := s.Get(ctx, Index(types.MessageIndex(prevIdx)), LimitCount(1))
   246  			if err != nil {
   247  				return nil, fmt.Errorf("failed to get previous datagram (%d): %s", prevIdx, err)
   248  			}
   249  
   250  			if len(logs) != 1 || logs[0].StreamIndex != prevIdx {
   251  				return nil, fmt.Errorf("previous datagram (%d) not returned", prevIdx)
   252  			}
   253  			if le, err = s.fetchFullDatagram(ctx, logs[0], false); err != nil {
   254  				return nil, fmt.Errorf("failed to recurse to previous datagram (%d): %s", prevIdx, err)
   255  			}
   256  			return le, nil
   257  		}
   258  	}
   259  
   260  	// If this is "Last", but it's also index 0, then it is a partial datagram
   261  	// with one entry. Weird ... but whatever.
   262  	if p.Index == 0 {
   263  		dg.Partial = nil
   264  		return le, nil
   265  	}
   266  
   267  	// Get the intermediate logs.
   268  	startIdx := types.MessageIndex(le.StreamIndex - uint64(p.Index))
   269  	count := int(p.Index)
   270  	logs, err := s.Get(ctx, Index(startIdx), LimitCount(count))
   271  	if err != nil {
   272  		return nil, fmt.Errorf("failed to get intermediate logs [%d .. %d]: %s",
   273  			startIdx, startIdx+types.MessageIndex(count)-1, err)
   274  	}
   275  
   276  	if len(logs) < count {
   277  		return nil, fmt.Errorf("incomplete intermediate logs results (%d < %d)", len(logs), count)
   278  	}
   279  	logs = append(logs[:count], le)
   280  
   281  	// Construct the full datagram.
   282  	aggregate := make([]byte, 0, int(p.Size))
   283  	for i, ple := range logs {
   284  		chunkDg := ple.GetDatagram()
   285  		if chunkDg == nil {
   286  			return nil, fmt.Errorf("intermediate datagram #%d is not a datagram", i)
   287  		}
   288  		chunkP := chunkDg.Partial
   289  		if chunkP == nil {
   290  			return nil, fmt.Errorf("intermediate datagram #%d is not partial", i)
   291  		}
   292  		if int(chunkP.Index) != i {
   293  			return nil, fmt.Errorf("intermediate datagram #%d does not have a contiguous index (%d)", i, chunkP.Index)
   294  		}
   295  		if chunkP.Size != p.Size {
   296  			return nil, fmt.Errorf("inconsistent datagram size (%d != %d)", chunkP.Size, p.Size)
   297  		}
   298  		if uint64(len(aggregate))+uint64(len(chunkDg.Data)) > p.Size {
   299  			return nil, fmt.Errorf("appending chunk data would exceed the declared size (%d > %d)",
   300  				len(aggregate)+len(chunkDg.Data), p.Size)
   301  		}
   302  		aggregate = append(aggregate, chunkDg.Data...)
   303  	}
   304  
   305  	if uint64(len(aggregate)) != p.Size {
   306  		return nil, fmt.Errorf("reassembled datagram length (%d) differs from declared length (%d)", len(aggregate), p.Size)
   307  	}
   308  
   309  	le = logs[0]
   310  	dg = le.GetDatagram()
   311  	dg.Data = aggregate
   312  	dg.Partial = nil
   313  	return le, nil
   314  }
   315  
   316  func loadStatePointer(stateP *LogStream, resp *logdog.GetResponse) error {
   317  	if stateP == nil {
   318  		return nil
   319  	}
   320  
   321  	// The service should always return this when requested, but handle the case
   322  	// where it doesn't for completeness.
   323  	if resp.Desc == nil {
   324  		return errors.New("descriptor was not returned")
   325  	}
   326  
   327  	ls := loadLogStream(resp.Project, resp.Desc.Path(), resp.State, resp.Desc)
   328  	*stateP = *ls
   329  	return nil
   330  }