farhadi / node-smpp

SMPP client and server implementation in node.js
MIT License
414 stars 177 forks source link

How to handle bind_transrecivier command status 5 #250

Open dotsinspace opened 6 months ago

dotsinspace commented 6 months ago

I have Pipeline ( Just imagine to ends of pipe with source smpp and destination smpp ) where destiantion smpp sometimes drops the connection for some reason and some time when i restart my server i start getting bind transrecivier flag with command status 5. Now how can i make sure to unbind destination smpp before sending bind request.

elhananjair commented 6 months ago

I am exactly facing the same issue, although I don't receive an Already bound response from SMSC, the bind_transiever request doesn't work on my end.

There is session.unbind() function but I don't think that can be executed on smpp client side, instead, it is used by the SMPP server side to clean up sessions.

dotsinspace commented 5 months ago

Mine is working fine now because i have extened it with Proxy and then making sure that server reconnect when ever possible on connect drops.

elhananjair commented 5 months ago

@dotsinspace proxy? how does that help in reconnecting to the SMSC again? Can you share the idea, please? I had a hard time reconnecting when the connection got closed, I couldn't distinguish if it was only an unbind issue or the close of the connection.

dotsinspace commented 5 months ago

Here you go.

/*
 * IMPORTS
 */
import Smpp from './node_modules/smpp' // Npm: SMPP library.
import Redis from 'ioredis' // Npm: Redis library.
import _ from 'underscore' // Npm: Utility library.
import { create as DeePool } from 'deepool' // Npm: Connection pool library.
import { Queue, Worker } from 'bullmq' // Npm: BullMQ library.

/*
 * PACKAGES
 */
import Tag from 'tag'

/*
 * GLOBALS
 */
const _functionName = 'SmppManager -> Session'

/*
 * EXPORTS
 */
export default new Proxy(Smpp, {
  'get': (__target, __name) => {
    /*
     * If the property is a function, return a function that will create a new connection pool.
     * and property name is connect
     */
    if (_.isFunction(__target[__name]) && 'connect' === __name) {
      // Return pooled object.
      return ($options, __cb) => {
        // Local variable.
        let _Session

        // Variable assignment.
        _Session = __target[__name]($options, r => {
          // Send unbind pdu.
          _Session.unbind()

          // Bind given session.
          _Session.bind_transceiver({ 'system_id': $options.username, 'password': $options.password }, j => (_Session.isBounded = 0 === j.command_status))

          // Call the callback.
          __cb(r)
        })

        // Update context.
        _Session.context = $options.context

        // Only proceed if session context is context
        if (_Session && !_Session.context.isContext) return new Error('MISSING__CONTEXT')

        // Create bull queue for given connection.
        _Session.connection = new Redis({ 'connectTimeout': 10000, 'maxRetriesPerRequest': null })
        _Session.id = String.random(32)
        _Session.reconnectAttempts = 0
        _Session.Queue = new Queue(_Session.id, { 'connection': _Session.connection })
        _Session.Worker = new Worker(_Session.id, __job => _Session.isBounded ? _Session.submit_sm(__job.data) : new Error('SESSION_NOT_BOUNDED'), { 'connection': _Session.connection })
        _Session.Reconnect = () => {
          // Increment reconnect attempts
          _Session.reconnectAttempts += 1

          // Unbind previous connection.
          _Session.unbind()

          // Calculate reconnect delay with backoff factor
          const calculatedDelay = ($options.reconnectBackoffFactor ** (_Session.reconnectAttempts - 1)) * $options.initialReconnectDelay
          const reconnectDelay = Math.min(calculatedDelay, 10)

          // Check if max reconnection attempts reached
          if (_Session.reconnectAttempts <= $options.maxReconnectAttempts) {
            // Reconnect socket with a delay.
            const reconnectTimeout = setTimeout(() => {
              // Check if the session is already connected; if yes, no need to reconnect
              if (_Session.closed) {
                // Reconnect session.
                _Session.connect()
                _Session.resume()
              }

              // Clear timeout.
              clearTimeout(reconnectTimeout)

              // Clear the reconnect timeout ID
              _Session.reconnectTimeoutId = void 0
            }, reconnectDelay)

            // Save the reconnect timeout ID
            _Session.reconnectTimeoutId = reconnectTimeout
          } else {
            // Destroy the existing session
            _Session.close(() => _Session.destroy())

            // Clear timeout.
            clearTimeout(_Session.reconnectTimeoutId)

            // Reinitialize the session
            _Session = _Session.connect($options, () => {
            // Make sure to unbind the previous session
              _Session.unbind()

              // Bind given session.
              _Session.bind_transceiver({ 'system_id': $options.username, 'password': $options.password }, j => (_Session.isBounded = 0 === j.command_status))
            })
          }
        }

        // Event handlers.
        _Session.on('enquire_link', __pdu => { _Session.send(__pdu.response()) })
        _Session.on('unbind', () => { _Session.unbind_resp(); _Session.close() })
        _Session.on('pdu', j => 0 === j.command_status ? void 0 : _Session.context.Debug({ 'message': JSON.stringify({ ip: _Session.remoteAddress, ...j }) }, _functionName))
        _Session.on('debug', (e, j, k) => _Session.context.Pubsub.publish(Tag.Smpp.DebuggingIpPort(_Session.remoteAddress, _Session.remotePort), { ..._Session, 'logs': { 'type': e, 'msg': j, 'payload': k } }))
        _Session.socket.on('timeout', () => { _Session.context.Debug({ 'message': `Smpp session with id: ${_Session.remoteAddress} got timeout.` }, _functionName) })
        _Session.socket.on('close', () => { _Session.context.Debug({ 'message': `Smpp session with id: ${_Session.remoteAddress} got closed.` }, _functionName) })
        _Session.socket.on('error', e => { _Session.context.Debug({ 'message': `Smpp session with id: ${_Session.remoteAddress} got error.`, 'error': e }, _functionName); _Session.Reconnect() })

        /*
         * If user hasn't asked for pooling.
         * then return session as is.
         */
        if ($options.noPooling) return _Session

        // Const assignment.
        const _DeePooledTarget_ = DeePool(() => _Session)

        // Increase the pool size.
        _DeePooledTarget_.grow($options.sessionAllowed)

        // Return the pooled object.
        return _DeePooledTarget_
      }
    }

    // Otherwise, return the property.
    return __target[__name]
  }
})

Ability:

  1. Bull MQ for QUEUE
  2. Deepool for pooling
  3. No Connection Drop only Destroy will going to clean and drop the connection.
  4. Auto reconnects on connection drop.
  5. Make sure to use 2 minimum session incase when SMSE takes time to update itself if connection is dropped.
elhananjair commented 5 months ago

@dotsinspace thank you for sharing. It's different from the way I am implementing it, but I will use it as a reference thank you.