Rails способ сопоставить двух пользователей в режиме реального времени - PullRequest
0 голосов
/ 04 февраля 2019

Я разрабатываю приложение Rails в качестве бэкэнда для мобильного приложения через JSON API.Я смоделировал почти все, но есть один (основной) процесс, который необходимо разработать, и я не нахожу четкого способа его реализации.

Кроме других функций, приложение должно соответствовать двум пользователям.которые отвечают определенным условиям, в основном географическим.Ради вопроса и для упрощения, скажем, ему нужно сопоставить пользователей, которые находятся близко друг к другу И которые в настоящее время ищут совпадение (это синхронный опыт), например:

  • ПользовательХит "Поиск партнеров" и появляется экран загрузки
  • Пользователь B нажимает "Поиск партнеров", и появляется экран загрузки

Пользователи, скажем, на расстоянии 5 км друг от друга.Опыт должен быть таким:

  • Они оба видят экран загрузки в течение 5 секунд, пока «система» ищет спички поблизости (3 км).Через 5 секунд он расширяет радиус до 6 км и соответствует двум пользователям.Они оба должны перейти к экрану «Найдено совпадение».

Моя главная проблема заключается в том, как смоделировать этот статус «ищет совпадение» в Rails.Я думал о создании таблицы и модели, включая ссылку на пользователя и его позицию.Но тогда я не могу понять, как справиться с «запросом на совпадение», не попадая в ситуацию «ведущий-ведомый».

В принципе, идеальная ситуация - это ситуация, когда оба пользовательских приложения находятся в некотором роде.статус незанятости и серверная часть, в случае совпадения, может уведомить их обоих, но в этом случае процесс в серверной части должен быть не основанным на запросах, а, возможно, рабочим ... Я использую Postgres с PostgisТаким образом, сохранение позиции пользователя возможно, но не уверен, что, возможно, Redis будет лучшим выбором, учитывая количество изменяющихся строк ...

Я знаю, что я довольно расплывчатовопрос, но на самом деле вопрос заключается в том, какой подход выбрать, а не в решении на уровне кода.

Большое вам спасибо!

Ответы [ 2 ]

0 голосов
/ 04 февраля 2019

Sidekiq + WebSockets + psql должны работать нормально.Чтобы избежать ситуации «главный-подчиненный», составление совпадений должно основываться на приглашениях между двумя пользователями, которые ищут совпадений.Решение довольно простое: когда пользователь подключается через веб-сокет к вашему приложению rails, он запускается FindMatchJob.Работа проверяет, не пригласил ли нас какой-либо другой пользователь на расстоянии 5 км и принимает первые приглашения.В противном случае, он приглашает других пользователей в заданном диапазоне и снова настраивается с задержкой в ​​1 секунду.Я добавил некоторый код в качестве доказательства концепции, но он не является пуленепробиваемым с точки зрения проблем параллелизма.Также я упростил расширение диапазона, потому что я ленивый:)

class FindMatchJob < ApplicationJob
  def perform(user, range: 5)
    return unless user.looking_for_a_match?

    invites = find_pending_invites(user, range)
    return accept_invite(invite.first) if invites.any?

    users_in_range = find_users_in_range(user, range)
    users_in_range.each do |other_user|
      Invite.create!(
        inviting: user,
        invited: other_user,
        distance: other_user.distance_from(user)
      )
    end

    self.class.set(wait: 1.second).perform_later(user, range: range + 1)
  end

  private

  def find_pending_invites(user)
    Invite.where('distance <= ?', distance).find_by(invited: user)
  end

  def accept_invite(invite)
    notify_users(invite)
    clear_other_invites(invite)
  end

  def find_users_in_range(user, range)
    # somehow find those users
  end

  def notify_users(invite)
    # Implement logic to notify users about a match via websockets
  end

  def clear_other_invites(invite)
    Invite.where(
      inviting: match.inviting
    ).or(
      invited: match.inviting
    ).or(
      inviting: match.invited
    ).or(
      invited: match.invited
    ).delete_all
  end
end

Еще одна заметка.Вы можете рассмотреть другой технический стек для решения этой проблемы.Rails не являются лучшими с точки зрения параллелизма.Я бы попробовал с GoLang, здесь было бы намного лучше.

0 голосов
/ 04 февраля 2019

Отказ от ответственности : у меня нет опыта использования этой функции.Но я бы сделал что-то вроде следующего.

Допущения:

  • Весь код ниже использует Postgis

  • *Модель 1013 * имеет атрибут t.st_point :geolocation, geographic: true. Ссылка

Возможное решение:

  • Клиентское соединение подключается к бэкэнду Rails через ActionCable (websocket)

    Пример:

    // JS
    // when "Search Partner" is clicked, perform the following:
    
    var currentRadius = 5;
    
    // let XXX.XX and YYY.YY below to be the "current" location
    // Request:
    //   POST /partners_locations.json
    //     { partners_location: { radius: 5, latitude: XXX.XX, longitude: YYY.YY } }
    // Response:
    //   201 Created
    //     { id: 14, user_id: 6, radius: 5, latitude: XXX.XX, longitude: YYY.YY }
    
    // Using the `partners_location` ID above ^
    App.cable.subscriptions.create(
      { channel: 'PartnersLocationsSearchChannel',  id: 14 },
      {
        connected: function() {
          this.search()
          // search every 5 seconds, and incrementally increase the radius
          setInterval(this.search, 5000)
        },
        // this function will be triggered when there is a match
        received: function(data) {
          console.log(data)
          // i.e. will output: { id: 22, user_id: 7, latitude: XXX.XX, longitude: YYY.YY  }
        }
        search: function() {
          this.perform('search', { radius: currentRadius })
          currentRadius += 1
        }
      }
    )
    
  • Внутренняя сторона будет выглядеть примерно так:

    app / controllers /partners_locations_controller.rb:

    class PartnersLocationsController < ApplicationController
    
      def create
        @partners_location = PartnersLocation.new(partners_location_params)
        @partners_location.user = current_user
    
        if @partners_location.save
          render @partners_location, status: :created, location: @partners_location
        else
          render json: @partners_location.errors, status: :unprocessable_entity
        end
      end
    
      private
    
      def partners_location_params
        params.require(:partners_location).permit(:radius, :latitude, :longitude)
      end
    end
    

    app / channel / application_cable / partners_locations_search_channel.rb:

    class PartnersLocationsSearchChannel < ApplicationCable::Channel
      def subscribed
        @current_partners_location = PartnersLocation.find(params[:id])
        stream_for @current_partners_location
      end
    
      def unsubscribed
        @current_partners_location.destroy
      end
    
      def search(data)
        radius = data.fetch('radius')
    
        @current_partners_location.update!(radius: radius)
    
        partners_locations = PartnersLocation.where.not(
          id: @current_partners_location.id
        ).where(
          # TODO: update this `where` to do a Postgis two-circle (via radius) intersection query to get all the `partners_locations` of which their "radiuses" have already been taken accounted for.
          # ^ haven't done this "circle-intersect" before, but probably the following will help: https://gis.stackexchange.com/questions/166685/postgis-intersect-two-circles-each-circle-must-be-built-from-long-lat-degrees?rq=1
        )
    
        partners_locations.each do |partners_location|
          PartnersLocationsSearchChannel.broadcast_to(
            partners_location,
            @current_partners_location.as_json
          )
    
          PartnersLocationsSearchChannel.broadcast_to(
            @current_partners_location,
            partners_location.as_json
          )
        end
      end
    end
    

Приведенный выше код все еще нуждается в настройке:

  • просто обновите любой из приведенных выше кодов, чтобы использовать JSON API вместо

  • обновить клиентсо стороны соответственно (это может быть не JS).то есть вы можете использовать:

  • partners_locations#create все еще необходимо настроить длясохранить latitude и longitude в атрибуте geolocation

  • current_partner_location.as_json и выше, все еще необходимо настроить для возврата latitude и longitude вместо атрибута geolocation

  • def search выше необходимо обновить: условие where Postgis 2-circle-intersect.Я, честно говоря, не знаю, как подойти к этому.Если кто-нибудь, пожалуйста, дайте мне знать.Это самый близкий , который я смог найти в Интернете.

  • Код JS, указанный выше, все еще необходимо настроить, чтобы корректно обрабатывать ошибки в соединении, а также останавливать * 1095.* после успешного сопоставления.

  • Код JS, указанный выше, все еще необходимо настроить, чтобы «отписаться» от PartnersLocationsSearchChannel после закрытия «экрана загрузки» или когда уже найден"матч" или что-то в этом роде (зависит от ваших требований)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...