docs: public void init(@NonNull VoidConfiguration voidConfiguration, @NonNull Transport transport,
                      TrainingDriver<? extends TrainingMessage> trainer) {
          /**
           * Basic plan here:
           *      start publishers/listeners/subscribers
           *      determine role of the current instance:
           *          Shard
           *          Backup
           *          Client
           *      shutdown unwanted aeron helpers (according to role)
           *      wait for incoming task queries (according to role
           *
           */
          if (initFinished.get())
              return;
  
          synchronized (this) {
              if (initLocker.compareAndSet(false, true)) {
                  this.trainer = trainer;
                  this.voidConfiguration = voidConfiguration;
  
                  this.transport = transport;
  
                  // first we need to check, if our current IP matches designated shards or backup
                  if (nodeRole == NodeRole.NONE && (voidConfiguration.getForcedRole() == null
                                  || voidConfiguration.getForcedRole() == NodeRole.NONE)) {
                      Pair<NodeRole, String> pair = null;
                      if (voidConfiguration.getShardAddresses().size() == 1
                                      && voidConfiguration.getShardAddresses().get(0).contains("127.0.0.1")) {
                          pair = Pair.create(NodeRole.SHARD, voidConfiguration.getShardAddresses().get(0));
                      } else {
                          pair = getRole(voidConfiguration, getLocalAddresses());
                      }
                      nodeRole = pair.getFirst();
  
                      String ipAndPort = pair.getSecond();
                      String ip = "127.0.0.1";
                      int port = 0;
                      // if we're Shard and have port enforced
                      if (ipAndPort.contains(":")) {
                          String[] split = ipAndPort.split(":");
                          ip = split[0];
                          port = Integer.valueOf(split[1]);
                      } else {
                          ip = ipAndPort;
                          port = voidConfiguration.getUnicastControllerPort();
                      }
  
                      // if we're Shard here, we should define shardIndex
                      if (nodeRole == NodeRole.SHARD && voidConfiguration.getShardAddresses().size() > 1) {
                          short cnt = 0;
                          for (String shard : voidConfiguration.getShardAddresses()) {
                              String lIp = null;
                              if (shard.contains(":")) {
                                  String[] split = ipAndPort.split(":");
                                  lIp = split[0];
                              } else
                                  lIp = shard;
  
                              if (lIp.equals(ip)) {
                                  shardIndex = cnt;
                              }
                              cnt++;
                          }
                      }
  
                      this.transport.init(voidConfiguration, clipboard, nodeRole, ip, port, shardIndex);
  
                  } else {
                      if (nodeRole == NodeRole.NONE)
                          nodeRole = voidConfiguration.getForcedRole();
  
                      // if we're using forced roles here, we'll assume that controllerAddress belongs to this box
                      String localIp = voidConfiguration.getExecutionMode() == ExecutionMode.MANAGED
                                      ? voidConfiguration.getControllerAddress() : "127.0.0.1";
  
                      this.transport.init(voidConfiguration, clipboard, nodeRole, localIp,
                                      voidConfiguration.getUnicastControllerPort(), shardIndex);
                  }
  
  
                  // TODO: we need real ip only if this is a shard *FOR NOW*, but later we'll need it for client as well
  
                  // we launch message processing if we're not in debug mode
                  if (!manualMode.get()) {
                      processingThreads = new Thread[numThreads];
                      processingRunnables = new Runnable[numThreads];
  
                      for (int x = 0; x < numThreads; x++) {
                          processingThreads[x] = new Thread(() -> {
                              runner.set(true);
                              while (runner.get()) {
                                  try {
                                      //VoidMessage message = transport.takeMessage();
  
                                      //                                    if (nodeRole == NodeRole.SHARD)
                                      //                                        log.info("Processing message: {}", message.getClass().getSimpleName());
  
                                      handleMessage(transport.takeMessage());
  
                                  } catch (ND4JIllegalStateException e) {
                                      throw new RuntimeException(e);
                                  } catch (Exception e) {
                                      throw new RuntimeException(e);
                                  }
                              }
                          });
  
                          //executor.submit(processingRunnables[x);
  
                          // TODO: maybe find the way to guarantee affinity in some other way, to make different devices usable as well?
                          Nd4j.getAffinityManager().attachThreadToDevice(processingThreads[x],
                                          Nd4j.getAffinityManager().getDeviceForCurrentThread());
                          processingThreads[x].setDaemon(true);
                          processingThreads[x].setName("VoidParameterServer messages handling thread");
                          processingThreads[x].start();
                      }
                  }
  
  
                  log.info("Launching transport...");
                  transport.launch(Transport.ThreadingModel.DEDICATED_THREADS);
                  trainer.init(this.voidConfiguration, this.transport, storage, clipboard);
  
                  initFinished.set(true);
              }
          }
      }