Buscar

Limpiar filtro
Artículo
Alberto Fuentes · 29 jul, 2019

Búsquedas en campos de texto libre de forma rápida

¡Hola a tod@s! Hoy os traigo un artículo de Kyle Baxter sobre búsquedas de texto libre que vale la pena guardar como referencia :) ¿Os gustaría buscar de forma eficiente campos de texto libres almacenados en vuestra aplicación? ¿Lo habéis intentado alguna vez pero no habéis encontrado una manera que os ofrezca un buen rendimiento? Hay un truco especial que resuelve el problema :) Como es habitual, si preferís la versión TL;DR (Demasiado largo, no lo he leído), podéis ir directamente al final del artículo, pero preferiríamos que leyeseis el artículo entero para evitar herir sentimientos. Vamos a utilizar como ejemplo la clase Sample.Company que encontrareis en: IRIS - incluida en el paquete de ejemplos https://github.com/intersystems/Samples-Data que puedes descargar e instalar Caché / Ensemble - namespace SAMPLES La clase Sample.Company tiene el campo Mission que es un texto generado al azar. Para el ejemplo, se han generado alrededor 256,246 empresas. /// The company's mission statement. Property Mission As %String(MAXLEN = 200, POPSPEC = "Mission()"); Supongamos que queremos buscar en este campo de texto: SELECT * FROM Sample.Company WHERE LIKE '%agile%' Es una consulta bastante razonable, pero ¿cómo funciona? Al no tener un índice, claramente necesita leer cada entrada, de modo que obtenemos 256,277 referencias a globals de nuestras 7,854 filas devueltas. ¡Esto no está bien! Añadamos un índice a Sample.Company y veamos si podemos hacerlo mejor: Index MissionIndex on Mission; Construimos el índice y ejecutamos la misma consulta. ¿Qué obtenemos ahora? 279,088 referencias a globals. Pero... ¡eso son más referencias a globals! ¿Acaso no es malo? ¡Los índices debían habernos ayudado! Calmémonos. Además, cuando nos enfrentamos a un comportamiento contradictorio, es mejor tomarse un momento para pensar. ¿Cuál es el coste de leer un índice en comparación con leer la tabla de datos completa? Un índice será más pequeño, así que leer el índice MissionIndex completo será una operación menos costosa que hacer un escaneo completo de la tabla. Pero además del índice, necesitaremos leer algunas partes de la tabla para mostrar los resultados. En este caso, aunque haya más referencias a globals, es menos trabajo (asumiendo que todo está en disco). Por supuesto, podemos hablar mucho más sobre este comportamiento y la estructura de bloques de bases de datos de Caché pero eso está fuera del ámbito de este artículo. De acuerdo, así que nuestra primera modificación y el ahorro de nuestra búsqueda posiblemente funcionaron, pero ciertamente el resultado fue menor de lo que nos gustaría. Buscamos algo mucho más rápido, queremos menos referencias a globals y que sea sencillo de implementar ¿qué podemos hacer? la respuesta es un índice iFind . Vamos a definir el índice de la siguiente forma: Index MissioniFind on (Mission) as %iFind.Index.Basic; Reconstruimos los índices ##class(Sample.Company).%BuildIndices() y ahora necesitamos decirle a la consulta que utilice este nuevo índice: SELECT * FROM Sample.Company WHERE %ID %FIND search_index(MissioniFind,’agile’) Ahora, esta consulta obtiene las mismas 7,854 filas, pero lo hace con 7,928 referencias a globals. ¡Son apenas más referencias a globals que filas! Lo cual es algo muy bueno. ¿Podemos combinar esto con otros índices? Sí, las combinaciones de índices funcionan muy bien con esta tecnología. ¿Existe alguna restricción? Pues sí. Necesita que el ID de la clase sea compatible con bitmaps (mapas de bits). Es decir, el ID de la tabla debe ser un número entero positivo. ¿Dónde puedo obtener más información? En la documentación, desde luego: https://irisdocs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLSRCH_txtsrch_indexing https://irisdocs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25iFind.Index.Basic ------------------------------------------------------ TL;DR (Demasiado largo, no lo he leído): Para buscar en un campo de texto libre: 1. Añadir un índice iFind sobre el campo de texto libre: Index <IndexName> on <FreeTextField> As %iFind.Index.Basic 2. Reconstruir índices de forma habitual, por ejemplo: do ##class(Sample.Company).%BuildIndices() 3. Re-escribir la consulta SQL de la siguiente manera en la cláusula WHERE: …WHERE ID %FIND search_index(<IndexName>,<Search Value>) AND … Este artículo está etiquetado como "Mejores prácticas" ("Best practices"). Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Alberto Fuentes · 25 ago, 2022

Recoger datos de rendimiento mientras se ejecutan pruebas unitarias

Hace varios años, estaba enseñando los conocimientos básicos de nuestro framework %UnitTest durante la clase de Fundamentos de Caché (ahora llamada Developing Using InterSystems Objects and SQL). Un alumno preguntó si era posible recoger estadísticas de rendimiento mientras se ejecutan pruebas unitarias. Unas semanas más tarde, añadí un código adicional a los ejemplos de %UnitTest para responder a esa pregunta. Ahora lo comparto con la Comunidad. La clase %SYSTEM,Process ofrece varias métricas que puedes recoger para un proceso (además de Duration). Duration Lines Executed Global References System CPU Time User CPU Time Disk Read Time Para permitir a cualquier prueba unitaria recoger esas estadísticas, crea una subclase de %UnitTest.TestCase y añade propiedades. Class Performance.TestCase Extends %UnitTest.TestCase { Property Duration As %Time; Property Lines As %Integer; Property Globals As %Integer; Property SystemCPUTime As %Integer; Property UserCPUTime As %Integer; Property DiskReadTime As %Integer; } Cualquier clase de unit test que crees debería heredar de tu nueva subclase en vez de %UnitTest.TestCase. En la subclase, usa OnBeforeOneTest() para iniciar la recogida de estadísticas para cada prueba unitaria. Para todo excepto DiskReadTime,el código inicia la propiedad con el valor actual. /// initialize performance stats Method OnBeforeOneTest(testname As %String) As %Status { // initialize with current values set ..Duration = $zh set ..Lines = $system.Process.LinesExecuted() set ..Globals = $system.Process.GlobalReferences() set ..SystemCPUTime = $piece(CPUTime, ",", 1) set ..UserCPUTime = $piece(CPUTime, ",", 2) // reset disk read time to 0 and start counting do $system.Process.ResetDiskReadTiming() do $system.Process.EnableDiskReadTiming() return $$$OK } Usa OnAfterOneTest() para terminar la recogida de estadísticas para cada prueba unitaria. Para todo excepto DiskReadTime, el código elimina el valor inicial desde el valor actual. /// Finalize performance stats /// This is where you could also add code to save the counters to a separate table for analysis. Method OnAfterOneTest(testname As %String) As %Status { set ..Duration = $zh - ..Duration set ..Lines = $system.Process.LinesExecuted() - ..Lines set ..Globals = $system.Process.GlobalReferences() - ..Globals set CPUTime = $system.Process.GetCPUTime() set ..SystemCPUTime = $piece(CPUTime, ",", 1) - ..SystemCPUTime set ..UserCPUTime = $piece(CPUTime, ",", 2) - ..UserCPUTime // get disk read time and stop counting set ..DiskReadTime = $system.Process.DiskReadMilliseconds() do $system.Process.DisableDiskReadTiming() // add message to unit test log set msg = "Performance: " _ "Duration: " _ ..Duration _ ", Lines: " _ ..Lines _ ", Globals: " _ ..Globals _ ", System CPU Time: " _ (..SystemCPUTime / 1000) _ ", User CPU Time: " _ (..UserCPUTime / 1000) _ ", Disk Read Time: " _ (..DiskReadTime / 1000) do $$$LogMessage(msg) return $$$OK } Hay otro pequeño truco. Puede que quieras ejecutar tus pruebas unitarias con o sin estadísticas de recogida. Así que, el código en el que estás invocando tus pruebas unitarias debe tomar un argumento (podría ser un %Boolean 1 o 0) y de alguna manera pasarlo. Los métodos que realmente ejecutan las pruebas (como RunTest() u otro de los métodos Run*()) toman un array como el 3er argumento, pasado por referencia. Este es un ejemplo: // create an array to hold the logging argument (1 or 0) and pass it by reference set p("logging") = logging do ##class(%UnitTest.Manager).RunTest(test, qualifiers, .p) Al valor que pasas en la matriz puede accederse en OnBeforeOneTest() y OnAfterOneTest(). Añade esto como la primera línea en ambos métodos: if (..Manager.UserFields.GetAt("logging") = 0) { return $$$OK } ¡Y ya está! Espero vuestras preguntas, comentarios e ideas. Este artículo está etiquetado como "Mejores prácticas" ("Best practices"). Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Alberto Fuentes · 13 sep, 2021

Importar CSV desde un fichero o una URL utilizando csvgen

¡Hola desarrolladores! A veces necesitamos importar datos CSV de forma programática en InterSystems IRIS desde un fichero o una URL. Y esperamos además que automáticamente se genere una clase con los tipos de datos adecuados y los datos importados. Echadle un ojo al módulo [csvgen](https://openexchange.intersystems.com/package/csvgen) en Open Exchange que hace exactamente eso que hemos descrito. Si necesitas importar un fichero CSV en IRIS puedes hacer esto: ``` USER>do ##class(community.csvgen).Generate("/usr/data/titanic.csv",,"Data.Titanic") Class name: Data.Titanic Header: PassengerId INTEGER,Survived INTEGER,Pclass INTEGER,Name VARCHAR(250),Sex VARCHAR(250),Age INTEGER,SibSp INTEGER,Parch INTEGER,Ticket VARCHAR(250),Fare MONEY,Cabin VARCHAR(250),Embarked VARCHAR(250) Records imported: 891 USER> ``` O si los datos CSV están publicados en internet, por ejemplo [Datos COVID-19 en GitHub](https://github.com/CSSEGISandData/COVID-19/blob/master/csse_covid_19_data/csse_covid_19_daily_reports/05-29-2020.csv), puedes hacer esto otro: ``` USER>d ##class(community.csvgen).GenerateFromURL("https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_daily_reports/05-29-2020.csv",",","Data.Covid19") Class name: Data.Covid19 Header: FIPS INTEGER,Admin2 VARCHAR(250),Province_State VARCHAR(250),Country_Region VARCHAR(250),Last_Update DATE,Lat MONEY,Long_ DOUBLE,Confirmed INTEGER,Deaths INTEGER,Recovered INTEGER,Active INTEGER,Combined_Key VARCHAR(250),Incidence_Rate DOUBLE,Case-Fatality_Ratio DOUBLE Records imported: 3522 USER> ``` ## Instalación Puedes instalar el módulo directamente utilizando ZPM: ``` USER>zpm zpm:USER>install csvgen ``` El módulo csvgen en realidad es un envoltorio del método [CSV2CLASS](https://cedocs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?APP=1&CLASSNAME=%25SQL.Util.Procedures) . Si os animáis a colaborar, ¡cualquier mejora es bienvenida!
Artículo
Muhammad Waseem · 19 nov, 2021

Cómo eliminar datos de Caché usando la conexión ODBC de Appeon PowerBuilder

En mis artículos anteriores, mostré los pasos para conectar, recuperar y guardar datos en Caché desde Appeon PowerBuilder usando ODBC. En este artículo, mostraré cómo eliminar datos de Caché con Appeon PowerBuilder (https://www.appeon.com/products/powerbuilder) usando ODBC. Estoy usando Company.cls de Samples-Data (https://github.com/intersystems/Samples-Data/tree/master/cls/Sample) ¡Empecemos! Paso 1: En primer lugar, debemos establecer una conexión (https://community.intersystems.com/post/connecting-cach%C3%A9-appeon-powerbuilder-using-odbc) Paso 2: Necesitamos crear un objeto de ventana de datos (datawindow object) que se vinculará a la clase Company. En el menú File, selecciona New y elige el objeto de ventana de datos Freeform en la pestaña DataWindow. Paso 3: Selecciona SQL Select de la lista de fuentes de datos Paso 4: Selecciona sample.company de la lista de tablas: Paso 5: Selecciona las columnas deseadas en la lista de columnas y haz clic en Return. Paso 6: Esto abrirá una vista de diseño. Guarda la ventana de datos (datawindow) como d_company_entry después de los ajustes deseados. Paso 7: Asegúrate de anular la selección de la columna id en la lista de columnas sin fecha (Updatable columns) y selecciona id en la lista desplegable del campo Identity Column, ya que la identificación se generará automáticamente desde Caché. Paso 8: En el control de ventana, añade el objeto de ventana de datos (d_company_entry) que ya creamos al control de ventana de datos Paso 9: Añade Retrieval Arguments para recuperar los datos basados en ID. Para esto, añadiremos (Desde el menú Design > Retrieval Arguments) en nuestro control de ventana de datos Paso 10: Añade el parámetro recién añadido en la cláusula where Paso 11: Recupera los datos basados en el ID utilizando la función de control de la ventana de datos Retrieve (ID) Paso 12: Elimina el registro usando la función de control de ventana de datos deleterow () . Es lo mismo que el método de clase% DeleteID Eso es todo. Se elimina el ID 21
Artículo
Muhammad Waseem · 14 jun, 2022

Comparación del tiempo de respuesta de los mensajes en Python y Objectscript

Esta es una comparación creada en Python y Objectscript en InterSystems IRIS. El objetivo es comparar la velocidad para enviar y recibir mil solicitudes/mensajes desde un BP a un BO en Python y en Objectscript. Consultar [https://github.com/LucasEnard/benchmark-python-objectscript](https://github.com/LucasEnard/benchmark-python-objectscript) para más información. **IMPORTANTE** : Aquí están los resultados del tiempo en segundos, para enviar **1000 mensajes** de *ida y vuelta* desde un `bp` a un `bo` usando Python, Graph Objectscript y Objectscript. Los mensajes de cadena se componen de diez variables de cadena. Los mensajes de objeto se componen de diez variables de objeto, cada objeto como su propio int, float, str y List(str). | Cadenas de mensajes| Tiempo (segundos) para 1000 mensajes de ida y vuelta | |------------------------|------------------| | Python BP | 1.8 | | BPL | 1.8 | | ObjectScript | 1.4 | | Objetos de mensajes| Tiempo (segundos) para 1000 mensajes de ida y vuelta | |------------------------|------------------| | Python BP | 3.2 | | BPL | 2.1 | | ObjectScript | 1.8 | La función en la fila tiene x veces el tiempo de la función en la columna: | Cadenas de mensajes| Python | BPL | ObjectScript | |------------------------|------------|------------------------|------------------| | Python | 1 | 1 | 1.3 | | BPL | 1 | 1 | 1.3 | | ObjectScript | 0.76 | 0.76 | 1 | Por ejemplo, la primera fila nos dice que el tiempo de cadena de Python es 1 vez el tiempo de la función de cadena de gráficos de Objectscript y 1,3 veces el tiempo de la función de cadena de Objectscript. (gracias a la primera tabla podemos verificar nuestros resultados: 1.3 * 1.4 = 1.8 1.3 es la x en la tabla en la última columna de la primera fila, 1.4s es el tiempo para los mensajes de cadena en Objectscript vistos en la primera tabla de esta sección y 1.8s es de hecho el tiempo para los mensajes de cadena en python que podemos encontrar buscando en la primera tabla de esta sección o mediante el cálculo como se mostró antes). Tenemos la función en la fila que tiene x veces el tiempo de la función en la columna: | Cadenas de mensajes| Python | BPL | ObjectScript | |------------------------|------------|------------------------|------------------| | Python | 1 | 1.5 | 1.8 | | BPL | 0.66 | 1 | 1.2 | | ObjectScript | 0.55 | 0.83 | 1 |
Artículo
Jose-Tomas Salvador · 31 mayo, 2023

API para importar/exportar rutinas

Este es un artículo de la página de "Preguntas frecuentes" (FAQ) de InterSystems. 1. Exportar API a. Usa $system.OBJ.Export() para especificar rutinas individuales para exportar. Por ejemplo: do $system.OBJ.Export("TEST1.mac,TEST2.mac","c:\temp\routines.xml",,.errors) El formato que debes especificar es: NombreDeLaRutina.extension, y la extensión puede ser: mac, bas, int, inc, obj. Los errores durante la exportación se almacenan en la variable "errors". Echa un vistazo a la referencia de clase %SYSTEM.OBJ para más detalles sobre $system.OBJ.Export(). b. Usa $system.OBJ.Export() incluso al hacer una exportación genérica usando * (wildcards). Por ejemplo: do $system.OBJ.Export("*.mac",c:\temp\allmacroutines.xml") *Antes de la versión 2008.1, utiliza $system.OBJ.ExportPattern(). 2. Importar API a. Usa $system.OBJ.Load() para importar todas las rutinas contenidas en el fichero. Por ejemplo: do $system.OBJ.Load("c:\temp\routines.xml",,.errors) b. Importa solo algunas de las rutinas contenidas en el fichero Observa el ejemplo de abajo. Si quieres seleccionar e importar solo algunas de las rutinas incluidas en el fichero XML, pon a 1 el 5º argumento "listonly" en una primera ejecución, y carga el archivo XML con $system.OBJ.Load(), estableciendo el 4º argumento (argumento de salida, list en el ejemplo abajo). Esto creará una lista de elementos en la variable list. Después podremos recorrer esa lista y decidir que elementos (loaditem) queremos cargar, volviendo a ejecutar $system.OBJ.Load() e indicando el elemento a cargar en el 6º argumento. Lo puedes ver más claramente en este ejemplo: Set file="c:\temp\routines.xml" // First get the list of items contained in the XML Do $system.OBJ.Load(file,,.errors,.list,1 /* listonly */) Set item=$Order(list("")) Kill loaditem While item'="" { If item["Sample" Set loaditem(item)="" { // Import only those containing Sample Set item=$Order(list(item)) } } // Execute import process with created list Do $system.OBJ.Load(file,,.errors,,,.loaditem)
Artículo
Ricardo Paiva · 19 feb, 2021

La API nativa de IRIS para Python en AWS Lambda

Si está buscando una forma ingeniosa para integrar su solución de IRIS en el ecosistema de Amazon Web Services, en una aplicación sin servidor o en `Boto3` (un potente script de Python), usar la [API nativa de IRIS para Python](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=AFL_PYNATIVE) podría ser el camino a seguir. No es necesario que invierta demasiado tiempo en la implementación de una producción hasta que deba acercarse y obtener algo o establecer algo en IRIS para hacer que su aplicación ejecute su característica más sobresaliente, así que esperamos este artículo sea útil y desarrolle algo aunque solamente usted pueda usarlo, ya que eso también es importante. ![imagen](/sites/default/files/inline/images/aws_lambda_native_api_iris_0.png) Si necesita algunas excusas para implementarla, utilice las siguientes: * Debe cargar el Activador que generará previamente los tokens en Cognito para buscar y completar el contexto de la identificación del paciente en el token. Esto con la finalidad de utilizar una solución basada en SMART on FHIR(R), la cual implementará un flujo de trabajo en OAUTH2. * Quiere publicar el suministro de la configuración para los parámetros en IRIS que se basa en el tipo de instancia, grupo de nodo, Toaster o el clúster ECS que inició para ejecutar IRIS en el anillo cero. * Quiere impresionar a su familia y amigos en Zoom con sus habilidades para administrar IRIS sin que su shell tenga que abandonar la CLI de AWS. ## Lo más importante Aquí proporcionamos una función lambda de AWS que se comunica con IRIS y brindaremos algunos ejemplos sobre cómo utilizarla, además indicaremos cómo podemos interactuar con ella en varias funciones con la esperanza de que todos podamos discutir sobre ella y **publicarla en pip para facilitar las cosas.** ## ¿¿¿¿Tiene problemas???? ![imagen](/sites/default/files/inline/images/hurry.png) Consulte el Stream ## En todos los casos... Para participar en la diversión, necesitará eliminar algunas cosas de sus planes. ### Interconexión Tiene a IRIS ejecutándose en cualquier lugar que desee, funcionando en una AWS VPC con exactamente dos subredes y un grupo de seguridad que permite el acceso al super servidor que inicia IRIS... utilizamos 1972 por razones nostálgicas y por el simple hecho de que InterSystems se tomó el tiempo para registrar ese puerto con [IANA](https://tools.ietf.org/html/rfc6335), y si agrega el nuevo puerto en `/etc/services` sin registrarlo, sufrirá las mismas consecuencias que si arrancara la etiqueta de un colchón nuevo. En nuestro caso, es un conjunto de réplicas de las instancias EC2 con las comprobaciones de estado adecuadas en torno a un balanceador de carga para la red AWS v2. ![imagen](/sites/default/files/inline/images/toaster2.png) ### Ejemplo de una clase importada De alguna forma consiguió crear e importar la clase a en la raíz de este repositorio con el namespace `%SYS` en su instancia de IRIS. A continuación, se muestra el ejemplo de una clase que impulsa la salida anterior. Si se pregunta por qué necesitamos una clase para importar aquí, consulte la siguiente nota donde el enfoque recomendado es proporcionar algunas clases envolventes para utilizarlas con Python. > Nota de los documentos: Aunque estos métodos también pueden utilizarse con las clases que se definieron en la Biblioteca de clases de InterSystems, una de las prácticas recomendadas consiste en llamarlos indirectamente, desde el interior de una clase o rutina definida por el usuario. Muchos métodos de clase devuelven solo un código de estado, y transmiten los resultados reales a un argumento (al que no es posible acceder por la API nativa). Las funciones definidas por el sistema (incluidas como parte de las Funciones de ObjectScript en la Referencia de ObjectScript) no pueden llamarse directamente. Ejemplo de la clase: ``` Class ZDEMO.IRIS.Lambda.Operations Extends %Persistent { ClassMethod Version() As %String { Set tSC = 0 Set tVersion = $ZV if ( tVersion '="" ) { set tSC = $$$OK } Set jsonret = {} Set jsonret.status = tSC Set jsonret.payload = tVersion Quit jsonret.%ToJSON() } } ``` Tenga en cuenta que decidí trabajar según lo establecido en los métodos, de modo que siempre emite un objeto JSON como respuesta, lo cual también me permite enviar el estado y posiblemente subsanar las deficiencias que implica devolver algunas cosas como referencia. ### AWS Access Obtenga algunas claves de acceso IAM que le permitirán suministrar e invocar la función Lambda con la que cambiaremos el mundo. Comprobación previa: ``` IRIS [ $$$OK ] VPC [ $$$OK ] Subnets [ $$$OK ] Security Group [ $$$OK ] IAM Access [ $$$OK ] Imported Class [ $$$OK ] ``` $$$OK, **Lesgo**. ## Empaquetar la API nativa de IRIS para Python con el fin de utilizarla en la función Lambda Esta parte sería fantástica si fuera un paquete pip, especialmente si solo es para Linux, ya que las funciones Lambda de AWS se ejecutan en Linux Boxen. Cuando realizamos esta asignación la API no estaba disponible por medio de pip, pero somos hábiles y podemos lanzar nuestro propio paquete. ``` mkdir iris_native_lambda cd iris_native_lambda wget https://github.com/intersystems/quickstarts-python/raw/master/Solutions/nativeAPI_wheel/irisnative-1.0.0-cp34-abi3-linux_x86_64.whl unzip nativeAPI_wheel/irisnative-1.0.0-cp34-abi3-linux_x86_64.whl ``` A continuación, cree `connection.config` Por ejemplo: [connection.config](https://raw.githubusercontent.com/basenube/iris_native_lambda/main/examples/connection.config) Cree su controlador, `index.py` o utilice el que se encuentra en la carpeta de ejemplos, en el [repositorio con demostraciones de GitHub](https://github.com/basenube/iris_native_lambda). Tenga en cuenta que la versión de demostración utiliza tanto variables de entorno como un archivo externo para guardar la información de conectividad de IRIS. Por ejemplo: [index.py](https://raw.githubusercontent.com/basenube/iris_native_lambda/main/examples/index.py) Ahora comprímalo para utilizarlo: ``` zip -r9 ../iris_native_lambda.zip * ``` Cree un bucket S3, y cargue la función zip en él. ``` cd .. aws s3 mb s3://iris-native-bucket s3 sync iris_native_lambda.zip s3://iris-native-bucket ``` > Con esto concluye el empaquetado de la API y el controlador para utilizarlos como una función Lambda de AWS. Ahora, haga clic hasta que la consola termine para crear la función, o utilice algo como Cloudformation para realizar el trabajo: ``` IRISAPIFunction: Type: "AWS::Lambda::Function" DependsOn: - IRISSG - VPC Properties: Environment: Variables: IRISHOST: "172.31.0.10" IRISPORT: "1972" NAMESPACE: "%SYS" USERNAME: "intersystems" PASSWORD: "lovetheyneighbor" Code: S3Bucket: iris-native-bucket S3Key: iris_native_lambda.zip Description: "IRIS Native Python API Function" FunctionName: iris-native-lambda Handler: "index.lambda_handler" MemorySize: 128 Role: "arn:aws:iam::8675309:role/BeKindtoOneAnother" Runtime: "python3.7" Timeout: 30 VpcConfig: SubnetIds: - !GetAtt - SubnetPrivate1 - Outputs.SubnetId - !GetAtt - SubnetPrivate2 - Outputs.SubnetId SecurityGroupIds: - !Ref IRISSG ``` Eso fue MUCHO, pero ahora puede volverse loco, llamar a IRIS mediante la función lambda con Python y cambiar el mundo. ## ¡Vamos a iniciarlo! De la forma en que se implementó lo anterior, se espera que la función sea aprobada en un evento del objeto que esté menos estructurado para reutilizarlo, puede ver la idea en el ejemplo para el evento del objeto que se muestra a continuación: ``` { "method": "Version", # important, if method requires no args, enforce "none" "args": "none" # example method with args, comma seperated # "args": "thing1, thing2" } ``` ahora puede hacerlo, si tiene cierta tolerancia para los ejemplos de la línea de comandos, eche un vistazo en la ejecución que se muestra a continuación utilizando la CLI de AWS: ``` (base) sween @ basenube-pop-os ~/Desktop/BASENUBE └─ $ ▶ aws lambda invoke --function-name iris-native-lambda --payload '{"method":"Version","args":"none"}' --invocation-type RequestResponse --cli-binary-format raw-in-base64-out --region us-east-2 --profile default /dev/stdout {{\"status\":1,\"payload\":\"IRIS for UNIX (Red Hat Enterprise Linux for x86-64) 2020.2 (Build 210U) Thu Jun 4 2020 15:48:46 EDT\"}" "StatusCode": 200, "ExecutedVersion": "$LATEST" } ``` Ahora bien, si vamos un poco más lejos, la CLI de AWS admite alias, así que cree uno y podrá jugar integrando completamente su increíble comando con AWS. Este es un ejemplo del alias para la CLI: ``` └─ $ ▶ cat ~/.aws/cli/alias [toplevel] whoami = sts get-caller-identity iris = !f() { aws lambda invoke \ --function-name iris-native-lambda \ --payload \ "{\"method\":\""${1}"\",\"args\":\"none\"}" \ --invocation-type RequestResponse \ --log-type None \ --cli-binary-format raw-in-base64-out \ gar.json > /dev/null cat gar.json echo echo }; f ``` ....y ahora puede hacerlo... ![imagen](/sites/default/files/inline/images/aws_lambda_iris_python_0.png) ¡Manténgase a salvo!~ ¡Todos los argumentos técnicos son bienvenidos!
Artículo
Luis Angel Pérez Ramos · 25 abr, 2023

Configurando Mirror en Docker

Una necesidad habitual en nuestros clientes es la configuración tanto de HealthShare HealthConnect como de IRIS en modo de alta disponibilidad. Es común en otros motores de integración del mercado que se promocionen con configuraciones de "alta disponibilidad", pero realmente no suele ser del todo cierto. Por lo general dichas soluciones trabajan con bases de datos externas y por lo tanto, si estas no están a su vez configuradas en alta disponibilidad, al producirse una caída de la base de datos o la pérdida de conexión a la misma toda la herramienta de integración queda inutilizable. En el caso de las soluciones de InterSystems este problema no existe, al ser la base de datos parte y nucleo de las propias herramientas. ¿Y cómo ha solucionado InterSystems el problema de la alta disponibilidad? ¿Con abstrusas configuraciones que podrían arrastrarnos a una espiral de enajenamiento y locura? ¡NO! Desde InterSystems hemos escuchado y atendido vuestras quejas (como siempre intentamos hacer ;) ) y hemos puesto a disposición de todos nuestros usuarios y desarrolladores la función de mirroring. Mirroring ¿Cómo funciona el Mirror? El concepto en si es muy sencillo. Como ya sabréis tanto IRIS como HealthShare trabajan con un sistema de journaling que registra toda operación de actualización sobre las bases de datos de cada instancia. Este sistema de journaling es el que posteriormente nos sirve para recuperar las instancias sin apenas pérdida de datos en caso de caída de las mismas. Pues bien, son estos archivos de journal los que se envían entre las instancias configuradas en mirror y permiten mantener permanentemente actualizados las instancias configuradas en mirror. Arquitectura Expliquemos brevemente como sería la arquitectura de un sistema configurado en Mirror: Dos instancias configuradas en modo Failover: Nodo activo: recibe todas las operaciones de lectura/escritura habituales. Nodo pasivo: en modo lectura, recibe de forma síncrona cualquier cambio producido en el nodo activo. 0-14 instancias asíncronas: tantas instancias asíncronas como se quieran utilizar, pueden ser de dos tipos: DR async (Disaster Recovery): nodos en modo lectura que no forman parte del Failover aunque se le puede promocionar manualmente.De ser así podría llegar a promocionarse automáticamente a nodo primario en caso de caída de los otros dos nodos del Failover. La actualización de sus datos es en modo asíncrono, por lo que no se garantiza la frescura de los mismos. Reporting Asyncs: nodos actualizados de forma asíncrona para su uso en tareas de BI o explotaciones de datos. No pueden ser promocionados al Failover ya que se pueden realizar escrituras sobre los datos. ISCAgent: instalado en cada servidor donde se encuentre una instancia. Será el encargado de monitorizar el estado de las instancias de dicho servidor. Es otra vía de comunicación entre los servidores del Mirror además de la comunicación directa. Arbiter: es un ISCAgent instalado de forma independiente a los servidores que forman el Mirror y permite aumentar la seguridad y el control de los failover dentro del mismo monitorizando tanto a los ISCAgent instalados como a las instancias de IRIS/HealthShare. Su instalación no es obligatoria. Este sería el funcionamiento de un Mirror formado por un failover con dos únicos nodos: Aviso previo El proyecto asociado a este artículo no dispone de una licencia activa que permita la configuración del mirror. Si queréis probarlo enviadme directamente un email o añadid un comentario al final del artículo y me pondré en contacto con vosotros. Despliegue en Docker Para este artículo vamos primeramente a montar un pequeño proyecto en Docker que nos permita montar 2 instancias en failover con un Arbiter. Por defecto las imágenes de IRIS disponibles para Docker tienen ya instalado y configurado el ISCAgent, por lo que nos podremos saltar ese paso. Será necesario configurar el proyecto que viene asociado al artículo desde un Visual Studio Code, ya que nos permitirá posteriormente trabajar de una forma más cómoda con los archivos del servidor. Veamos que forma tendría nuestro docker-compose.yml: version: '3.3' services: arbiter: container_name: arbiter hostname: arbiter image: containers.intersystems.com/intersystems/arbiter:2022.1.0.209.0 init: true command: - /usr/local/etc/irissys/startISCAgent.sh 2188 mirrorA: image: containers.intersystems.com/intersystems/iris:2022.1.0.209.0 container_name: mirrorA depends_on: - arbiter ports: - "52775:52773" volumes: - ./sharedA:/shared - ./install:/install - ./management:/management command: --check-caps false --key /install/iris.key -a /install/installer.sh environment: - ISC_DATA_DIRECTORY=/shared/durable hostname: mirrorA mirrorB: image: containers.intersystems.com/intersystems/iris:2022.1.0.209.0 container_name: mirrorB depends_on: - arbiter - mirrorA ports: - "52776:52773" volumes: - ./sharedB:/shared - ./install:/install - ./management:/management command: --check-caps false --key /install/iris.key -a /install/installer.sh environment: - ISC_DATA_DIRECTORY=/shared/durable hostname: mirrorB Podemos ver que tenemos definidos 3 containers: Arbiter: correspondiente al ISCAgent (aunque la imagen se llame Arbiter) que se desplegará para el control de las instancias de IRIS que formarán el Failover del Mirror. Al arrancar el container ejecutará un fichero shell que arrancará el ISCAgent escuchando en el puerto 2188 del container. mirrorA: container en el que se desplegará la imagen de IRIS v.2022.1.0.209 y que posteriormente configuraremos como nodo primario del Failover. mirrorB: container en el que se desplegará la imagen de IRIS v.2022.1.0.209 y que posteriormente configuraremos como nodo secundario del Failover. Cuando ejecutemos el comando docker-compose up -d se desplegarán en nuestro Docker los contenedores definidos, debiendose ver tal que así en nuestro Docker Desktop (si lo hacemos desde Windows). Configuración del mirror. Con nuestros contenedores desplegados procederemos a acceder a las instancias que vamos a configurar en mirror, la primera se encontrará escuchando en el puerto 52775 (mirrorA) y la segunda en el 52776 (mirrorB). El usuario y la contraseña de acceso serán superuser / SYS Debido a la que las instancias se encuentran desplegadas en Docker, tendremos dos opciones de para configurar las IP de nuestros servidores. La primera es usar directamente el nombre de nuestros contenedores en la configuración (que es la más sencilla) o bien comprobar las IP que Docker ha asignado a cada contenedor (abriendo la consola y ejecutando un ifconfig que nos devuelva la IP asignada). Por motivos de claridad utilizaremos para el ejemplo los nombres que les hemos dado a cada contenedor como dirección de cada uno dentro de Docker. Primeramente configuraremos la instancia que utilizaremos como nodo activo del FailOver. En nuestro caso será la que hemos denominado mirrorA. El primer paso será habilitar el servicio de mirroring, por lo que accederemos al menú de mirror desde el portál de gestión: System Adminsitration --> Configuration --> Mirror Settings --> Enable Mirror Service y marcaremos el check de Service Enabled: Con el servicio habilitado ya podremos empezar a configurar nuestro nodo activo. Tras habilitar el servicio podréis observar que se han habilitado nuevas opciones en el menú de Mirror: En este caso, al no tener ninguna configuración de mirror ya creada deberemos crear una nueva con la opción de Create Mirror. Cuando accedemos a dicha opción el portal de gestión nos abrirá una nueva ventana desde la que podremos configurar nuestro mirror: Veamos con más detalle cada una de las opciones: Mirror Name: el nombre con el que identificaremos a nuestro mirror. Para nuestro ejemplo lo llamaremos MIRRORSET Require SSL/TLS: para nuestro ejemplo no configuraremos conexión mediante SSL/TLS aunque en entornos productivos sería más que conveniente para evitar que el archivo de journal se comparta sin ningún tipo de cifrado entre las instancias. Si tenéis interés en configurarlo tenéis toda la información necesaria en la siguiente URL de la documentación. Use Arbiter: esta opción no es obligatoria, pero es bastante recomendable, ya que añade una capa de seguridad a la configuración de nuestro mirror. Para nuestro ejemplo lo dejaremos marcado e indicaremos la IP en el que tenemos funcionando nuestro Arbiter. Para nuestro ejemplo la IP será en nombre de contenedor "arbiter". User Virtual IP: en entornos Linux/Unix está opción es muy intersante ya que nos permite configurar una IP virtual para nuestro nodo activo que será gestionada por nuestro Mirror. Esta IP virtual debe pertenecer a la misma subred en la que se encuentren los nodos del Failover. El funcionamiento de la IP virtual es muy sencillo, en caso de caída del nodo activo el mirror configurará automáticamente la IP virtual en el servidor en el que se encuentre el nodo pasivo que va a ser promocionado. De tal forma, la promoción del nodo pasivo a activo será totalmente transparente para los usuarios, ya que estos seguirán conectados a la misma IP, aunque esta se encontrará configurada en un servidor distinto. Si queréis saber más al respecto de la IP virtual podéis revisar esta URL de la documentación. El resto de la configuración podemos dejarla tal como está. En el lado derecho de la pantalla veremos la información relativa a este nodo en el mirror: Mirror Member Name: nombre de este miembro del mirror, por defecto tomará el nombre del servidor junto con el de la instancia. Superserver Address: dirección IP del superserver de este nodo, en nuestro caso, mirrorA. Agent Port: puerto en el que se ha configurado el ISCAgent correspondiente a este nodo. Por defecto el 2188. Una vez configurado los campos necesarios podemos proceder a guardar el mirror. Podemos comprobar como ha quedado la configuración desde el monitor del mirror (System Operation --> Mirror Monitor). Perfecto, aquí tenemos nuestro mirror recién configurado. Como véis únicamente figura el nodo activo que acabamos de crear. Muy bien, vayamos entonces a añadir nuestro nodo pasivo en el Failover. Accedemos al portal de gestión de mirrorB y accedemos al menú de Mirror Settings. Como ya hicimos para la instancia de mirrorA deberemos habilitar el servicio de Mirror. Repetimos la operación y en cuanto se actualicen las opciones del menú elegiremos Join as Failover. Aquí tenemos la pantalla de conexión al mirror. Expliquemos brevemente que son cada uno de los campos: Mirror Name: nombre que dimos al mirror en el momento de la creación, en nuestro ejemplo MIRRORSET. Agent Address on Other System: IP del servidor en el que se encuentra desplegado el ISCAgent del nodo activo, para nosotros será mirrorA Agent Port: puerto de escucha del ISCAgent del servidor en el que creamos el mirror. Por defecto el 2188. InterSystems IRIS Instance Name: el nombre de la instancia de IRIS en el nodo activo. En este caso coincide con el del nodo pasivo, IRIS. Tras grabar los datos del mirror tendremos la opción de definir la información relativa al nodo pasivo que estamos configurando. Echemos nuevamente un vistazo a los campos que podemos configurar del nodo pasivo: Mirror Member Name: nombre que tomará el nodo pasivo en el mirror. Por defecto formado por el nombre del servidor y la instancia. Superserver Address: dirección IP de acceso al superserver nuestro nodo pasivo. En este caso mirrorB. Agent Port: puerto de escucha del ISCAgent instalado en el servidor del nodo pasivo que estamos configurando. Por defecto el 2188. SSL/TLS Requirement: al no configurarlo en la declaración del mirror no es configurable. Mirror Private Address: dirección IP del nodo pasivo. Como hemos visto, al usar Docker podremos usar el nombre del contenedor mirrorB. Agent Address: dirección IP al servidor donde está instalado el ISCAgent. Igual que antes, mirrorB. Grabamos la configuración tal y como hemos indicado y volvemos al monitor del mirror para comprobar que tenemos todo correctamente configurado. Podemos visualizar el monitor tanto del nodo activo en mirrorA como el del pasivo en mirrorB. Veamos las sutiles diferencias entre ambos. Monitor del mirror en nodo activo mirrorA: Monitor del mirror en nodo pasivo mirrorB: Como véis la información mostrada el similar, cambiando básicamente el orden de los miembros del failover. También las opciones son distintas, veamos alguna de ellas: Nodo activo mirrorA: Set No Failover: impide la ejecución del failover en caso de parada de alguna de las instancias que forman parte de él. Demote other member: elimina al otro miembro del failover (en este caso mirrorB) de la configuración del mirror. Nodo pasivo mirrorB: Stop Mirror On This Member: detiene la sincronización del mirror en el nodo pasivo del failover. Demote To DR Member: "degrada" a este nodo pasando de formar parte del failover con su sincronización en tiempo real al modo Disaster Recovery en modo asíncrono. Perfecto, ya tenemos nuestros nodos configurados, ahora nos queda el paso final en nuestra configuración. Decidir que tablas pasaran a formar parte del mirror y configurarlo en ambos nodos. Si observáis el README.md del proyecto asociado a este artículo veréis que indicamos la existencia de dos aplicaciones que usamos habitualmente para las formaciones y que son automáticamente desplegadas al arrancar los contenedores de Docker. La ventaja de usar esta configuración es que no tendremos que replicar a mano los NAMESPACES ni las bases de datos relacionadas de cara a configurar a mano el mirror, ya tenemos en ambas instancias los mismos namespaces y bases de datos. La primera es COMPANY que nos permite guardar registros de empresas y la segunda es PHONEBOOK que nos permite añadir contactos de personal relacionados con las empresas registradas, así como clientes. Añadamos una compañía: Y a continuación un contacto de dicha compañía: Los datos de la compañía se registrarán en la base de datos COMPANY y los del contacto en PERSONAL, ambas bases de datos están mapeadas para que puedan ser accesibles desde el Namespace PHONEBOOK. Si comprobamos las tablas en ambos nodos veremos que en mirrorA tenemos los datos tanto de la compañía como del contacto pero que el mirrorB aún no hay nada, como es lógico. Compañías registradas en mirrorA: Muy bien, procedamos a configurar las bases de datos en nuestro mirror. Para ello, desde nuestro nodo activo (mirrorA), accedemos a la pantalla de administración de las bases de datos locales (System Administrator --> Configuration --> System Configuration --> Local Databases) y pulsamos en la opción de Add to Mirror, seleccionamos de la lista las bases de datos que queremos añadir leyendo con detenimiento el mensaje que se nos muestra en pantalla: Una vez que hayamos añadido las bases de datos al mirror desde el nodo activo deberemos realizar un backup de las mismas o bien copiar los archivos de base de datos (IRIS.dat) y restaurarlos sobre el nodo pasivo. Si decidís realizar la copia directa de los archivos IRIS.dat tened en cuenta que debería realizarse previamente una pausa de la escritura en el fichero, podéis ver los comandos necesarios en la siguiente URL de la documentación. En nuestro ejemplo no será necesario realizar dicha pausa ya que nadie más que nosotros estamos escribiendo en ella. Antes de realizar dicha copia de los archivos de las bases de datos comprobemos el estado del mirror desde el monitor del nodo activo: Veamos ahora el nodo pasivo: Como podemos observar, desde el nodo pasivo se nos está informando que si bien tenemos configuradas 3 bases de datos en el mirror la configuración aún no está hecha. Procedamos a copiar las bases de datos del nodo activo al pasivo, no olvidemos que debemos desmontar las bases de datos del nodo pasivo para poder hacer la copia y para ello accederemos desde el portal de gestión a System Configuration --> Databases y accediendo a cada una de ellas procedemos a desmontarlas. ¡Perfecto! Bases de datos desmontadas. Accedamos al código del proyecto asociado al artículo desde Visual Studio Code y vemos que tenemos una serie de carpetas donde se encuentran las instalaciones de IRIS, sharedA para el mirrorA y sharedB para el mirrorB. Accedamos a las carpetas donde se encuentran las base de datos COMPANY, CUSTOMER y PERSONAL en (/sharedA/durable/mgr) y procedamos a copiar los IRIS.dat de cada base de datos en el mirror a los directorios oportunos del mirrorB (/sharedB/durable/mgr). Una vez finalizada la copia montamos nuevamente las bases de datos de mirrorB y comprobamos desde el monitor del mirror en mirrorB el estado de las bases de datos configuradas: ¡Bingo! nuestro mirror ha reconocido las bases de datos y ahora sólo necesitaremos activarlas y ponerlas al día. Para ello pulsaremos sobre la acción Activate y a continuación sobre Catchup, que aprecerá tras la activación. Veamos como quedan finalmente: Perfecto, nuestras bases de datos ya están correctamente configuradas en mirror, si consultamos la base de datos de COMPANY deberíamos ver la que registramos inicialmente desde mirrorA: Obviamente nuestra base de datos COMPANY tiene el registro que introdujimos con anterioridad en mirrorA, al fin y al cabo hemos copiado toda la base de datos. Procedamos a añadir una nueva empresa desde mirrorA a la que llamaremos "Another company" y procedamos a consultar nuevamente la tabla de la base de datos COMPANY: Aquí la tenemos. Únicamente nos quedará asegurarnos de que nuestras bases de datos configuradas en mirror se encuentran únicamente en modo lectura en nuestro nodo pasivo mirrorB: ¡Ahí están! en modo R de lectura. Pues ya tenemos nuestro mirror configurado y nuestras bases de datos sincronizadas. En el caso de que tuviesemos funcionando producciones no sería ningún problema ya que el mirror se encarga de forma automática de gestionarlas, arrancándolas en el nodo pasivo caso de caída del nodo activo. ¡Muchas gracias todos los que habéis llegado hasta este punto! Ha sido largo pero confío que os resulte de utilidad.
Artículo
Mario Sanchez Macias · 13 sep, 2022

Manual para realizar actualizaciones con éxito (consejos y trucos)

Nota: Lo que sigue es solo una guía. Cada cliente es diferente. A través de nuestra experiencia en soporte ayudando a clientes, hemos visto muchos casos en los que no tener un plan de actualización adecuado (y documentado) conduce a problemas inesperados con prioridad de Crisis. En algunos casos, podemos solucionar el problema durante el periodo de actualización, pero no siempre, ya que algunas situaciones pueden requerir una investigación más exhaustiva que puede llevar días o incluso meses. Es esencial documentar el proceso de actualización, incluyendo los pasos que se deben efectuar antes, durante y después de la actualización, ¡incluso en servidores y aplicaciones pequeñas! Además, un plan de actualización documentado es muy útil cuando involucra a terceros, como proveedores de software o hardware. Al entregar este documento a un proveedor externo (como InterSystems) se acelerará la comprensión de todo el contexto. Después de trabajar con diferentes documentos, me gustaría compartir unas instrucciones generales para ayudaros a crear un plan de actualización o para ponerlo al día. Por supuesto, se me escaparán algunas cosas. Estoy seguro de que algunos de vosotros tenéis mucha experiencia y podréis añadir ideas y sugerencias, ¡así que no dudéis en comentar este artículo! Nota: Lo que sigue es solo una guía. Cada cliente es diferente. Manual para realizar actualizaciones con éxito Cada actualización a una versión principal (major) debería tener unos pasos y estrategias imprescindibles, similares a las que se describen aquí. Nuestra experiencia con clientes demuestra que un plan detallado es clave para una actualización con éxito, con pruebas y acciones documentadas. Introducción Incluye una breve descripción de la actualización, sin detalles. Incluye el objetivo y la situación actual. 1. Infraestructura Incluye una tabla con los datos esenciales y documentos detallados (listos para ser enviados). Debe incluir la arquitectura actual y la futura (si corresponde). Arquitectura ACTUAL (ejemplo general) Servidor IP FUNCIÓN HW Producto Sistema Operativo PROD1 192.178.1.10 REPLICAR 10 cores 20 RAM Caché 2017.2 Windows 2012 PROD2 192.178.1.11 REPLICAR 10 cores 20 RAM Caché 2017.2 Windows 2012 192.178.1.30 VIP ARB 192.178.1.12 Árbitro 2 cores 4 RAM Caché 2017.2 Windows 2012 BCK 192.178.1.13 Recuperación en caso de desastre (DR) 24cores 10 RAM Caché 2017.2 Windows 2012 DetallesExtrae los siguientes datos y guárdalos en un sitio compartido, listos para ser enviados si alguien los requiere: Informe de ^Buttons o ^SystemCheck para cada servidor Informe de ^Buttons o ^SystemPerformance para cada servidor. Los datos deben abarcar unos cuantos días, incluyendo el día de mayor actividad Detalles del hardware (proveedor, especificaciones, etc.) Arquitectura FUTURA (si está prevista su actualización) Obtener datos similares a los de la arquitectura actual. 2. Contactos Puede parecer de sentido común, pero es esencial saber quién tiene autoridad para tomar decisiones relacionadas con las actualizaciones, quién está a cargo de qué y a quién hay que contactar en caso de emergencia. Es imprescindible disponer de una tabla sencilla adjunta al documento de actualización. Nombre Función Empresa Teléfono Correo electrónico John Administrador ACME +34222222 John@acme.com Gary Desarrollador ACME +34222222 Gary@acme.com Susan Gestor ACME +34222222 susana@acme.com Centro de Soporte Internacional (WRC) Soporte de InterSystems (ISC) InterSystems +112321321 support@intersytems.com Dell Soporte de Dell Dell +1xxx support@delll.com 3. Plan de prueba y detalles Incluye una descripción de las pruebas realizadas actualmente y las previstas. Las pruebas deben abarcar una actualización completa con un entorno similar. Las pruebas de actualización deberían permitir completar las siguientes secciones sobre la actualización. Si no hay pruebas o no se han completado, deberá anotarse y prepararse para cruzar los dedos. ;-) 4. Principales pasos de la actualización Debería incluir un resumen sencillo de los diferentes pasos. Por ejemplo: Stop instances (Prod1 and Prod2) Copy database folders to new systems (NewProd1 and NewProd2) Start NewProd1 Compile classes on new Prod1, run update data scripts Start NewProd2 Check Mirror synch Assign old IPs Allow users to connect 5. Detalles del plan Detalla los pasos anteriores y cómo realizarlos, incluyendo los comandos que se deben ejecutar, los archivos para importar, etc. La mayoría de los pasos no son útiles para empresas externas que no conocen las aplicaciones, y puede que no sea necesario incluirlas cuando se envíe el plan a terceros. 6. Tareas relacionadas Las actualizaciones, incluyendo los cambios en las aplicaciones, normalmente requieren tareas que hay que realizar antes, durante y después. Se puede crear una sencilla tabla de tareas para controlar todos los pasos, los tiempos, etc. Un plan detallado pueden incluir cosas como: Tareas previas a la actualización (ejemplo) (Detallar todas las tareas necesarias antes de la actualización) Tarea Fecha Estado Responsable Notas Actualizar índices 09/07/22 Hecho Integración Actualizar los índices en X para soportar XY Actualizar el IIS 09/08/22 Pendiente Administración Web Actualizar los servidores web antes de realizar la actualización final Obtener el informe de integridad 09/07/22 Hecho Administración de Iris Obtener un informe de integridad de todas las bases de datos Tareas durante la actualización (ejemplo) (Detallar las tareas y el plan de actualización paso a paso) Tarea Hora (planificada) Hora (real) Estado Responsable Notas Parar a los usuarios 06:00 06:10 Pendiente Integración Llamar a X para detener las conexiones Obtener ^SystemCheck 06:10 Pendiente Administración de Iris ... .. ... ... Permitir conexiones 09:10 Pendiente Administración de Iris Tareas después de la actualización (Detallar todas las tareas necesarias después de la actualización) Tarea Fecha Estado Responsable Notas Activar ^SystemPerformance 01/01/22 Pendiente Administración de Iris Ejecutar el funcionamiento durante unos días 5. Incidencias/Problemas durante la actualización Escribe los problemas encontrados durante la actualización. Especialmente los problemas que no impidan la actualización. Esta lista puede incluir una falta de comandos necesarios, mensajes de error nuevos, etc. Esta lista te ayudará a mejorar los documentos de actualización futuros y a que no te olvides de arreglar los problemas actuales. 6. Plan de recuperación en caso de desastres / Regresar Describe el plan en caso de que la actualización falle. Esto debería implicar volver a los antiguos servidores, detener los nuevos, iniciar X, Z, etc. Es útil tener escritos todos los pasos para volver a un escenario de seguridad. Recuerda que el plan de recuperación en caso de desastres normalmente se realiza bajo estrés, y es fácil omitir pasos y olvidar cómo hacerlo o dónde están los archivos, las configuraciones, etc. Este plan de recuperación debería ser un enlace o ampliación al documento de recuperación existente. Y sino hubiese ninguno, sería un buen momento de crearlo, ya sabéis, la ley de Murphy... * * * El objetivo principal de esta guía es que conozcas las dificultades del proceso de actualización y que pienses por adelantado. Probar, documentar y volver a probar es la garantía de actualizaciones con éxito.
Artículo
Ricardo Paiva · 25 feb, 2021

Yape - Otro extractor de pButtons (que también crea gráficos de forma automática)

> **Nota (junio de 2019)**: han cambiado muchas cosas [para obtener los detalles más recientes, haz clic aquí](https://community.intersystems.com/post/unpacking-pbuttons-yape-update-notes-and-quick-guides) > **Nota (septiembre de 2018)**: ha habido grandes cambios desde que esta publicación apareció por primera vez; sugiero que utilices la versión del contenedor en Docker dado que el proyecto y la información para que se ejecute como un contenedor sigue [publicada en GitHub](https://github.com/murrayo/yape), en el mismo lugar, para que puedas descargarlo, ejecutarlo y modificarlo, si lo necesitas. Cuando trabajo con clientes en revisiones de rendimiento, planificaciones de capacidad y resolución de problemas, con frecuencia tengo que descomprimir y revisar las métricas del sistema operativo y de caché desde pButtons. En vez de lidiar con los archivos html para cortar y pegar secciones que serán graficadas en Excel, [hace algún tiempo escribí una publicación sobre una herramienta para descomprimir las métricas de pButtons](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting), escrita con el intérprete de unix, perl y los scripts de awk. Si bien este es un *valioso ahorro de tiempo*, no es la historia completa… También utilizo scripts para realizar automáticamente gráficos de las métricas, para revisiones rápidas y para inconporarlos a informes. Sin embargo, estos gráficos realizados con scripts no se mantienen fácilmente y se vuelven especialmente confusos cuando se necesita una configuración específica del sitio, por ejemplo, una lista de discos para iostat o windows perfmon. Por eso, nunca publiqué las herramientas para hacer gráficos. Pero estoy muy contento de anunciar que ahora existe una solución mucho más sencilla. El descubrimiento fortuito ocurrió cuando estaba en las instalaciones de un cliente observando el rendimiento del sistema con [ Fabian, y él me mostró lo que hacía para utilizar ciertos módulos de Python para realizar gráficos](https://community.intersystems.com/post/visualizing-data-jungle-part-i-lets-make-graph). Esta es una solución mucho más flexible y fácil de mantener que los scripts que utilizaba. Y la facilidad de integrar los módulos de Python para administrar archivos y gráficos, incluyendo la posibilidad de compartir el html interactivo, significa que el resultado puede ser mucho más útil. Al tomar las publicaciones de Fabian como base, escribí __Yape__ para extraer de forma rápida y sencilla, y luego hacer gráficos de los archivos pButtons de los clientes, en varios formatos. El proyecto se [publicó en GitHub](https://github.com/murrayo/yape) para que puedas descargarlo, ejecutarlo y modificarlo, si lo necesitas. ## Resumen Actualmente, este proceso consiste en _dos_ pasos. ### Paso 1. `extract_pButtons.py` Extrae las secciones de interés desde pButtons, y escríbelas en archivos de tipo .csv para abrirlos con Excel o para que se procesen con gráficos utilizando `graph_pButtons.py`. ### Paso 2. `graph_pButtons.py` Los archivos de gráficos se crearon en el Paso 1. Actualmente las salidas pueden ser gráficas de líneas o de puntos que se representan como `.png` o `.html interactivos` e incluyen opciones de vista panorámica, zoom, impresión, etc. _Readme.md_ en GitHub tiene especificaciones sobre cómo configurar y ejecutar los dos scripts de Python, y será la referencia más reciente. ## Notas adicionales Por ejemplo: si utilizas las opciones para agregar prefijos a los directorios de entrada y salida, podrás examinar fácilmente un directorio con un conjunto de archivos pButtons de html (por ejemplo, semanas) y enviarlos a un directorio separado para cada archivo pButtons. for i in `ls *.html`; do ./extract_pButtons.py $i -p ${i}_; done for i in `ls *.html`; do ./graph_pButtons.py ./${i}_metrics -p ${i}_; done A corto plazo, mientras continúo con la serie sobre [cómo planificar y aumentar rendimiento de la plataforma de datos InterSystems IRIS](https://community.intersystems.com/post/intersystems-data-platforms-capacity-planning-and-performance-series-index), utilizaré los gráficos creados con estas herramientas. Lo probé en OS X, pero no en Windows. No debería tener ningún problema para instalar y ejecutar Python en Windows. Supongo que necesitarás realizar algunos cambios en las barras que se encuentran en la ruta de un archivo. Deja tus comentarios si lo pruebas. > Nota: hasta hace unas semanas nunca había escrito nada en Python, así que si es un experto en este lenguaje, posiblemente te darás cuenta de que algunas partes del código no siguen las prácticas recomendadas. Sin embargo, utilizo los scripts casi todos los días, por lo que seguiré mejorándolos. Espero perfeccionar mis habilidades en Python, ¡pero siéntete libre de “educarme” si ves algo que debería corregirse! Si descubres que los scripts te resultan útiles, házmelo saber y vuelve con frecuencia para obtener nuevas características y actualizaciones.
Artículo
Pablo Frigolett · 22 nov, 2021

Python en Servidor de lenguajes externos en un contenedor.

Servidor Externo de Lenguaje Python en un contenedor La primera vez que se intenta iniciar un servidor externo de lenguaje Python (de aquí en adelante Gateway de Python), en la versión en contenedor, intenta ejecutar cierto código para detectar si está instalado el paquete python3-venv. La imagen que está en containers.intersystems.com: a la fecha de este artículo es la versión 2021.1.0.215.0 está basada en Ubuntu 18 LTS viene con python 3.6.9 instalado Para trascender al contenedor en inicio de nuestro Gateway de Python - para las actualizaciones por ejemplo - es necesario tener un punto de montaje externo para /usr/irissys/dev/python/virtual. Allí se registra los entornos virtuales de python que IRIS crea cuando inicia por primera vez un Gateway de Python. El usuario de sistema operativo que ejecuta los scripts de inicio del Gateway de Python, debe poder escribir en ese directorio. La razón es que además de crear allí el entorno virtual de python, cada vez que se intenta iniciar el Gateway, intenta crear un archivo temporal de prueba allí. A continuación haremos: creación de un contenedor con pasos adicionales para el inicio por primera vez del Gateway de Python. Los pasos adicionales son incluir un punto de mensaje, instalar unos paquetes e iniciar por primera vez el Gateway de Python. creación de un contenedor con menos pasos adicionales para los subsecuentes inicios del Gateway Python. Los pasos adicionales son incluir el punto de montaje para python y darle privilegio de escritura a todos los usuarios. Creación de un contenedor e inicio del Gateway de Python por primera vez Este paso crea el contenedor de la misma forma que se creará en otras ocasiones agregando la instalación del paquete python3-venv y sus dependencias, el inicio del Gateway de Python. Se asume que en el directorio /data/durable (también en negrita en el comando) está el archivo iris.key. Y la versión del contenedor es la que existía al momento de escribir este artículo. En el directorio /data/python se creará el entorno virtual de python. Ese directorio se vuelve read-only al menos en versiones docker-desktop en Mac y docker-ce en linux. Por eso el comando que sigue a la creación busca dar permiso para la creación del entorno virtual de python al usuario que ejecute IRIS en el contenedor. Entonces para crear el contenedor aqui va el método más crudo (directo con docker): docker run -d -v /data/durable:/dur -v /data/python:/usr/irissys/dev/python/virtual --name iris --cap-add IPC_LOCK containers.intersystems.com/intersystems/iris:2021.1.0.215.0 --key /dur/iris.key chmod 777 /data/python Ahora es necesario instalar el paquete python3-venv con sus dependencias e iniciar el Gateway de Python. El contenedor necesita acceso a internet y el comando se ejecuta como root (-u 0). docker exec -u 0 iris bash -c "apt-get update && apt-get install -y python3-venv" Si todo va bien, veremos en la últimas líneas "Setting up python3-venv" entre otros mensajes. Con esto, solamente faltaría iniciar el Gateway de Python que puede tomar desde los 20s mientras crea el entorno virtual, instala el módulo de iris nativo e inicia el proceso : docker exec iris iris terminal IRIS <<EOFCMD w "Iniciando:",\$system.external.startServer("%Python Server") w "Terminando:",\$system.external.stopServer("%Python Server") halt EOFCMD con esto, el entorno virtual queda creado en /data/python/'%Python Server_3252368016' que puede ser compartido con otros contenedores. Para no seguir con un contenedor con paquetes instalados inútilmente, eliminamos el contenedor para volver a crearlo en el próximo paso. docker stop iris docker rm iris Creación de un contenedor con menos pasos adicionales Ya creado nuestro entorno virtual Python, el único paso adicional necesario a dar permiso de escritura en la carpeta que es montada (en Mac y Linux al menos). De nuevo, directo desde docker: docker run -d -v /data/durable:/dur -v /data/python:/usr/irissys/dev/python/virtual --name iris --cap-add IPC_LOCK containers.intersystems.com/intersystems/iris:2021.1.0.215.0 --key /dur/iris.key chmod 777 /data/python El tema de los permisos en mi caso fue devastador. Tuve la suerte de encontrarme con ese problema y tener que indagar qué estaba haciendo que fallara el inicio del Gateway de Python. Los scripts setup.sh y runpython.sh ubicados en /usr/irissys/dev/python eran los que se caian con diversos problemas (que no podían crear un archivo, que no estaba el paquete python3-venv, etc.). Suerte!
Artículo
Ricardo Paiva · 9 feb, 2023

iris-tripleslash - ¡Toquemos juntos!

Hola a todos, Aquí estamos de nuevo. Nuevo año, nuevo concurso, nuevo proyecto, viejos motivos. ¡Triple Slash ya está en casa! 1999, el año que aprendí a programar, mi primer "if," mi primer "Hello world." Aún recuerdo a mi profesor explicándonos en aquella clase el sencillo "while" y cómo podemos saber si se cumplió una condición específica. ¿Te acuerdas, @Renato.Banzai? El profesor Barbosa, un tipo único. Desde entonces, me ha encantado la idea de programar, transformando ideas en proyectos, en algo útil. Pero todos sabemos que para crear algo, necesitamos asegurarnos de que está funcionando; necesitamos no solo crear, sino también probar si funciona y si no se rompe si añadimos algo nuevo. Y para ser honesto con todos vosotros, hacer pruebas es aburrido. Al menos para mí, no tengo nada contra vosotros si os gusta. Usando una analogía, podría decir que crear métodos de prueba es como limpiar la casa o planchar la ropa. Es aburrido, pero necesario. Con estas ideas en mente, ¿por qué no desarrollar una forma mejor y más sencilla de probar? Así que, inspirado por el estilo de elixir y por esta idea de InterSystems Ideas (gracias, @Evgeny.Shvarov), intentamos mejorar el proceso de prueba y convertirlo en una tarea divertida otra vez. Simplificamos el %UnitTest y para mostraros cómo usar TripleSlash para crear pruebas unitarias (unit tests), vamos a utilizar un ejemplo sencillo. Pongamos que tienes la siguiente clase y método del que te gustaría escribir una prueba unitaria: Class dc.sample.ObjectScript { ClassMethod TheAnswerForEverything() As %Integer { Set a = 42 Write "Hello World!",! Write "This is InterSystems IRIS with version ",$zv,! Write "Current time is: "_$zdt($h,2) Return a } } Como se puede ver, el método TheAnswerForEverything() solo devuelve el número 42. Así que vamos a marcar en la documentación del método cómo TripleSlash debería crear una prueba unitaria para este método: /// A simple method for testing purpose. /// /// <example> /// Write ##class(dc.sample.ObjectScript).Test() /// 42 /// </example> ClassMethod TheAnswerForEverything() As %Integer { ... } Las pruebas unitarias deben estar todas en una etiqueta <example></example>. Se puede añadir cualquier tipo de documentación, pero todas las pruebas tienen que estar dentro de ese tipo de etiqueta. Ahora, arranca una instancia de IRIS e inicia una sesión de terminal, ve al namespace IRISAPP, crea una instancia de la clase Core pasando el nombre de clase (o su nombre de paquete para todas sus clases) y después ejecuta el método Execute(): USER>ZN "IRISAPP" IRISAPP>Do ##class(iris.tripleSlash.Core).%New("dc.sample.ObjectScript").Execute() TripleSlash interpretará esto como "Dado el resultado del método Test(), afirmo que es igual a 42". Así que se creará una nueva clase dentro de la prueba unitaria: Class iris.tripleSlash.tst.ObjectScript Extends %UnitTest.TestCase { Method TestTheAnswerForEverything() { Do $$$AssertEquals(##class(dc.sample.ObjectScript).TheAnswerForEverything(), 42) } } Ahora, vamos a añadir un nuevo método para probar otras formas de decir a TripleSlash cómo escribir las pruebas unitarias. Class dc.sample.ObjectScript { ClassMethod GuessTheNumber(pNumber As %Integer) As %Status { Set st = $$$OK Set theAnswerForEveryThing = 42 Try { Throw:(pNumber '= theAnswerForEveryThing) ##class(%Exception.StatusException).%New("Sorry, wrong number...") } Catch(e) { Set st = e.AsStatus() } Return st } } Como se puede ver, el método GuessTheNumber() espera un número, devuelve $$$OK solo cuando se pasa como argumento el número 42 o un error para cualquier otro valor. Así que vamos a marcar en la documentación del método cómo TripleSlash debería crear una prueba unitaria para este método: /// Another simple method for testing purpose. /// /// <example> /// Do ##class(dc.sample.ObjectScript).GuessTheNumber(42) /// $$$OK /// Do ##class(dc.sample.ObjectScript).GuessTheNumber(23) /// $$$NotOK /// </example> ClassMethod GuessTheNumber(pNumber As %Integer) As %Status { ... } Ejecuta otra vez el método Execute() y verás un nuevo método de prueba en la clase de prueba unitaria iris.tripleSlash.tst.ObjectScript: Class iris.tripleSlash.tst.ObjectScript Extends %UnitTest.TestCase { Method TestGuessTheNumber() { Do $$$AssertStatusOK(##class(dc.sample.ObjectScript).GuessTheNumber(42)) Do $$$AssertStatusNotOK(##class(dc.sample.ObjectScript).GuessTheNumber(23)) } } En este momento, las siguientes afirmaciones están disponibles: $$$AssertStatusOK, $$$AssertStatusNotOK y $$$AssertEquals. TripleSlash nos permite generar pruebas desde ejemplos de código que se encuentran en las descripciones de métodos. Te ayuda a matar dos pájaros de un tiro, mejorando tu documentación de clase y creando automatización de pruebas. ReconocimientoUna vez más, me gustaría agradecer todo el apoyo de la Comunidad en todas las aplicaciones que creamos! Muchas gracias @Ricardo.Paiva Muito obrigado pelo excelente artigo @Henrique.GonçalvesDias
Artículo
David Reche · 9 jun, 2019

Cómo hacer preguntas de calidad como entradas de la Comunidad

¡Hola Comunidad!Este artículo es una guía sencilla sobre cómo preguntar y cómo conseguir respuestas en la Comunidad. Ya que el objetivo obvio cuando publicamos una pregunta en la Comunidad es obtener una respuesta, veamos cómo conseguir buenas preguntas que tengan visibilidad para encontrar fácilmente.Cuando se publica una pregunta es necesario completar tres campos: Título, Cuerpo y Grupo, además de las etiquetas.1. El TítuloUn buen título debería contener una descripción breve de tu problema - no debería ser más largo de 80 o 90 caracteres.Pero tampoco debería ser demasiado breve - un título de una única palabra tampoco es una buena idea. Ejemplos de buenas preguntas:Consultar una lista de propiedades con SQLCómo ignorar la cabecera en un fichero CSV cuando se usa Record MapperEquivalente a $CASE o $SELECT en SQL2. El CuerpoEl cuerpo debería contener una descripción de tu problema de manera textual y opcionalmente con un ejemplo de código como ObjectScript, SQL, JS u otros lenguajes. Utiliza bloques de código para resaltar el código ObjectScript.Proporcionar la versión del producto utilizado siempre es útil (puedes obtenerla con $zversion desde el Terminal).Y siempre que sea posible, pregunta solo una cosa en el cuerpo. Si tienes más de una pregunta y puedes separarlas, es mejor crear dos entradas diferentes. De esta forma, será más fácil para otros miembros encontrar respuestas a tus preguntas.3. El GrupoEl grupo es una etiqueta obligatoria que ayuda a categorizar tus preguntas, asociándolas a uno de los Productos de InterSystems (IRIS, Caché, Ensemble, HealthShare), Tecnologías (DeepSee, iKnow) or Servicios (Online Learning, WRC).4. EtiquetasUtiliza etiquetas para facilitar a otros miembros expertos (que están suscritos a diferentes etiquetas) a encontrar tu pregunta. Puedes elegir diferentes etiquetas relacionadas con desarrollo, pruebas, gestión de cambios, despliegues y entornos.Si preguntas correctamente y el texto es claro, se suelen obtener respuestas en poco tiempo. Puedes observar qué preguntas tienen respuestas en el contador de respuestas con fondo verde, a la derecha de cada pregunta. Y acuérdate de marcar una respuesta como aceptada (indicando que la pregunta ha sido resuelta) en caso de que la respuesta resuelva tu duda y se ajuste a lo que necesitabas.Por supuesto, esta no es la lista definitiva de recomendaciones sobre cómo hacer buenas preguntas. Así que puedes añadir tus comentarios e ideas sobre el post, para complementar lo que creas oportuno. ¡Gracias!
Artículo
Nancy Martínez · 20 abr, 2021

Docker - durabilidad ligera

Al trabajar desde casa durante estos "días de coronavirus", me faltan recursos. - no tengo ninguna máquina Linux disponible - espacio en disco limitado Además, Docker Desktop (en Windows10) bloqueó de alguna manera los scripts de [Durable %SYS](https://docs.intersystems.com/iris20192/csp/docbook/Doc.View.cls?KEY=ADOCK_iris_durable) como se describe [**aquí.**](https://community.intersystems.com/post/docker-vs-durability) Investigando el caso, descubrí que se almacenaban muchos más datos de los que realmente necesitaba. Así que diseñé mi durabilidad personalizada. De forma similar a lo que hice tiempo atrás para los contenedores de Caché. Utilizando las características de [**iris-main **](https://docs.intersystems.com/iris20192/csp/docbook/Doc.View.cls?KEY=ADOCK#ADOCK_iris_iscmain)agrego un script de pre-procesamiento para iniciar mi contenedor con  Después de que termina, guardo lo que creo necesitar para el próximo inicio. Y eso es bastante menos. De acuerdo: Puede que se me escapen algunas cosas y finalmente es más lento en start/stop. Muy bien. ¡Pero está en mis manos!   Los scripts para ejecutar y también los datos guardados están almacenados en el directorio externo que necesito para la licencia. Y esos scripts son bastante sencillos: # post.copycp -uf /usr/irissys/iris.cpf /external/iris.cpfcp -uf /usr/irissys/mgr/IRIS.DAT /external/mgr/IRIS.DATcp -uf /usr/irissys/mgr/messages.log /external/console.log y # pre.copycp -f /external/iris.cpf /usr/irissys/iris.cpfcp -f /external/mgr/IRIS.DAT /usr/irissys/mgr/IRIS.DATrm -f /usr/irissys/mgr/messages.log Como no hay nada que configurar en la primera ejecución, omito pre.copy como la primera ejecución hecha. Por lo demás, mi comando de ejecución en Docker tiene el siguiente aspecto: docker run --name iris1 --init -it  -p 52773:52773 -p 51773:51773  --volume c:/external:/external  --rm intersystems/iris:2020.2.0.198.0   --key /external/key/iris.key   -e /external/post.copy   -b /external/pre.copy  Si tiempo después descubro que necesito guardar/recuperar algo más (por ejemplo, para CSP, ... ) es fácil añadirlo. La clave del éxito fue dejar la base de datos IRISSYS en la ubicación que se incluye en el contenedor. Su tamaño de ~92 MB (en mi caso) no es relevante.
Anuncio
Esther Sanchez · 8 jun, 2022

Mejoras en la Comunidad de Desarrolladores, mayo 2022

¡Hola desarrolladores! Hemos hecho algunos cambios en los sitios web de las Comunidades de Desarrolladores de InterSystems: 🆕 Mejor seguimiento de eventos en marcha 🆕 Programación de publicaciones 🆕 Formato del código mejorado 🆕 Creación de tablas mejorada 🆕 Mejor seguimiento de respuestas 🆕 Nuevo diseño en la parte inferior de las publicaciones Vamos a explicar en detalle cada uno de ellos. Eventos EN MARCHA AHORA Para que la búsqueda de eventos sea aún más sencilla, hemos añadido una nueva sección "EN MARCHA AHORA" en la esquina superior derecha de la página. Si hacéis clic ahí, iréis a la página del evento. Programación de publicaciones Lo habíais pedido... ¡y ya está aquí! Ahora se pueden programar las publicaciones, para que se publiquen en un momento determinado. Para programar una publicación, solo hay que hacer clic en la flecha hacia abajo al lado del botón "Publicar" y elegir "Programar publicación". Aparecerá un calendario en el que se puede elegir el día y hora en el que se quiere publicar. Después, hay que hacer clic en el botón "Programar publicación" y la publicación se publicará en el día y hora elegida. ¡Así de fácil! Formato del código Para compartir el código con otras personas, hemos añadido un editor integrado cuando se introduce código. En él, se puede elegir el lenguaje de programación y el tamaño de la tabulación. Además, el resaltado de sintaxis se produce automáticamente cuando escribes el texto. Y el lenguaje de programación se muestra en la esquina superior izquierda. Como resultado, en tu publicación verás el código bonito y ordenado, en el lenguaje de programación elegido. Creación de tablas Para simplificar el formato de las tablas, hemos añadido una función para crear tablas rápidamente, solo eligiendo el número de celdas que se necesiten. Al hacer clic en el botón "More" (Más) se abre una ventana para configurar las propiedades de la tabla. Respuestas y suscripciones Para ver toda la información sobre las respuestas de una publicación, hemos añadido el número de respuestas y también el icono de suscribirse al debate, para recibir notificaciones de las nuevas respuestas. Nuevo diseño en la parte inferior de las publicaciones Hemos reorganizado y cambiado los iconos en la parte inferior de las publicaciones. ¡Esperamos que os resulten útiles estos cambios! Podéis solicitar mejoras o reportar errores en el GitHub de la Comunidad. O en los comentarios de esta publicación, claro. ¡Muchas gracias!