Jenkins Pipeline NotSerializableException: groovy.json.internal.LazyMap

80

Resuelto : Gracias a la siguiente respuesta de S.Richmond. Necesitaba desarmar todos los mapas almacenados del groovy.json.internal.LazyMaptipo que significaba anular las variables envServersy objectdespués de su uso.

Adicional : las personas que busquen este error podrían estar interesadas en utilizar el paso de canalización de Jenkins en su readJSONlugar; encuentre más información aquí .


Estoy tratando de usar Jenkins Pipeline para tomar la entrada del usuario que se pasa al trabajo como una cadena json. Pipeline luego analiza esto usando el slurper y selecciono la información importante. Luego utilizará esa información para ejecutar 1 trabajo varias veces en paralelo con diferentes parámetros de trabajo.

Hasta que agregue el código debajo, "## Error when below here is added"el script funcionará bien. Incluso el código debajo de ese punto se ejecutará por sí solo. Pero cuando se combinan, aparece el siguiente error.

Debo señalar que se llama al trabajo activado y se ejecuta con éxito, pero se produce el siguiente error y falla el trabajo principal. Debido a esto, el trabajo principal no espera la devolución del trabajo activado. Yo podría tratar / catch alrededor del build job:embargo quiero que el trabajo principal que esperar a que el trabajo disparado a fin.

¿Alguien puede ayudar aquí? Si necesita más información, hágamelo saber.

Salud

def slurpJSON() {
return new groovy.json.JsonSlurper().parseText(BUILD_CHOICES);
}

node {
  stage 'Prepare';
  echo 'Loading choices as build properties';
  def object = slurpJSON();

  def serverChoices = [];
  def serverChoicesStr = '';

  for (env in object) {
     envName = env.name;
     envServers = env.servers;

     for (server in envServers) {
        if (server.Select) {
            serverChoicesStr += server.Server;
            serverChoicesStr += ',';
        }
     }
  }
  serverChoicesStr = serverChoicesStr[0..-2];

  println("Server choices: " + serverChoicesStr);

  ## Error when below here is added

  stage 'Jobs'
  build job: 'Dummy Start App', parameters: [[$class: 'StringParameterValue', name: 'SERVER_NAME', value: 'TestServer'], [$class: 'StringParameterValue', name: 'SERVER_DOMAIN', value: 'domain.uk'], [$class: 'StringParameterValue', name: 'APP', value: 'application1']]

}

Error:

java.io.NotSerializableException: groovy.json.internal.LazyMap
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:860)
    at org.jboss.marshalling.river.RiverMarshaller.doWriteObject(RiverMarshaller.java:569)
    at org.jboss.marshalling.river.BlockMarshaller.doWriteObject(BlockMarshaller.java:65)
    at org.jboss.marshalling.river.BlockMarshaller.writeObject(BlockMarshaller.java:56)
    at org.jboss.marshalling.MarshallerObjectOutputStream.writeObjectOverride(MarshallerObjectOutputStream.java:50)
    at org.jboss.marshalling.river.RiverObjectOutputStream.writeObjectOverride(RiverObjectOutputStream.java:179)
    at java.io.ObjectOutputStream.writeObject(Unknown Source)
    at java.util.LinkedHashMap.internalWriteEntries(Unknown Source)
    at java.util.HashMap.writeObject(Unknown Source)
...
...
Caused by: an exception which occurred:
    in field delegate
    in field closures
    in object org.jenkinsci.plugins.workflow.cps.CpsThreadGroup@5288c
Sunvic
fuente
Me encontré con esto yo mismo. ¿Has hecho más progresos todavía?
S.Richmond

Respuestas:

71

Me encontré con esto yo mismo hoy y, a través de un poco de fuerza bruta, he descubierto cómo resolverlo y potencialmente por qué.

Probablemente sea mejor comenzar con el por qué:

Jenkins tiene un paradigma en el que todos los trabajos pueden interrumpirse, pausarse y reanudarse mediante el reinicio del servidor. Para lograr esto, la canalización y sus datos deben ser completamente serializables; es decir, debe poder guardar el estado de todo. Del mismo modo, debe poder serializar el estado de las variables globales entre los nodos y los subtrabajos en la compilación, que es lo que creo que nos está sucediendo a usted y a mí y por qué solo ocurre si agrega ese paso de compilación adicional.

Por alguna razón, los JSONObject no se pueden serializar de forma predeterminada. No soy un desarrollador de Java, así que lamentablemente no puedo decir mucho más sobre el tema. Hay muchas respuestas sobre cómo se puede solucionar esto correctamente, aunque no sé qué tan aplicables son a Groovy y Jenkins. Ver esta publicación para obtener más información.

Cómo lo arreglas:

Si sabe cómo, posiblemente pueda hacer que JSONObject sea serializable de alguna manera. De lo contrario, puede resolverlo asegurándose de que ninguna variable global sea de ese tipo.

Intente desarmar su objectvar o envolverlo en un método para que su alcance no sea global de nodo.

S.Richmond
fuente
2
Gracias, esa es la pista que necesitaba para resolver esto. Si bien ya había probado su sugerencia, me hizo mirar nuevamente y no había considerado que estaba almacenando partes del mapa en otras variables, estas estaban causando los errores. Así que necesitaba desarmarlos también. Enmendaré mi pregunta para incluir los cambios correctos en el código. Saludos
Sunvic
1
Esto se ve ~ 8 veces al día. ¿Les importaría proporcionar un ejemplo más detallado de cómo implementar esta solución?
Jordan Stefanelli
1
No existe una solución sencilla, ya que depende de lo que hayas hecho. La información proporcionada aquí, así como la solución que @Sunvic agregó en la parte superior de su publicación, es suficiente para llevarlo a uno a una solución para su propio código.
S.Richmond
1
La solución a continuación, usando JsonSlurperClassic solucionó exactamente el mismo problema que tuve, probablemente debería ser la opción aprobada aquí. Esta respuesta tiene méritos, pero no es la solución adecuada para este problema en particular.
Cuarzo
@JordanStefanelli Publiqué el código de mi solución. Vea mi respuesta a continuación
Nils El-Himoud
127

Úselo en su JsonSlurperClassiclugar.

Dado que Groovy 2.3 ( nota: Jenkins 2.7.1 usa Groovy 2.4.7 ) JsonSlurperdevuelve en LazyMaplugar de HashMap. Esto hace que la nueva implementación de JsonSlurper no sea segura para subprocesos y no sea serializable. Esto lo hace inutilizable fuera de las funciones de @NonDSL en los scripts DSL de canalización.

Sin embargo, puede recurrir a lo groovy.json.JsonSlurperClassicque admite el comportamiento anterior y podría usarse de manera segura dentro de los scripts de canalización.

Ejemplo

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

node('master') {
    def config =  jsonParse(readFile("config.json"))

    def db = config["database"]["address"]
    ...
}    

PD. Aún tendrá que aprobarlo JsonSlurperClassicantes de que se pueda llamar.

luka5z
fuente
2
¿Podría decirme cómo aprobar JsonSlurperClassic?
mybecks
7
El administrador de Jenkins deberá navegar hasta Administrar Jenkins »Aprobación de secuencia de comandos en proceso.
luka5z
Desafortunadamente, solo obtengohudson.remoting.ProxyException: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed: Script1.groovy: 24: unable to resolve class groovy.json.JsonSlurperClassic
dvtoever
13
JsonSluperClassic .. Este nombre dice mucho sobre el estado actual del desarrollo de software
Marcos Brigante
1
Muchas gracias por esta explicación detallada. Me ahorraste mucho tiempo. Esta solución funciona a la perfección en mi tubería de Jenkins.
Sathish Prakasam
16

EDITAR: Como señaló @Sunvic en los comentarios, la siguiente solución no funciona como está para JSON Arrays.

Lidé con esto usando JsonSlurpery luego creando uno nuevo a HashMappartir de los resultados perezosos. HashMapes Serializable.

Creo que esto requiere tanto de la lista blanca new HashMap(Map)y la JsonSlurper.

@NonCPS
def parseJsonText(String jsonText) {
  final slurper = new JsonSlurper()
  return new HashMap<>(slurper.parseText(jsonText))
}

En general, recomendaría usar el complemento Pipeline Utility Steps , ya que tiene un readJSONpaso que puede admitir archivos en el espacio de trabajo o texto.

mkobit
fuente
1
No me funcionó, seguía recibiendo un error Could not find matching constructor for: java.util.HashMap(java.util.ArrayList). La documentación sugiere que debería escupir una lista o un mapa: ¿cómo se configura para devolver un mapa?
Sunvic
@Sunvic Buen truco, los datos que hemos estado analizando son siempre objetos, nunca matrices JSON. ¿Estás intentando analizar una matriz JSON?
mkobit
Ah, sí, es una matriz JSON, eso será todo.
Sunvic
Tanto esta respuesta como la siguiente, en Jenkins, generaron una RejectedEception porque Jenkins funciona de maravilla en el entorno de la caja de arena
yiwen
@yiwen Mencioné que requiere una lista blanca de administrador, pero tal vez la respuesta podría aclararse en cuanto a lo que eso significa.
mkobit
8

Quiero votar a favor de una de las respuestas: recomendaría usar el complemento Pipeline Utility Steps, ya que tiene un paso readJSON que puede admitir archivos en el espacio de trabajo o texto: https://jenkins.io/doc/pipeline/steps / pipeline-utility-steps / # readjson-read-json-from-files-in-the-workspace

script{
  def foo_json = sh(returnStdout:true, script: "aws --output json XXX").trim()
  def foo = readJSON text: foo_json
}

Esto NO requiere ninguna lista blanca o cosas adicionales.

Regnoult
fuente
6

Esta es la respuesta detallada que se pidió.

El desarmado funcionó para mí:

String res = sh(script: "curl --header 'X-Vault-Token: ${token}' --request POST --data '${payload}' ${url}", returnStdout: true)
def response = new JsonSlurper().parseText(res)
String value1 = response.data.value1
String value2 = response.data.value2

// unset response because it's not serializable and Jenkins throws NotSerializableException.
response = null

Leo los valores de la respuesta analizada y cuando ya no necesito el objeto, lo desarmo.

Nils El-Himoud
fuente
5

Una forma un poco más generalizada de la respuesta de @mkobit que permitiría la decodificación de matrices y mapas sería:

import groovy.json.JsonSlurper

@NonCPS
def parseJsonText(String json) {
  def object = new JsonSlurper().parseText(json)
  if(object instanceof groovy.json.internal.LazyMap) {
      return new HashMap<>(object)
  }
  return object
}

NOTA: Tenga en cuenta que esto solo convertirá el objeto LazyMap de nivel superior en un HashMap. Cualquier objeto LazyMap anidado seguirá estando allí y seguirá causando problemas con Jenkins.

TomDotTom
fuente
2

La forma en que se implementó el complemento de canalización tiene implicaciones bastante serias para el código Groovy no trivial. Este enlace explica cómo evitar posibles problemas: https://github.com/jenkinsci/pipeline-plugin/blob/master/TUTORIAL.md#serializing-local-variables

En su caso específico, consideraría agregar una @NonCPSanotación slurpJSONy devolver el mapa de mapas en lugar del objeto JSON. No solo el código se ve más limpio, sino que también es más eficiente, especialmente si ese JSON es complejo.

Marcin Płonka
fuente
2

De acuerdo con las mejores prácticas publicadas en el blog de Jenkins ( mejores prácticas de escalabilidad de canalizaciones ), se recomienda encarecidamente utilizar herramientas de línea de comandos o scripts para este tipo de trabajo:

Entendido: ¡especialmente evite el análisis de Pipeline XML o JSON utilizando XmlSlurper y JsonSlurper de Groovy! Prefiere fuertemente las herramientas de línea de comandos o los scripts.

yo. Las implementaciones de Groovy son complejas y, como resultado, más frágiles en el uso de Pipeline.

ii. XmlSlurper y JsonSlurper pueden tener un alto costo de memoria y CPU en canalizaciones

iii. xmllint y xmlstartlet son herramientas de línea de comandos que ofrecen extracción XML a través de xpath

iv. jq ofrece la misma funcionalidad para JSON

v. Estas herramientas de extracción se pueden acoplar a curl o wget para obtener información de una API HTTP

Por lo tanto, explica por qué la mayoría de las soluciones propuestas en esta página están bloqueadas de forma predeterminada por el sandbox del complemento de script de seguridad de Jenkins.

La filosofía del lenguaje de Groovy está más cerca de Bash que Python o Java. Además, significa que no es natural hacer un trabajo complejo y pesado en Groovy nativo.

Dado eso, personalmente decidí usar lo siguiente:

sh('jq <filters_and_options> file.json')

Consulte jq Manual y Select objects con jq stackoverflow post para obtener más ayuda.

Esto es un poco contrario a la intuición porque Groovy proporciona muchos métodos genéricos que no están en la lista blanca predeterminada.

Si decide utilizar el lenguaje Groovy de todos modos para la mayor parte de su trabajo, con sandbox habilitado y limpio (lo cual no es fácil porque no es natural), le recomiendo que consulte las listas blancas de la versión de su complemento de script de seguridad para saber cuáles son sus posibilidades: Script listas blancas de complementos de seguridad

vhamon
fuente
2

Puede utilizar la siguiente función para convertir LazyMap en un LinkedHashMap normal (mantendrá el orden de los datos originales):

LinkedHashMap nonLazyMap (Map lazyMap) {
    LinkedHashMap res = new LinkedHashMap()
    lazyMap.each { key, value ->
        if (value instanceof Map) {
            res.put (key, nonLazyMap(value))
        } else if (value instanceof List) {
            res.put (key, value.stream().map { it instanceof Map ? nonLazyMap(it) : it }.collect(Collectors.toList()))
        } else {
            res.put (key, value)
        }
    }
    return res
}

... 

LazyMap lazyMap = new JsonSlurper().parseText (jsonText)
Map serializableMap = nonLazyMap(lazyMap);

o mejor use un paso readJSON como se notó en comentarios anteriores:

Map serializableMap = readJSON text: jsonText
Sergey P.
fuente
1

Las otras ideas en esta publicación fueron útiles, pero no todo lo que estaba buscando, así que extraje las partes que se ajustan a mis necesidades y agregué algunas de mis propias magix ...

def jsonSlurpLaxWithoutSerializationTroubles(String jsonText)
{
    return new JsonSlurperClassic().parseText(
        new JsonBuilder(
            new JsonSlurper()
                .setType(JsonParserType.LAX)
                .parseText(jsonText)
        )
        .toString()
    )
}

Sí, como señalé en mi propio git commit del código, "Salvajemente ineficaz, pero pequeño coeficiente: solución JSON slurp" (que estoy de acuerdo con este propósito). Los aspectos que necesitaba resolver:

  1. Aléjese completamente del java.io.NotSerializableExceptionproblema, incluso cuando el texto JSON define contenedores anidados
  2. Trabajar para contenedores de mapas y matrices
  3. Admite el análisis de LAX (la parte más importante, para mi situación)
  4. Fácil de implementar (incluso con los incómodos constructores anidados que obvian @NonCPS)
Stevel
fuente
1

Noob error de mi parte. ¿Se movió el código de alguien de un antiguo complemento de canalización, jenkins 1.6? a un servidor que ejecute la última versión 2.x de jenkins.

Falló por esta razón: "java.io.NotSerializableException: groovy.lang.IntRange" Seguí leyendo y leyendo esta publicación varias veces por el error anterior. Realizado: para (num en 1..numSlaves) {IntRange - tipo de objeto no serializable.

Reescrito en forma simple: para (num = 1; num <= numSlaves; num ++)

Todo está bien en el mundo.

No uso Java o Groovy con mucha frecuencia.

Gracias chicos.

mpechner
fuente
0

Encontré una forma más fácil de utilizar documentos externos para la canalización de Jenkins

Ejemplo de trabajo

import groovy.json.JsonSlurperClassic 


@NonCPS
def jsonParse(def json) {
    new groovy.json.JsonSlurperClassic().parseText(json)
}

@NonCPS
def jobs(list) {
    list
        .grep { it.value == true  }
        .collect { [ name : it.key.toString(),
                      branch : it.value.toString() ] }

}

node {
    def params = jsonParse(env.choice_app)
    def forBuild = jobs(params)
}

Debido a las limitaciones en el flujo de trabajo, es decir, JENKINS-26481 , no es realmente posible usar cierres Groovy o una sintaxis que dependa de cierres, por lo que no puede> hacer el estándar Groovy de usar .collectEntries en una lista y generar los pasos como valores para las entradas resultantes. Tampoco puede usar la sintaxis estándar> Java para bucles For - es decir, "for (String s: strings)" - y en su lugar tiene que usar bucles for basados ​​en contadores de la vieja escuela.

Kirill K
fuente
1
Recomendaría usar el paso readJSON de la canalización de Jenkins en su lugar; encuentre más información aquí .
Sunvic