Search code examples
javascalaakkaslf4jtyped

Why there is no log output in Scala + Akka Typed App?


I have an issue that I don't have any log output in my program. Please, help me solve the problem and tell me where I did mistake. The app is meant to be a Chat app. It consists UserActor, Main, RoomActor.

Main:

package Chat

import akka.actor.typed.{ActorRef, ActorSystem, Behavior}
import akka.actor.typed.scaladsl.Behaviors

object Main extends App {
    sealed trait MainActor

    def apply(): Behavior[MainActor] = Behaviors.setup { context =>
        val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "ChatSystem")

        val userActor = system.systemActorOf(UserActor(), "userActor")

        val roomActor = system.systemActorOf(RoomActor(), "roomActor")

        userActor ! UserActor.CreateUser("John", "Doe", 25, system.deadLetters)
        val roomName = "General"
        roomActor ! RoomActor.CreateRoom(userActor, roomName)

        userActor ! UserActor.SendMessageToRoom(roomActor, "Hello, everyone!", "John")

        Thread.sleep(5000)
        system.terminate()

        Behaviors.receiveMessage {
            case _ =>
                context.log.info("Unknown message for MainActor")
                Behaviors.same
        }
    }
}

UserActor:

package Chat

import Chat.RoomActor.RoomCommand
import akka.actor.typed.{ActorRef, Behavior}
import akka.actor.typed.scaladsl.Behaviors

import java.util.UUID

object UserActor {
    sealed trait UserCommand
    case class CreateUser(name: String, surname: String, age: Int, userActor: ActorRef[UserCommand]) extends UserCommand
    case class User(name: String, surname: String, age: Int, userId: String, userActor: ActorRef[UserCommand])
    case class SendMessageToRoom(room: ActorRef[RoomCommand], content: String, userName: String) extends UserCommand
    case class LeftRoomNotification(user: ActorRef[UserCommand], roomName: String) extends UserCommand
    case class JoinRoomNotification(user: ActorRef[UserCommand], roomName: String) extends UserCommand
    case class ReceiveMessage(sender: ActorRef[UserCommand], content: String) extends UserCommand

    def apply(): Behavior[UserCommand] = userBehavior(Map.empty)

    private def userBehavior(users: Map[String, User]): Behavior[UserCommand] =
        Behaviors.receive{(context, message) =>
            message match {
                case ReceiveMessage(sender, content) =>
                    context.log.info(s"User ${context.self} received message from $sender: $content")
                    Behaviors.same
                case SendMessageToRoom(room, content, userName) =>
                    users.get(userName) match {
                        case Some(user) =>
                            room ! RoomActor.BroadcastMessage(user.userActor, content)
                        case None =>
                            throw new Exception(s"Unknown User $userName!")
                    }
                    Behaviors.same
                case CreateUser(name, surname, age, userActor) =>
                    val userId = UUID.randomUUID().toString
                    val newUser = User(name, surname, age, userId, userActor)
                    context.log.info(s"User $newUser added in user list")
                    context.log.info(s"Created user: $userId")
                    users + (userId -> newUser)
                    Behaviors.same
                case LeftRoomNotification(user, roomName) =>
                    context.log.info(s"User $user received LeftRoomNotification for room: $roomName")
                    Behaviors.same
                case JoinRoomNotification(user, roomName) =>
                    context.log.info(s"User $user received JoinRoomNotification for room: $roomName")
                    Behaviors.same
                case _ =>
                    context.log.warn("Unknown message for UserActor")
                    Behaviors.same
            }
        }
}

RoomActor:

package Chat

import Chat.UserActor.{JoinRoomNotification, UserCommand}
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior}

import java.time.LocalDateTime
import scala.collection.mutable

object RoomActor {
    sealed trait RoomCommand

    case class CreateRoom(user: ActorRef[UserCommand], roomName: String) extends RoomCommand
    case class LeaveRoom(user: ActorRef[UserCommand], roomName: String) extends RoomCommand
    case class JoinRoom(user: ActorRef[UserCommand], roomName: String) extends RoomCommand
    case class BroadcastMessage(user: ActorRef[UserCommand], content: String) extends RoomCommand
    case class Room(roomName: String, owner: ActorRef[UserCommand], creationDate: LocalDateTime, users: mutable.Set[ActorRef[UserCommand]])

    def apply(): Behavior[RoomCommand] = roomBehavior(Map.empty)

    private def roomBehavior(rooms: Map[String, Room]): Behavior[RoomCommand] =
        Behaviors.receive { (context, message) =>
            message match {
                case CreateRoom(user, roomName) =>
                    val room = Room(roomName, user, LocalDateTime.now(), mutable.Set(user))
                    context.log.info(s"$user created room: $roomName")
                    roomBehavior(rooms + (roomName -> room))
                case LeaveRoom(user, roomName) =>
                    rooms.get(roomName).foreach { room =>
                        room.users -= user
                        if(user == room.owner) {
                            if(room.users.isEmpty) {
                                context.log.info(s"$user was the owner and the last user in room: $roomName. Removing the room.")
                                roomBehavior(rooms - roomName)
                            } else {
                                val newOwner = room.users.head
                                context.log.info(s"$user was the owner. Assigning the new owner: $newOwner")
                                val updatedRoom = room.copy(owner = newOwner)
                                roomBehavior(rooms + (roomName -> updatedRoom))
                            }
                        } else {
                            context.log.info(s"$user left the room: $roomName")
                            for (elem <- room.users) {
                                elem.tell(UserActor.LeftRoomNotification(user, roomName))
                            }
                            Behaviors.same
                        }
                    }
                    context.log.info(s"$user left room: $roomName")
                    Behaviors.same
                case JoinRoom(user, roomName) =>
                    rooms.get(roomName).foreach {room =>
                        room.users += user
                        user ! JoinRoomNotification(user, roomName)
                    }
                    Behaviors.same

                case BroadcastMessage(sender, content) =>
                    rooms.values.foreach{room =>
                        room.users.foreach{user =>
                            if(user != sender) {
                                user ! UserActor.ReceiveMessage(sender, content)
                            }
                        }
                    }
                    context.log.info(s"Broadcast message from $sender: $content")
                    Behaviors.same

                case _ =>
                    throw new IllegalArgumentException("Unknown message for RoomActor")
                    Behaviors.same
            }
        }
}

The output is:

15:24:35: Executing ':Main.main()'...

> Task :compileJava NO-SOURCE

> Task :compileScala
    [Warn] : -target is deprecated: Use -release instead to compile against the correct platform API.
    one warning found

> Task :processResources NO-SOURCE
> Task :classes
> Task :Main.main()

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.5/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 13s
2 actionable tasks: 2 executed
15:24:49: Execution finished ':Main.main()'.

I've added Thread.sleep(5000), but the program still doesn't have any log output. I tried to add logback.xml with root level = "DEBUG" and "info". Also, I tried replace Thread.sleep by Await.result(system.whenTerminated, Duration.Inf)


Solution

  • Because you aren't running anything.

    Behaviors.xx only creates a factory of actors, you have to explicitly let ActorSystem Spawn it. This is an example:

    object Main extends App {
      sealed trait MainActor
    
      // setup your guardian actor here
      val guardianActor: Behavior[Any] = Behaviors.receive { (context, message) =>
        // like java constructor here
        val userActor = context.spawn(UserActor(), "userActor")
        // other...
    
        // react to message
        message match {
          case _ =>
            context.log.info("Unknown message for MainActor")
            Behaviors.same
        }
      }
    
      // start ActorSystem here...
      val system: ActorSystem[Nothing] = ActorSystem(guardianActor, "ChatSystem")
    
    }
    

    One more thing, system.systemActorOf isn't recommended for the user, you need to create from guardian actor, which means the root (user) actor of ActorSystem.

    If you want to create actor from anywhere, use the SpawnProtocol

    Actually, the actor system itself is a root actor of JVM.