// Copyright (C) 2024 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. package main import ( "context" "fmt" "io" "log" amqp "github.com/rabbitmq/amqp091-go" "github.com/thejerf/suture/v4" "google.golang.org/protobuf/proto" "github.com/syncthing/syncthing/internal/gen/discosrv" "github.com/syncthing/syncthing/internal/protoutil" "github.com/syncthing/syncthing/lib/protocol" ) type amqpReplicator struct { suture.Service broker string sender *amqpSender receiver *amqpReceiver outbox chan *discosrv.ReplicationRecord } func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator { svc := suture.New("amqpReplicator", suture.Spec{PassThroughPanics: true}) sender := &amqpSender{ broker: broker, clientID: clientID, outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize), } svc.Add(sender) receiver := &amqpReceiver{ broker: broker, clientID: clientID, db: db, } svc.Add(receiver) return &amqpReplicator{ Service: svc, broker: broker, sender: sender, receiver: receiver, outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize), } } func (s *amqpReplicator) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) { s.sender.send(key, ps, seen) } type amqpSender struct { broker string clientID string outbox chan *discosrv.ReplicationRecord } func (s *amqpSender) Serve(ctx context.Context) error { conn, ch, err := amqpChannel(s.broker) if err != nil { return err } defer ch.Close() defer conn.Close() buf := make([]byte, 1024) for { select { case rec := <-s.outbox: size := proto.Size(rec) if len(buf) < size { buf = make([]byte, size) } n, err := protoutil.MarshalTo(buf, rec) if err != nil { replicationSendsTotal.WithLabelValues("error").Inc() return fmt.Errorf("replication marshal: %w", err) } err = ch.PublishWithContext(ctx, "discovery", // exchange "", // routing key false, // mandatory false, // immediate amqp.Publishing{ ContentType: "application/protobuf", Body: buf[:n], AppId: s.clientID, }) if err != nil { replicationSendsTotal.WithLabelValues("error").Inc() return fmt.Errorf("replication publish: %w", err) } replicationSendsTotal.WithLabelValues("success").Inc() case <-ctx.Done(): return nil } } } func (s *amqpSender) String() string { return fmt.Sprintf("amqpSender(%q)", s.broker) } func (s *amqpSender) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) { item := &discosrv.ReplicationRecord{ Key: key[:], Addresses: ps, Seen: seen, } // The send should never block. The inbox is suitably buffered for at // least a few seconds of stalls, which shouldn't happen in practice. select { case s.outbox <- item: default: replicationSendsTotal.WithLabelValues("drop").Inc() } } type amqpReceiver struct { broker string clientID string db database } func (s *amqpReceiver) Serve(ctx context.Context) error { conn, ch, err := amqpChannel(s.broker) if err != nil { return err } defer ch.Close() defer conn.Close() msgs, err := amqpConsume(ch) if err != nil { return err } for { select { case msg, ok := <-msgs: if !ok { return fmt.Errorf("subscription closed: %w", io.EOF) } // ignore messages from ourself if msg.AppId == s.clientID { continue } var rec discosrv.ReplicationRecord if err := proto.Unmarshal(msg.Body, &rec); err != nil { replicationRecvsTotal.WithLabelValues("error").Inc() return fmt.Errorf("replication unmarshal: %w", err) } id, err := protocol.DeviceIDFromBytes(rec.Key) if err != nil { id, err = protocol.DeviceIDFromString(string(rec.Key)) } if err != nil { log.Println("Replication device ID:", err) replicationRecvsTotal.WithLabelValues("error").Inc() continue } if err := s.db.merge(&id, rec.Addresses, rec.Seen); err != nil { return fmt.Errorf("replication database merge: %w", err) } replicationRecvsTotal.WithLabelValues("success").Inc() case <-ctx.Done(): return nil } } } func (s *amqpReceiver) String() string { return fmt.Sprintf("amqpReceiver(%q)", s.broker) } func amqpChannel(dst string) (*amqp.Connection, *amqp.Channel, error) { conn, err := amqp.Dial(dst) if err != nil { return nil, nil, fmt.Errorf("AMQP dial: %w", err) } ch, err := conn.Channel() if err != nil { return nil, nil, fmt.Errorf("AMQP channel: %w", err) } err = ch.ExchangeDeclare( "discovery", // name "fanout", // type false, // durable false, // auto-deleted false, // internal false, // no-wait nil, // arguments ) if err != nil { return nil, nil, fmt.Errorf("AMQP declare exchange: %w", err) } return conn, ch, nil } func amqpConsume(ch *amqp.Channel) (<-chan amqp.Delivery, error) { q, err := ch.QueueDeclare( "", // name false, // durable false, // delete when unused true, // exclusive false, // no-wait nil, // arguments ) if err != nil { return nil, fmt.Errorf("AMQP declare queue: %w", err) } err = ch.QueueBind( q.Name, // queue name "", // routing key "discovery", // exchange false, nil, ) if err != nil { return nil, fmt.Errorf("AMQP bind queue: %w", err) } msgs, err := ch.Consume( q.Name, // queue "", // consumer true, // auto-ack false, // exclusive false, // no-local false, // no-wait nil, // args ) if err != nil { return nil, fmt.Errorf("AMQP consume: %w", err) } return msgs, nil }