~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
341 lines · 9.5 KB Raw History
package requester

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market"
)

const (
	submitAcceptProvisionTimeout = 3 * time.Minute

	submitAcceptReceiptTimeout = 5 * time.Second

	acceptSettleDelay = 2 * time.Second

	acceptRetryCeiling = 2 * time.Second

	acceptRetryFloor = 500 * time.Millisecond

	maxQueuedBids = 100
)

type bidQueue struct {
	mu sync.Mutex

	pending map[string][]BidRecord

	changed chan struct{}
}

func newBidQueue() *bidQueue {
	return &bidQueue{pending: map[string][]BidRecord{}, changed: make(chan struct{})}
}

func (b *bidQueue) put(rfpURI string, bid BidRecord) {
	if rfpURI == "" {
		return
	}
	b.mu.Lock()
	defer b.mu.Unlock()
	list := b.pending[rfpURI]
	if len(list) >= maxQueuedBids {
		list = list[1:]
	}
	b.pending[rfpURI] = append(list, bid)
	close(b.changed)
	b.changed = make(chan struct{})
}

func (b *bidQueue) take(rfpURI string) []BidRecord {
	b.mu.Lock()
	defer b.mu.Unlock()
	bids := b.pending[rfpURI]
	delete(b.pending, rfpURI)
	return bids
}

func (b *bidQueue) arrived() <-chan struct{} {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.changed
}

func (q *Requester) queueBid(rfpURI string, bid BidRecord) {
	q.bids.put(rfpURI, bid)
}

func (q *Requester) noteReceipt(record market.ResolvedRecord) {
	accept, _ := record.Value["accept"].(map[string]any)
	acceptURI := stringOf(accept["uri"])
	if acceptURI == "" || record.URI == "" {
		return
	}
	q.receipts.Put(acceptURI, record)
}

func (q *Requester) inboxReceipt(bidderDID, acceptURI string) (*market.ResolvedRecord, bool) {
	record, ok := q.receipts.Get(acceptURI)
	if !ok || market.ATURIAuthority(record.URI) != bidderDID {
		return nil, false
	}
	return &record, true
}

func (q *Requester) awaitReceipt(ctx context.Context, bidderDID, acceptURI string, until time.Time) (*market.ResolvedRecord, error) {
	waitCtx, cancel := context.WithDeadline(ctx, until)
	defer cancel()
	var found *market.ResolvedRecord
	err := q.receipts.Await(waitCtx, func() bool {
		var ok bool
		found, ok = q.inboxReceipt(bidderDID, acceptURI)
		return ok
	})
	if found != nil {
		q.receipts.Remove(acceptURI)
		return found, nil
	}
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return nil, nil
	}
	return nil, err
}

func (q *Requester) readReceipt(ctx context.Context, bidderDID, acceptURI, acceptCID string) (*market.ResolvedRecord, error) {
	if found, ok := q.inboxReceipt(bidderDID, acceptURI); ok {
		q.receipts.Remove(acceptURI)
		return found, nil
	}
	readCtx, cancel := context.WithTimeout(ctx, submitAcceptReceiptTimeout)
	defer cancel()
	return q.market.FindReceipt(readCtx, bidderDID, acceptURI, acceptCID)
}

func (q *Requester) waitReceiptWithin(ctx context.Context, bidderDID, acceptURI, acceptCID string, timeout time.Duration) (*market.ResolvedRecord, error) {
	until := q.now().Add(timeout)
	receipt, readErr := q.readReceipt(ctx, bidderDID, acceptURI, acceptCID)
	if readErr == nil && receipt != nil {
		return receipt, nil
	}
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}
	receipt, err := q.awaitReceipt(ctx, bidderDID, acceptURI, until)
	if err != nil || receipt != nil {
		return receipt, err
	}
	return nil, readErr
}

func (q *Requester) submitAcceptAndWait(ctx context.Context, bidder Bidder, acceptURI, acceptCID, winnerDID string) (*market.ResolvedRecord, error) {
	provisionTimeout := max(q.cfg.ReceiptTimeout, submitAcceptProvisionTimeout)
	if bidder.Endpoint == "" {
		return q.waitReceiptWithin(ctx, winnerDID, acceptURI, acceptCID, q.cfg.ReceiptTimeout)
	}
	if receipt, err := q.readReceipt(ctx, winnerDID, acceptURI, acceptCID); err == nil && receipt != nil {
		return receipt, nil
	}
	receipt, err := q.awaitReceipt(ctx, winnerDID, acceptURI, q.now().Add(acceptSettleDelay))
	if err != nil || receipt != nil {
		return receipt, err
	}
	if copies := int(q.cfg.Spec.AcceptCopies); copies > 1 {
		copiesCtx, cancel := context.WithTimeout(ctx, provisionTimeout)
		var wg sync.WaitGroup
		for range copies {
			wg.Go(func() {
				_, _ = q.market.SubmitAccept(copiesCtx, bidder, acceptURI, acceptCID)
			})
		}
		wg.Wait()
		cancel()
		receipts := []string{}
		receipt, receiptErr := q.waitReceiptWithin(ctx, winnerDID, acceptURI, acceptCID, provisionTimeout)
		errText := ""
		if receiptErr != nil {
			errText = receiptErr.Error()
		}
		if receiptErr == nil && receipt != nil {
			receipts = append(receipts, receipt.URI)
		}
		q.log.Info("submitAccept_copies", "copies", copies, "receipts", receipts, "err", errText)
		if receiptErr == nil && receipt != nil {
			return receipt, nil
		}
	}
	deadline := q.now().Add(q.cfg.ReceiptTimeout)
	delay := q.cfg.ReceiptPoll
	if delay <= 0 {
		delay = acceptRetryFloor
	}
	for {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		if q.now().After(deadline) {
			return nil, nil
		}
		q.log.Warn("submitAccept_retry", "bidder", bidder.DID, "accept", acceptURI, "acceptCid", acceptCID)
		attemptCtx, cancel := context.WithTimeout(ctx, provisionTimeout)
		reply, err := q.market.SubmitAccept(attemptCtx, bidder, acceptURI, acceptCID)
		cancel()
		if err != nil {
			q.log.Warn("requester_submit_accept_failed", "bidder", bidder.DID, "err", err)
		}
		if err == nil && reply != nil && acceptDeclined(reply.Status) {
			q.log.Warn("requester_submit_accept_declined", "bidder", bidder.DID, "accept", acceptURI, "status", reply.Status, "body", reply.Body)
			if receipt, err := q.readReceipt(ctx, winnerDID, acceptURI, acceptCID); err == nil && receipt != nil {
				return receipt, nil
			}
			return nil, nil
		}
		if receipt, err := q.readReceipt(ctx, winnerDID, acceptURI, acceptCID); err == nil && receipt != nil {
			return receipt, nil
		}
		receipt, err := q.awaitReceipt(ctx, winnerDID, acceptURI, q.now().Add(delay))
		if err != nil || receipt != nil {
			return receipt, err
		}
		delay = min(delay*2, acceptRetryCeiling)
	}
}

func acceptDeclined(status int) bool {
	if status < 400 || status >= 500 {
		return false
	}
	return status != http.StatusRequestTimeout && status != http.StatusTooEarly && status != http.StatusTooManyRequests
}

func (q *Requester) rememberBidders(bidders []Bidder) {
	q.bidderMu.Lock()
	defer q.bidderMu.Unlock()
	for _, bidder := range bidders {
		if strings.TrimSpace(bidder.DID) == "" {
			continue
		}
		if bidder.Endpoint == "" && bidder.Audience == "" {
			continue
		}
		if current, ok := q.bidders[bidder.DID]; ok && current.Endpoint != "" && bidder.Endpoint == "" {
			continue
		}
		q.bidders[bidder.DID] = bidder
	}
}

func (q *Requester) bidderFor(did string) (Bidder, bool) {
	q.bidderMu.Lock()
	defer q.bidderMu.Unlock()
	bidder, ok := q.bidders[did]
	return bidder, ok
}

func (q *Requester) bidderForBid(ctx context.Context, bid BidRecord) (Bidder, bool) {
	if bidder, ok := q.bidderFor(bid.DID); ok && bidder.Endpoint != "" {
		return bidder, true
	}
	raw := strings.TrimSpace(stringOf(bid.Record["submitAccept"]))
	if raw == "" {
		return Bidder{}, false
	}
	endpoint, audience, err := resolveBidderEndpoint(ctx, q.deps.Directory, raw)
	if err != nil || endpoint == "" {
		return Bidder{}, false
	}
	bidder := Bidder{DID: bid.DID, Endpoint: endpoint, Audience: audience}
	q.rememberBidders([]Bidder{bidder})
	return bidder, true
}

func (q *Requester) bidderForDID(ctx context.Context, did string) (Bidder, bool) {
	if bidder, ok := q.bidderFor(did); ok && bidder.Endpoint != "" {
		return bidder, true
	}
	if discovered, err := q.market.Discover(ctx, q.now(), []string{did}); err == nil {
		q.rememberBidders(discovered.Bidders)
		if bidder, ok := q.bidderFor(did); ok && bidder.Endpoint != "" {
			return bidder, true
		}
	}
	parsed, err := syntax.ParseDID(did)
	if err != nil {
		return Bidder{}, false
	}
	identityDoc, err := q.deps.Directory.LookupDID(ctx, parsed)
	if err != nil || identityDoc == nil {
		return Bidder{}, false
	}
	for fragment, service := range identityDoc.Services {
		if service.URL == "" {
			continue
		}
		if fragment != defaultMarketServiceID && !strings.Contains(fragment, "market") {
			continue
		}
		endpoint, audience, err := resolveBidderEndpoint(ctx, q.deps.Directory, service.URL)
		if err != nil || endpoint == "" {
			continue
		}
		bidder := Bidder{DID: did, Endpoint: endpoint, Audience: audience}
		q.rememberBidders([]Bidder{bidder})
		return bidder, true
	}
	return Bidder{}, false
}

func (q *Requester) classifyBid(ctx context.Context, bid *BidRecord) error {
	if bid == nil {
		return errors.New("requester: no bid")
	}
	ref := payloadRef(bid.Record)
	if ref.URI == "" {
		return nil
	}
	resolved, err := q.repo.Resolve(ctx, ref)
	if err != nil {
		return err
	}
	switch market.NSIDFromURI(resolved.URI) {
	case market.BidsFreeNSID:
		bid.Free = true
		bid.Reason = stringOf(resolved.Value["reason"])
	case market.BidsX402NSID:
		bid.Cost = numberValue(resolved.Value["cost"])
	}
	if bid.Free {
		bid.Cost = 0
	}
	return nil
}

func (q *Requester) bidAllowedWithPolicy(ctx context.Context, bid BidRecord, workflow PolicyGate) bool {
	self := q.marketDID
	if operator := strings.TrimSpace(q.cfg.Spec.OperatorDID); operator != "" {
		self = operator
	}
	input := workflow.Input(PolicyInput{
		SelfDid:          self,
		SubjectDid:       bid.DID,
		CounterpartyDid:  bid.DID,
		RootRequesterDid: self,
	})
	if rfp, _ := bid.Record["rfp"].(map[string]any); rfp != nil {
		input.RFP = &PolicyRef{URI: stringOf(rfp["uri"]), CID: stringOf(rfp["cid"])}
	}
	decision := q.policy.Scope(ctx, input)
	if !decision.Allow {
		q.log.Info("requester_bid_policy_denied", "bidder", bid.DID, "reason", decision.Reason)
	}
	return decision.Allow
}