All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.linkis.entrance.EntranceWebSocketService.scala Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package org.apache.linkis.entrance

import java.text.SimpleDateFormat
import java.util
import java.util.Date
import java.util.concurrent.TimeUnit

import org.apache.linkis.common.listener.Event
import org.apache.linkis.common.log.LogUtils
import org.apache.linkis.common.utils.Utils
import org.apache.linkis.entrance.conf.EntranceConfiguration
import org.apache.linkis.entrance.event._
import org.apache.linkis.entrance.execute.EntranceJob
import org.apache.linkis.entrance.job.EntranceExecutionJob
import org.apache.linkis.entrance.log.LogReader
import org.apache.linkis.entrance.restful.EntranceRestfulApi
import org.apache.linkis.manager.label.constant.LabelKeyConstant
import org.apache.linkis.manager.label.entity.engine.EngineTypeLabel
import org.apache.linkis.manager.label.utils.LabelUtil
import org.apache.linkis.protocol.constants.TaskConstant
import org.apache.linkis.protocol.engine.JobProgressInfo
import org.apache.linkis.protocol.utils.ZuulEntranceUtils
import org.apache.linkis.rpc.Sender
import org.apache.linkis.scheduler.queue.{Job, SchedulerEventState}
import org.apache.linkis.server._
import org.apache.linkis.server.conf.ServerConfiguration
import org.apache.linkis.server.socket.controller.{ServerEvent, ServerEventService, SocketServerEvent}
import org.apache.commons.lang.StringUtils


class EntranceWebSocketService extends ServerEventService with EntranceEventListener with EntranceLogListener {

  private val jobIdToEventId = new util.HashMap[String, Integer]
  private var entranceServer: EntranceServer = _
  private var entranceRestfulApi:EntranceRestfulApi = _
  private val websocketTagJobID = new util.HashMap[String, String]()
  private val restfulURI = if(ServerConfiguration.BDP_SERVER_RESTFUL_URI.getValue.endsWith("/")) ServerConfiguration.BDP_SERVER_RESTFUL_URI.getValue
  else ServerConfiguration.BDP_SERVER_RESTFUL_URI.getValue + "/"
  private val executePattern = restfulURI + "entrance/execute"
  private val logUrlPattern = (restfulURI + """entrance/(.+)/log""").r
  private val statusUrlPattern = (restfulURI + """entrance/(.+)/status""").r
  private val progressUrlPattern = (restfulURI + """entrance/(.+)/progress""").r
  private val killUrlPattern = (restfulURI + """entrance/(.+)/kill""").r
  private val pauseUrlPattern = (restfulURI + """entrance/(.+)/pause""").r
  private val backgroundUrlPattern = restfulURI + """entrance/backgroundservice"""

  def setEntranceServer(entranceServer: EntranceServer):Unit = this.entranceServer = entranceServer
  def setEntranceRestfulApi(entranceRestfulApi: EntranceRestfulApi):Unit = this.entranceRestfulApi = entranceRestfulApi

  Utils.defaultScheduler.scheduleAtFixedRate(new Runnable {
    override def run(): Unit = jobIdToEventId.keys.map(entranceServer.getJob).foreach {
      case Some(job) => if(job.isCompleted && System.currentTimeMillis - job.getEndTime > 5000) {
        jobIdToEventId synchronized jobIdToEventId.remove(job.getId)
        websocketTagJobID synchronized websocketTagJobID.remove(job.getId)
      }
    }
  }, 30, 60, TimeUnit.SECONDS) //TODO Time interval(时间间隔做成参数)

  Utils.defaultScheduler.scheduleAtFixedRate(new Runnable {
    override def run(): Unit = {
      if(jobIdToEventId.isEmpty) return
      val queues = entranceServer.getEntranceContext.getOrCreateScheduler().getSchedulerContext
        .getOrCreateConsumerManager.listConsumers().map(_.getConsumeQueue).toSet
      queues.foreach { queue =>
        val waitingEvents = queue.getWaitingEvents
        waitingEvents.indices.foreach { index =>
          waitingEvents(index) match {
            case job: Job => if (jobIdToEventId.containsKey(job.getId)) {
              val entranceJob = job.asInstanceOf[EntranceJob]
              val engineTypeLabel = entranceJob.getJobRequest.getLabels.filter(l => l.getLabelKey.equalsIgnoreCase(LabelKeyConstant.ENGINE_TYPE_KEY)).headOption.getOrElse(null)
              if (null == engineTypeLabel) {
                error("Invalid engineTpyeLabel")
                return
              }
              val realID = ZuulEntranceUtils.generateExecID(entranceJob.getJobRequest.getReqId, engineTypeLabel.asInstanceOf[EngineTypeLabel].getEngineType,
                Sender.getThisInstance)
              val taskID = job.asInstanceOf[EntranceJob].getJobRequest.getId
              Utils.tryQuietly(sendMsg(job, "Get waiting size succeed.".data("execID", realID).data("taskID", taskID)
                .data("waitingSize", index).data("websocketTag", websocketTagJobID.get(job.getId)) << restfulURI + s"entrance/$realID/waitingSize"))
            }
          }
        }
      }
    }
  }, 60, 30, TimeUnit.SECONDS) //TODO Time interval(时间间隔做成参数)

  override val serviceName: String = restfulURI + "entrance/"

  override def onEvent(event: ServerEvent): Message = event.getMethod match {
    case `executePattern` => dealExecute(event)
    case logUrlPattern(id) => dealLog(event, id)
    case statusUrlPattern(id) => dealStatus(event, id)
    case progressUrlPattern(id) => dealProgress(event, id)
    case killUrlPattern(id) => dealKill(event, id)
    case pauseUrlPattern(id) => dealPause(event, id)
    case _ => warn("Unresolvable webSocket request, URI is(无法解析的webSocket请求,URI为):" + event.getMethod)
      Message.error("Unresolvable webSocket request, URI is(无法解析的webSocket请求,URI为:" + event.getMethod)
  }




  def dealExecute(event:ServerEvent):Message = {
    val params = event.getData.map{case (k, v) => k -> v.asInstanceOf[Any]}  //TODO Convert to a suitable Map(转换成合适的Map)
    val websocketTag = event.getWebsocketTag
    params.put(TaskConstant.EXECUTE_USER, event.getUser)
    val jobId = entranceServer.execute(params)
    jobIdToEventId synchronized jobIdToEventId.put(jobId, event.getId)
    websocketTagJobID synchronized websocketTagJobID.put(jobId, websocketTag)
    val jobRequest = entranceServer.getJob(jobId).get.asInstanceOf[EntranceJob].getJobRequest
    val taskID = jobRequest.getId
    val job = entranceServer.getJob(jobId).get
    val engineTypeLabel = LabelUtil.getEngineTypeLabel(jobRequest.getLabels)
    val executeApplicationName:String = engineTypeLabel.getEngineType
    val creator:String = LabelUtil.getUserCreatorLabel(jobRequest.getLabels).getCreator
    val execID = ZuulEntranceUtils.generateExecID(jobId, executeApplicationName, Sender.getThisInstance, creator)
    val executeResponseMsg  = Message.ok("Request execution succeeded(请求执行成功)")
    executeResponseMsg.data("execID", execID).data("taskID", taskID).data("websocketTag", websocketTagJobID.get(jobId))
    executeResponseMsg.setMethod(restfulURI + "entrance/execute")
    executeResponseMsg.setStatus(0)
    sendMsg(job, executeResponseMsg)
    val currentTime = System.currentTimeMillis()
    val simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    val time = simpleDateFormat.format(new Date(currentTime))
    entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job,
      LogUtils.generateInfo(s"You have submitted a new job at " + time))
    entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job,
      LogUtils.generateInfo(s"Your job's execution code is (after variable substitution and code check) "))
    entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job,
      "************************************SCRIPT CODE************************************")
    val jobGroups = job.asInstanceOf[EntranceJob].getJobGroups
    jobGroups.foreach(subJobInfo => {
      entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job, subJobInfo.getCode)
    })
    entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job,
      "************************************SCRIPT CODE************************************")
    entranceServer.getEntranceContext.getOrCreateLogManager().onLogUpdate(job,
      LogUtils.generateInfo(s"Your job is accepted,  jobID is ${job.getId} and taskID is $taskID. Please wait it to be scheduled"))
    //val executeApplicationName:String = jobRequest.getExecuteApplicationName
    //val execID = ZuulEntranceUtils.generateExecID(jobId, executeApplicationName, Sender.getThisInstance)
    "The request was executed successfully!".data("execID", execID).data("taskID", taskID).data("websocketTag", websocketTagJobID.get(jobId))
    //executeResponseMsg
  }

  def dealLog(event: ServerEvent, id:String):Message = {
    var retMessage:Message = null
    val realID = ZuulEntranceUtils.parseExecID(id)(3)
    entranceServer.getJob(realID) foreach {
      case entranceExecutionJob: EntranceExecutionJob => {
        info(s"begin to get job $realID log via websocket")
        val logsArr: Array[String] = new Array[String](4)
        entranceExecutionJob.getWebSocketLogReader.foreach(logReader => logReader.readArray(logsArr, 0, 100))
        val logList: util.List[String] = new util.ArrayList[String]()
        logsArr foreach logList.add
        retMessage = Message.ok("Successfully obtained log information(成功获取到日志信息)")
        retMessage.data("execID", id).data("log", logList).data("websocketTag", websocketTagJobID.get(realID))
        retMessage.setStatus(0)
        retMessage.setMethod(restfulURI + "entrance/" + id + "/log")
        info(s"end to get job $realID log via websocket")
        return retMessage
      }
      case _ =>
    }
    retMessage = Message.error(s"Failed to get the log, $id failed to find the corresponding job(获取日志失败,$id 未能找到对应的job)")
    retMessage.setStatus(1)
    retMessage.setMethod(restfulURI + "entrance/" + id + "/log")
    retMessage
  }

  def dealStatus(event:ServerEvent, id:String) : Message = {
    //    val response:Response = entranceRestfulApi.status(id)
    //    Message.responseToMessage(response)
    var retMessage:Message = null

    //val realID:String = if (entranceServer.getJob(id).isDefined) id else ZuulEntranceUtils.parseExecID(id)(2)
    val realID:String = if (!id.contains(":")) id else ZuulEntranceUtils.parseExecID(id)(3)
    entranceServer.getJob(realID) foreach {
      case entranceExecutionJob: EntranceExecutionJob =>
        val engineTypeLabel = LabelUtil.getEngineTypeLabel(entranceExecutionJob.getJobRequest.getLabels)
        val userCreatorLabel = LabelUtil.getUserCreatorLabel(entranceExecutionJob.getJobRequest.getLabels)
        val longExecID = ZuulEntranceUtils.generateExecID(realID,
          engineTypeLabel.getEngineType, Sender.getThisInstance,
          userCreatorLabel.getCreator)
        if (!jobIdToEventId.containsKey(realID) && event != null) jobIdToEventId synchronized jobIdToEventId.put(realID, event.getId)
        val status = entranceExecutionJob.getState

        retMessage = Message.ok("Get the status of the task successfully(获取任务状态成功)")
        val taskID = entranceExecutionJob.getJobRequest.getId
        retMessage.data("execID", longExecID).data("status", status.toString).data("websocketTag", websocketTagJobID.get(realID)).data("taskID", taskID)
        logger.info(s" retMessage: execID is $longExecID, status is ${status.toString}, websocketTag is ${websocketTagJobID.get(realID)}")
        retMessage.setStatus(0)
        retMessage.setMethod(restfulURI + "entrance/" + longExecID + "/status")
        if(SchedulerEventState.isCompleted(status)) {
          websocketTagJobID.remove(realID)
        }
        return retMessage
      case _ =>
    }
    retMessage = Message.error("Get task status failed(获取任务状态失败)")
    retMessage.setStatus(1)
    retMessage.setMethod(restfulURI + "entrance/" + id + "/status")
    retMessage
  }
  def dealProgress(event:ServerEvent, id:String) : Message = {
    //    val response:Response = entranceRestfulApi.progress(id)
    //    Message.responseToMessage(response)
    var retMessage:Message = null
    val realID = ZuulEntranceUtils.parseExecID(id)(3)
    entranceServer.getJob(realID) foreach {
      case entranceExecutionJob: EntranceExecutionJob => {
        val progress = entranceExecutionJob.getProgress
        retMessage = Message.ok("Get the task progress successfully(获取任务进度成功)")
        val taskID = entranceExecutionJob.getJobRequest.getId
        retMessage.data("execID", id).data("progress", progress).data("websocketTag", websocketTagJobID.get(realID)).data("taskID", taskID)
        retMessage.setStatus(0)
        retMessage.setMethod(restfulURI + "entrance/" + id + "/progress")
        return retMessage
      }
      case _ =>
    }
    retMessage = Message.error("Get task progress failed(获取任务进度失败)")
    retMessage.setStatus(1)
    retMessage.setMethod(restfulURI + "entrance/" + id + "/progress")
    retMessage
  }
  def dealPause(event:ServerEvent, id:String) : Message = {
    var retMessage:Message = Message.error("Temporarily does not support the task pause(暂时不支持任务暂停)")
    retMessage.setStatus(1)
    retMessage.setMethod(restfulURI + "entrance/" + id + "/progress")
    retMessage
  }
  def dealKill(event:ServerEvent, id:String) : Message = {
    //    val response:Response = entranceRestfulApi.kill(id)
    //    Message.responseToMessage(response)
    var retMessage:Message = null
    val realID = ZuulEntranceUtils.parseExecID(id)(3)
    entranceServer.getJob(realID) foreach {
      case entranceExecutionJob: EntranceExecutionJob =>
        try {
          entranceExecutionJob.kill()
          retMessage = Message.ok("Kill task succeeded(kill任务成功)")
          retMessage.setMethod(restfulURI + "entrance/" + id + "/kill")
          retMessage.setStatus(0)
          return retMessage
        } catch {
          case e: Exception => retMessage = Message.error("Kill task failed(kill任务失败)", e)
          case t: Throwable => retMessage = Message.error("Kill task failed(kill任务失败)", t)
        }
      case _ =>
    }
    if (retMessage == null){
      retMessage = Message.error("Get task failed(获取任务失败)")
    }
    retMessage.setMethod(restfulURI + "entrance/" + id + "/kill")
    retMessage.setStatus(1)
    retMessage
  }

  /* def dealBackGroundService(event: ServerEvent): Message = {
     val params = event.getData.map{case (k, v) => k -> v.asInstanceOf[Any]}
     val backgroundType = params.get("background").get
     //val backgroundType = "export"
     val backgroundService =  entranceServer.getEntranceContext.getOrCreateBackGroundService.find(f =>backgroundType.equals(f.serviceType))
     if (backgroundService.isEmpty) {
       info("The corresponding background service was not found...(未找到相应的后台服务...)")
       dealExecute(event)
     }else{
       dealExecute(backgroundService.get.operation(event))
     }
   }*/


  private def concatLog(length:Int, log:String, flag:StringBuilder, all:StringBuilder):Unit = {
    if(length == 1){
      flag ++= log ++= "\n"
      all ++= log ++= "\n"
    }else{
      flag ++= log ++= "\n"
      all ++= log ++= "\n"
    }
  }

  /**
   * Push the log message to the front end(将日志的消息推送给前端)
   * @param job required(需要)
   * @param log
   */
  def pushLogToFrontend(job: Job, log: String): Unit = {
    import LogReader._
    if (StringUtils.isBlank(log)) return
    var message:Message = null
    val logs:Array[String] = new Array[String](4)
    val logArr:Array[String] = log.split("\n\n").filter(StringUtils.isNotBlank)
    val info = new StringBuilder
    val warn = new StringBuilder
    val error = new StringBuilder
    val all = new StringBuilder
    val length = logArr.length
    logArr.foreach(singleLog => {
      if (StringUtils.isNotEmpty(singleLog)){
        singleLog match {
          case ERROR_HEADER1() | ERROR_HEADER2() =>
            concatLog(length, singleLog, error, all)
          case WARN_HEADER1() |  WARN_HEADER2() =>
            val arr = EntranceConfiguration.LOG_WARN_EXCLUDE.getValue.split(",").map (word => word.trim)
            var flag = false
            for (keyword <- arr){
              flag = singleLog.contains(keyword) || flag
            }
            if (!flag) {
              val message = singleLog.split("\n")(0)
              concatLog(length, message, warn, all)
            }
          case INFO_HEADER1() | INFO_HEADER2() =>
            val hiveLogSpecial:String = EntranceConfiguration.HIVE_SPECIAL_LOG_INCLUDE.getValue
            val sparkLogSpecial:String = EntranceConfiguration.SPARK_SPECIAL_LOG_INCLUDE.getValue
            val hiveCreateTableLog:String = EntranceConfiguration.HIVE_CREATE_TABLE_LOG.getValue
            if (singleLog.contains(hiveLogSpecial) && singleLog.contains(hiveCreateTableLog)){
              val threadName = EntranceConfiguration.HIVE_THREAD_NAME.getValue
              val printInfo = EntranceConfiguration.HIVE_PRINT_INFO_LOG.getValue
              val start = singleLog.indexOf(threadName)
              val end = singleLog.indexOf(printInfo) + printInfo.length
              if(start > 0 && end > 0) {
                val realLog = singleLog.substring(0, start) + singleLog.substring(end, singleLog.length)
                concatLog(length, realLog, info, all)
              }
            }
            if (singleLog.contains(hiveLogSpecial) && singleLog.contains("map") && singleLog.contains("reduce")){
              val threadName = EntranceConfiguration.HIVE_THREAD_NAME.getValue
              val stageName = EntranceConfiguration.HIVE_STAGE_NAME.getValue
              val start = singleLog.indexOf(threadName)
              val end = singleLog.indexOf(stageName)
              if(start > 0 && end > 0) {
                val realLog = singleLog.substring(0, start) + singleLog.substring(end, singleLog.length)
                concatLog(length, realLog, info, all)
              }
            }else if (singleLog.contains(sparkLogSpecial)){
              val className = EntranceConfiguration.SPARK_PROGRESS_NAME.getValue
              val endFlag = EntranceConfiguration.END_FLAG.getValue
              val start = singleLog.indexOf(className)
              val end = singleLog.indexOf(endFlag) + endFlag.length
              if(start > 0 && end > 0) {
                val realLog = singleLog.substring(0, start) + singleLog.substring(end, singleLog.length)
                concatLog(length, realLog, info, all)
              }
            }else{
              val arr = EntranceConfiguration.LOG_EXCLUDE.getValue.split(",").map (word => word.trim)
              var flag = false
              for (keyword <- arr){
                flag = singleLog.contains(keyword) || flag
              }
              if (!flag) concatLog(length, singleLog, info, all)
            }
          case _ =>
            val arr = EntranceConfiguration.LOG_EXCLUDE.getValue.split(",").map (word => word.trim)
            var flag = false
            for (keyword <- arr){
              flag = singleLog.contains(keyword) || flag
            }
            if (!flag) concatLog(length, singleLog, info, all)
        }
      }
    })
    if (StringUtils.isBlank(info.toString()) &&
      StringUtils.isBlank(warn.toString()) &&
      StringUtils.isBlank(error.toString()) &&
      StringUtils.isBlank(all.toString())) {
      return
    }
    val logList: util.List[String] = new util.ArrayList[String]()
    logList.add(error.toString())
    logList.add(warn.toString())
    logList.add(info.toString())
    logList.add(all.toString())
    message = Message.ok("Return log information(返回日志信息)")
    val jobRequest = job.asInstanceOf[EntranceJob].getJobRequest
    val engineType = LabelUtil.getEngineType(jobRequest.getLabels)
    val creator = LabelUtil.getUserCreator(jobRequest.getLabels)._2
    val executeApplicationName = engineType
    val execID: String = ZuulEntranceUtils.generateExecID(job.getId, executeApplicationName, Sender.getThisInstance, creator)
    message.setMethod(restfulURI + "entrance/" + execID + "/log")
    val taskID = jobRequest.getId
    message.data("execID", execID).data("log", logList).data("websocketTag", websocketTagJobID.get(job.getId)).data("taskID", taskID)
    sendMsg(job, message)
  }

  def pushProgressToFrontend(job: Job, progress: Float, progressInfo: Array[JobProgressInfo]): Unit = {
    val progressInfoMap = progressInfo.map(info => toJavaMap(Map("id" -> info.id, "succeedTasks" -> info.succeedTasks,
      "failedTasks" -> info.failedTasks, "runningTasks" -> info.runningTasks, "totalTasks" -> info.totalTasks)))
    val jobRequest = job.asInstanceOf[EntranceJob].getJobRequest
    val engineType = LabelUtil.getEngineType(jobRequest.getLabels)
    val creator = LabelUtil.getUserCreator(jobRequest.getLabels)._2
    val executeApplicationName = engineType
    job.asInstanceOf[EntranceJob].setProgressInfo(progressInfo)
    val execID: String = ZuulEntranceUtils.generateExecID(job.getId, executeApplicationName, Sender.getThisInstance, creator)
    val message = Message.ok("return the schedule information (返回进度信息!)")
    message.setMethod(restfulURI + "entrance/" + execID + "/progress")
    val taskID = jobRequest.getId
    sendMsg(job, message.data("progress", progress).data("progressInfo", progressInfoMap).data("execID",
      ZuulEntranceUtils.generateExecID(job.getId, executeApplicationName, Sender.getThisInstance, creator)).data("websocketTag", websocketTagJobID.get(job.getId)).data("taskID", taskID))
  }

  private def sendMsg(job: Job, message: Message): Unit = {
    //Determine whether the eventId exists, if it does not exist, it may be because the entity that submitted the job was hanged, and now directly push all instances of the user
    //Enclose the jobID into the Message, send out
    //判断eventId是否存在,如果不存在,则可能是由于之前提交该job的entrance挂掉了,现在就直接推送给该用户的所有实例
    //将jobID封装进Message,send出去
    val eventId = jobIdToEventId.get(job.getId)
    if(eventId == null) job match {
      case entranceJob: EntranceJob => sendMessageToUser(entranceJob.getUser, message)
      case _ =>
    } else sendMessage(eventId, message)
  }

  override def onEvent(event: EntranceEvent): Unit = event match {
    //Process job progress and pass information to the front end(处理Job进度,将信息传递给前端)
    case EntranceJobEvent(jobId) =>
      entranceServer.getJob(jobId).foreach { job =>
        val message = dealStatus(null, jobId)
        sendMsg(job, message)
      }
    case EntranceProgressEvent(job, progress, progressInfo) => pushProgressToFrontend(job, progress, progressInfo)
    case _ =>
  }

  override def onEvent(event: EntranceLogEvent): Unit = event match {
    case EntrancePushLogEvent(job, log) => pushLogToFrontend(job, log)
    case _ =>
  }

  override def onEventError(event: Event, t: Throwable): Unit = event match {
    case e: EntranceEvent => onEventError(e, t)
    case e: SocketServerEvent => super.onEventError(e, t)
    case _ => warn(s"cannot recognize the event type $event.", t)
  }

  override def onEventError(event: EntranceEvent, t: Throwable): Unit = event match {
    case EntranceJobEvent(jobId) => info(s"WebSocket send the new status of Job $jobId to webClient failed!", t)
    case EntranceProgressEvent(job, progress, _) =>
      info(s"Job $job send progress $progress by webSocket to webClient failed!", t)
    case _ => info(s"WebSocket send event $event to webClient failed!", t)
  }

  override def onEventError(event: EntranceLogEvent, t: Throwable): Unit = event match {
    case EntrancePushLogEvent(job, _) => info(s"WebSocket send the new log of Job $job to webClient failed!", t)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy